Merge "Default display topology initialization" into main
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 67e2ca2..ec1ec3a 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -45,6 +45,7 @@
 import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL;
 import static android.os.Process.FIRST_APPLICATION_UID;
 import static android.os.Process.ROOT_UID;
+import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
 import static android.provider.Settings.Secure.RESOLUTION_MODE_FULL;
 import static android.provider.Settings.Secure.RESOLUTION_MODE_HIGH;
 import static android.provider.Settings.Secure.RESOLUTION_MODE_UNKNOWN;
@@ -655,12 +656,14 @@
         mExtraDisplayLoggingPackageName = DisplayProperties.debug_vri_package().orElse(null);
         mExtraDisplayEventLogging = !TextUtils.isEmpty(mExtraDisplayLoggingPackageName);
 
-        mExternalDisplayStatsService = new ExternalDisplayStatsService(mContext, mHandler);
+        mExternalDisplayStatsService = new ExternalDisplayStatsService(mContext, mHandler,
+                this::isExtendedDisplayEnabled);
         mDisplayNotificationManager = new DisplayNotificationManager(mFlags, mContext,
                 mExternalDisplayStatsService);
         mExternalDisplayPolicy = new ExternalDisplayPolicy(new ExternalDisplayPolicyInjector());
         if (mFlags.isDisplayTopologyEnabled()) {
-            mDisplayTopologyCoordinator = new DisplayTopologyCoordinator();
+            mDisplayTopologyCoordinator =
+                    new DisplayTopologyCoordinator(this::isExtendedDisplayEnabled);
         } else {
             mDisplayTopologyCoordinator = null;
         }
@@ -2262,6 +2265,17 @@
         updateLogicalDisplayState(display);
     }
 
+    private boolean isExtendedDisplayEnabled() {
+        try {
+            return 0 != Settings.Global.getInt(
+                    mContext.getContentResolver(),
+                    DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0);
+        } catch (Throwable e) {
+            // Some services might not be initialised yet to be able to call getInt
+            return false;
+        }
+    }
+
     @SuppressLint("AndroidFrameworkRequiresPermission")
     private void handleLogicalDisplayAddedLocked(LogicalDisplay display) {
         final int displayId = display.getDisplayIdLocked();
diff --git a/services/core/java/com/android/server/display/DisplayTopology.java b/services/core/java/com/android/server/display/DisplayTopology.java
new file mode 100644
index 0000000..90038a0
--- /dev/null
+++ b/services/core/java/com/android/server/display/DisplayTopology.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2024 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.server.display;
+
+import android.annotation.Nullable;
+import android.util.IndentingPrintWriter;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents the relative placement of extended displays.
+ */
+class DisplayTopology {
+    private static final String TAG = "DisplayTopology";
+
+    /**
+     * The topology tree
+     */
+    @Nullable
+    @VisibleForTesting
+    TreeNode mRoot;
+
+    /**
+     * The logical display ID of the primary display that will show certain UI elements.
+     * This is not necessarily the same as the default display.
+     */
+    @VisibleForTesting
+    int mPrimaryDisplayId;
+
+    /**
+     * Add a display to the topology.
+     * If this is the second display in the topology, it will be placed above the first display.
+     * Subsequent displays will be places to the left or right of the second display.
+     * @param displayId The ID of the display
+     * @param width The width of the display
+     * @param height The height of the display
+     */
+    void addDisplay(int displayId, double width, double height) {
+        if (mRoot == null) {
+            mRoot = new TreeNode(displayId, width, height, /* position= */ null, /* offset= */ 0);
+            mPrimaryDisplayId = displayId;
+            Slog.i(TAG, "First display added: " + mRoot);
+        } else if (mRoot.mChildren.isEmpty()) {
+            // This is the 2nd display. Align the middles of the top and bottom edges.
+            double offset = mRoot.mWidth / 2 - width / 2;
+            TreeNode display = new TreeNode(displayId, width, height,
+                    TreeNode.Position.POSITION_TOP, offset);
+            mRoot.mChildren.add(display);
+            Slog.i(TAG, "Second display added: " + display + ", parent ID: " + mRoot.mDisplayId);
+        } else {
+            TreeNode rightMostDisplay = findRightMostDisplay(mRoot, mRoot.mWidth).first;
+            TreeNode newDisplay = new TreeNode(displayId, width, height,
+                    TreeNode.Position.POSITION_RIGHT, /* offset= */ 0);
+            rightMostDisplay.mChildren.add(newDisplay);
+            Slog.i(TAG, "Display added: " + newDisplay + ", parent ID: "
+                    + rightMostDisplay.mDisplayId);
+        }
+    }
+
+    /**
+     * Print the object's state and debug information into the given stream.
+     * @param pw The stream to dump information to.
+     */
+    void dump(PrintWriter pw) {
+        pw.println("DisplayTopology:");
+        pw.println("--------------------");
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
+        ipw.increaseIndent();
+
+        ipw.println("mPrimaryDisplayId: " + mPrimaryDisplayId);
+
+        ipw.println("Topology tree:");
+        if (mRoot != null) {
+            ipw.increaseIndent();
+            mRoot.dump(ipw);
+            ipw.decreaseIndent();
+        }
+    }
+
+    /**
+     * @param display The display from which the search should start.
+     * @param xPos The x position of the right edge of that display.
+     * @return The display that is the furthest to the right and the x position of the right edge
+     * of that display
+     */
+    private Pair<TreeNode, Double> findRightMostDisplay(TreeNode display, double xPos) {
+        Pair<TreeNode, Double> result = new Pair<>(display, xPos);
+        for (TreeNode child : display.mChildren) {
+            // The x position of the right edge of the child
+            double childXPos;
+            switch (child.mPosition) {
+                case POSITION_LEFT -> childXPos = xPos - display.mWidth;
+                case POSITION_TOP, POSITION_BOTTOM ->
+                        childXPos = xPos - display.mWidth + child.mOffset + child.mWidth;
+                case POSITION_RIGHT -> childXPos = xPos + child.mWidth;
+                default -> throw new IllegalStateException("Unexpected value: " + child.mPosition);
+            }
+
+            // Recursive call - find the rightmost display starting from the child
+            Pair<TreeNode, Double> childResult = findRightMostDisplay(child, childXPos);
+            // Check if the one found is further right
+            if (childResult.second > result.second) {
+                result = new Pair<>(childResult.first, childResult.second);
+            }
+        }
+        return result;
+    }
+
+    @VisibleForTesting
+    static class TreeNode {
+
+        /**
+         * The logical display ID
+         */
+        @VisibleForTesting
+        final int mDisplayId;
+
+        /**
+         * The width of the display in density-independent pixels (dp).
+         */
+        @VisibleForTesting
+        double mWidth;
+
+        /**
+         * The height of the display in density-independent pixels (dp).
+         */
+        @VisibleForTesting
+        double mHeight;
+
+        /**
+         * The position of this display relative to its parent.
+         */
+        @VisibleForTesting
+        Position mPosition;
+
+        /**
+         * The distance from the top edge of the parent display to the top edge of this display (in
+         * case of POSITION_LEFT or POSITION_RIGHT) or from the left edge of the parent display
+         * to the left edge of this display (in case of POSITION_TOP or POSITION_BOTTOM). The unit
+         * used is density-independent pixels (dp).
+         */
+        @VisibleForTesting
+        double mOffset;
+
+        @VisibleForTesting
+        final List<TreeNode> mChildren = new ArrayList<>();
+
+        TreeNode(int displayId, double width, double height, Position position,
+                double offset) {
+            mDisplayId = displayId;
+            mWidth = width;
+            mHeight = height;
+            mPosition = position;
+            mOffset = offset;
+        }
+
+        /**
+         * Print the object's state and debug information into the given stream.
+         * @param ipw The stream to dump information to.
+         */
+        void dump(IndentingPrintWriter ipw) {
+            ipw.println(this);
+            ipw.increaseIndent();
+            for (TreeNode child : mChildren) {
+                child.dump(ipw);
+            }
+            ipw.decreaseIndent();
+        }
+
+        @Override
+        public String toString() {
+            return "Display {id=" + mDisplayId + ", width=" + mWidth + ", height=" + mHeight
+                    + ", position=" + mPosition + ", offset=" + mOffset + "}";
+        }
+
+        @VisibleForTesting
+        enum Position {
+            POSITION_LEFT, POSITION_TOP, POSITION_RIGHT, POSITION_BOTTOM
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java
index 631f147..cbd224c 100644
--- a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java
+++ b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java
@@ -16,89 +16,91 @@
 
 package com.android.server.display;
 
-import android.annotation.Nullable;
-import android.util.IndentingPrintWriter;
+import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.DisplayInfo;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.function.BooleanSupplier;
 
 /**
- * This class manages the relative placement (topology) of extended displays. It is responsible for
- * updating and persisting the topology.
+ * Manages the relative placement (topology) of extended displays. Responsible for updating and
+ * persisting the topology.
  */
 class DisplayTopologyCoordinator {
 
-    /**
-     * The topology tree
-     */
-    @Nullable
-    private TopologyTreeNode mRoot;
+    @GuardedBy("mLock")
+    private final DisplayTopology mTopology;
 
     /**
-     * The logical display ID of the primary display that will show certain UI elements.
-     * This is not necessarily the same as the default display.
+     * Check if extended displays are enabled. If not, a topology is not needed.
      */
-    private int mPrimaryDisplayId;
+    private final BooleanSupplier mIsExtendedDisplayEnabled;
+
+    private final Object mLock = new Object();
+
+    DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayEnabled) {
+        this(new Injector(), isExtendedDisplayEnabled);
+    }
+
+    @VisibleForTesting
+    DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayEnabled) {
+        mTopology = injector.getTopology();
+        mIsExtendedDisplayEnabled = isExtendedDisplayEnabled;
+    }
+
+    /**
+     * Add a display to the topology.
+     * @param info The display info
+     */
+    void onDisplayAdded(DisplayInfo info) {
+        if (!isDisplayAllowedInTopology(info)) {
+            return;
+        }
+        synchronized (mLock) {
+            mTopology.addDisplay(info.displayId, getWidth(info), getHeight(info));
+        }
+    }
 
     /**
      * Print the object's state and debug information into the given stream.
      * @param pw The stream to dump information to.
      */
-    public void dump(PrintWriter pw) {
-        pw.println("DisplayTopologyCoordinator:");
-        pw.println("--------------------");
-        IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
-        ipw.increaseIndent();
-
-        ipw.println("mPrimaryDisplayId: " + mPrimaryDisplayId);
-
-        ipw.println("Topology tree:");
-        if (mRoot != null) {
-            ipw.increaseIndent();
-            mRoot.dump(ipw);
-            ipw.decreaseIndent();
+    void dump(PrintWriter pw) {
+        synchronized (mLock) {
+            mTopology.dump(pw);
         }
     }
 
-    private static class TopologyTreeNode {
+    /**
+     * @param info The display info
+     * @return The width of the display in dp
+     */
+    private double getWidth(DisplayInfo info) {
+        return info.logicalWidth * (double) DisplayMetrics.DENSITY_DEFAULT
+                / info.logicalDensityDpi;
+    }
 
-        /**
-         * The logical display ID
-         */
-        private int mDisplayId;
+    /**
+     * @param info The display info
+     * @return The height of the display in dp
+     */
+    private double getHeight(DisplayInfo info) {
+        return info.logicalHeight * (double) DisplayMetrics.DENSITY_DEFAULT
+                / info.logicalDensityDpi;
+    }
 
-        private final List<TopologyTreeNode> mChildren = new ArrayList<>();
+    private boolean isDisplayAllowedInTopology(DisplayInfo info) {
+        return mIsExtendedDisplayEnabled.getAsBoolean()
+                && info.displayGroupId == Display.DEFAULT_DISPLAY_GROUP;
+    }
 
-        /**
-         * The position of this display relative to its parent.
-         */
-        private Position mPosition;
-
-        /**
-         * The distance from the top edge of the parent display to the top edge of this display (in
-         * case of POSITION_LEFT or POSITION_RIGHT) or from the left edge of the parent display
-         * to the left edge of this display (in case of POSITION_TOP or POSITION_BOTTOM). The unit
-         * used is density-independent pixels (dp).
-         */
-        private double mOffset;
-
-        /**
-         * Print the object's state and debug information into the given stream.
-         * @param ipw The stream to dump information to.
-         */
-        void dump(IndentingPrintWriter ipw) {
-            ipw.println("Display {id=" + mDisplayId + ", position=" + mPosition
-                    + ", offset=" + mOffset + "}");
-            ipw.increaseIndent();
-            for (TopologyTreeNode child : mChildren) {
-                child.dump(ipw);
-            }
-            ipw.decreaseIndent();
-        }
-
-        private enum Position {
-            POSITION_LEFT, POSITION_TOP, POSITION_RIGHT, POSITION_BOTTOM
+    static class Injector {
+        DisplayTopology getTopology() {
+            return new DisplayTopology();
         }
     }
 }
diff --git a/services/core/java/com/android/server/display/ExternalDisplayStatsService.java b/services/core/java/com/android/server/display/ExternalDisplayStatsService.java
index 608fb35..666bd26 100644
--- a/services/core/java/com/android/server/display/ExternalDisplayStatsService.java
+++ b/services/core/java/com/android/server/display/ExternalDisplayStatsService.java
@@ -19,7 +19,6 @@
 import static android.media.AudioDeviceInfo.TYPE_HDMI;
 import static android.media.AudioDeviceInfo.TYPE_HDMI_ARC;
 import static android.media.AudioDeviceInfo.TYPE_USB_DEVICE;
-import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -32,7 +31,6 @@
 import android.media.AudioPlaybackConfiguration;
 import android.os.Handler;
 import android.os.PowerManager;
-import android.provider.Settings;
 import android.util.Slog;
 import android.util.SparseIntArray;
 import android.view.Display;
@@ -44,6 +42,7 @@
 import com.android.server.display.utils.DebugUtils;
 
 import java.util.List;
+import java.util.function.BooleanSupplier;
 
 
 /**
@@ -203,8 +202,9 @@
         }
     };
 
-    ExternalDisplayStatsService(Context context, Handler handler) {
-        this(new Injector(context, handler));
+    ExternalDisplayStatsService(Context context, Handler handler,
+            BooleanSupplier isExtendedDisplayEnabled) {
+        this(new Injector(context, handler, isExtendedDisplayEnabled));
     }
 
     @VisibleForTesting
@@ -599,25 +599,21 @@
         private final Context mContext;
         @NonNull
         private final Handler mHandler;
+        private final BooleanSupplier mIsExtendedDisplayEnabled;
         @Nullable
         private AudioManager mAudioManager;
         @Nullable
         private PowerManager mPowerManager;
 
-        Injector(@NonNull Context context, @NonNull Handler handler) {
+        Injector(@NonNull Context context, @NonNull Handler handler,
+                BooleanSupplier isExtendedDisplayEnabled) {
             mContext = context;
             mHandler = handler;
+            mIsExtendedDisplayEnabled = isExtendedDisplayEnabled;
         }
 
         boolean isExtendedDisplayEnabled() {
-            try {
-                return 0 != Settings.Global.getInt(
-                        mContext.getContentResolver(),
-                        DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0);
-            } catch (Throwable e) {
-                // Some services might not be initialised yet to be able to call getInt
-                return false;
-            }
+            return mIsExtendedDisplayEnabled.getAsBoolean();
         }
 
         void registerInteractivityReceiver(BroadcastReceiver interactivityReceiver,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt
new file mode 100644
index 0000000..17af633
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 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.server.display
+
+import android.util.DisplayMetrics
+import android.view.Display
+import android.view.DisplayInfo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyDouble
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import java.util.function.BooleanSupplier
+
+class DisplayTopologyCoordinatorTest {
+    private lateinit var coordinator: DisplayTopologyCoordinator
+    private val displayInfo = DisplayInfo()
+
+    private val mockTopology = mock<DisplayTopology>()
+    private val mockIsExtendedDisplayEnabled = mock<BooleanSupplier>()
+
+    @Before
+    fun setUp() {
+        displayInfo.displayId = 2
+        displayInfo.logicalWidth = 300
+        displayInfo.logicalHeight = 200
+        displayInfo.logicalDensityDpi = 100
+
+        val injector = object : DisplayTopologyCoordinator.Injector() {
+            override fun getTopology() = mockTopology
+        }
+        coordinator = DisplayTopologyCoordinator(injector, mockIsExtendedDisplayEnabled)
+    }
+
+    @Test
+    fun addDisplay() {
+        whenever(mockIsExtendedDisplayEnabled.asBoolean).thenReturn(true)
+
+        coordinator.onDisplayAdded(displayInfo)
+
+        val widthDp = displayInfo.logicalWidth * (DisplayMetrics.DENSITY_DEFAULT.toDouble()
+                / displayInfo.logicalDensityDpi)
+        val heightDp = displayInfo.logicalHeight * (DisplayMetrics.DENSITY_DEFAULT.toDouble()
+                / displayInfo.logicalDensityDpi)
+        verify(mockTopology).addDisplay(displayInfo.displayId, widthDp, heightDp)
+    }
+
+    @Test
+    fun addDisplay_extendedDisplaysDisabled() {
+        whenever(mockIsExtendedDisplayEnabled.asBoolean).thenReturn(false)
+
+        coordinator.onDisplayAdded(displayInfo)
+
+        verify(mockTopology, never()).addDisplay(anyInt(), anyDouble(), anyDouble())
+    }
+
+    @Test
+    fun addDisplay_notInDefaultDisplayGroup() {
+        whenever(mockIsExtendedDisplayEnabled.asBoolean).thenReturn(true)
+        displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1
+
+        coordinator.onDisplayAdded(displayInfo)
+
+        verify(mockTopology, never()).addDisplay(anyInt(), anyDouble(), anyDouble())
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyTest.kt
new file mode 100644
index 0000000..1fad14b
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 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.server.display
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class DisplayTopologyTest {
+    private val topology = DisplayTopology()
+
+    @Test
+    fun oneDisplay() {
+        val displayId = 1
+        val width = 800.0
+        val height = 600.0
+
+        topology.addDisplay(displayId, width, height)
+
+        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId)
+
+        val display = topology.mRoot!!
+        assertThat(display.mDisplayId).isEqualTo(displayId)
+        assertThat(display.mWidth).isEqualTo(width)
+        assertThat(display.mHeight).isEqualTo(height)
+        assertThat(display.mChildren).isEmpty()
+    }
+
+    @Test
+    fun twoDisplays() {
+        val displayId1 = 1
+        val width1 = 800.0
+        val height1 = 600.0
+
+        val displayId2 = 2
+        val width2 = 1000.0
+        val height2 = 1500.0
+
+        topology.addDisplay(displayId1, width1, height1)
+        topology.addDisplay(displayId2, width2, height2)
+
+        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1)
+
+        val display1 = topology.mRoot!!
+        assertThat(display1.mDisplayId).isEqualTo(displayId1)
+        assertThat(display1.mWidth).isEqualTo(width1)
+        assertThat(display1.mHeight).isEqualTo(height1)
+        assertThat(display1.mChildren).hasSize(1)
+
+        val display2 = display1.mChildren[0]
+        assertThat(display2.mDisplayId).isEqualTo(displayId2)
+        assertThat(display2.mWidth).isEqualTo(width2)
+        assertThat(display2.mHeight).isEqualTo(height2)
+        assertThat(display2.mChildren).isEmpty()
+        assertThat(display2.mPosition).isEqualTo(
+            DisplayTopology.TreeNode.Position.POSITION_TOP)
+        assertThat(display2.mOffset).isEqualTo(width1 / 2 - width2 / 2)
+    }
+
+    @Test
+    fun manyDisplays() {
+        val displayId1 = 1
+        val width1 = 800.0
+        val height1 = 600.0
+
+        val displayId2 = 2
+        val width2 = 1000.0
+        val height2 = 1500.0
+
+        topology.addDisplay(displayId1, width1, height1)
+        topology.addDisplay(displayId2, width2, height2)
+
+        val noOfDisplays = 30
+        for (i in 3..noOfDisplays) {
+            topology.addDisplay(/* displayId= */ i, width1, height1)
+        }
+
+        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1)
+
+        val display1 = topology.mRoot!!
+        assertThat(display1.mDisplayId).isEqualTo(displayId1)
+        assertThat(display1.mWidth).isEqualTo(width1)
+        assertThat(display1.mHeight).isEqualTo(height1)
+        assertThat(display1.mChildren).hasSize(1)
+
+        val display2 = display1.mChildren[0]
+        assertThat(display2.mDisplayId).isEqualTo(displayId2)
+        assertThat(display2.mWidth).isEqualTo(width2)
+        assertThat(display2.mHeight).isEqualTo(height2)
+        assertThat(display2.mChildren).hasSize(1)
+        assertThat(display2.mPosition).isEqualTo(
+            DisplayTopology.TreeNode.Position.POSITION_TOP)
+        assertThat(display2.mOffset).isEqualTo(width1 / 2 - width2 / 2)
+
+        var display = display2
+        for (i in 3..noOfDisplays) {
+            display = display.mChildren[0]
+            assertThat(display.mDisplayId).isEqualTo(i)
+            assertThat(display.mWidth).isEqualTo(width1)
+            assertThat(display.mHeight).isEqualTo(height1)
+            // The last display should have no children
+            assertThat(display.mChildren).hasSize(if (i < noOfDisplays) 1 else 0)
+            assertThat(display.mPosition).isEqualTo(
+                DisplayTopology.TreeNode.Position.POSITION_RIGHT)
+            assertThat(display.mOffset).isEqualTo(0)
+        }
+    }
+}
\ No newline at end of file