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