diff options
6 files changed, 494 insertions, 77 deletions
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 67e2ca2b312c..ec1ec3ad9c96 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.hardware.display.HdrConversionMode.HDR_CONVERSION_UNSUPPOR 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 @@ public final class DisplayManagerService extends SystemService { 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 @@ public final class DisplayManagerService extends SystemService { 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 000000000000..90038a09850f --- /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 631f14755b12..cbd224c3d842 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 { + @GuardedBy("mLock") + private final DisplayTopology mTopology; + /** - * The topology tree + * Check if extended displays are enabled. If not, a topology is not needed. */ - @Nullable - private TopologyTreeNode mRoot; + 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; + } /** - * The logical display ID of the primary display that will show certain UI elements. - * This is not necessarily the same as the default display. + * Add a display to the topology. + * @param info The display info */ - private int mPrimaryDisplayId; + 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 { - - /** - * The logical display ID - */ - private int mDisplayId; - - private final List<TopologyTreeNode> mChildren = new ArrayList<>(); - - /** - * 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(); - } + /** + * @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; + } + + /** + * @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 boolean isDisplayAllowedInTopology(DisplayInfo info) { + return mIsExtendedDisplayEnabled.getAsBoolean() + && info.displayGroupId == Display.DEFAULT_DISPLAY_GROUP; + } - 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 608fb35cea9a..666bd26db340 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 @@ package com.android.server.display; 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.AudioManager.AudioPlaybackCallback; 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.internal.util.FrameworkStatsLog; import com.android.server.display.utils.DebugUtils; import java.util.List; +import java.util.function.BooleanSupplier; /** @@ -203,8 +202,9 @@ public final class ExternalDisplayStatsService { } }; - 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 @@ public final class ExternalDisplayStatsService { 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 000000000000..17af6335e25b --- /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 000000000000..1fad14b0a062 --- /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 |