Loading src/com/android/settings/connecteddevice/display/ConnectedDisplayInjector.kt +12 −0 Original line number Diff line number Diff line Loading @@ -133,6 +133,7 @@ open class ConnectedDisplayInjector(open val context: Context?) { } /** * TODO(b/419742776): Unify this with #getAllDisplayIds * @return all displays including disabled. */ open fun getConnectedDisplays(): List<DisplayDevice> { Loading @@ -152,6 +153,17 @@ open class ConnectedDisplayInjector(open val context: Context?) { .toList() } /** * This method return all enabled display ids without further filtering * TODO(b/419742776): Unify this with #getConnectedDisplays * * @see getConnectedDisplays to specifically fetch all connected displays */ open fun getAllDisplayIds(): List<Int> { val dm = displayManager ?: return emptyList() return dm.getDisplays().map { it.displayId }.toList() } /** * @param displayId which must be returned * @return display object for the displayId, or null if display is not a connected display, Loading src/com/android/settings/connecteddevice/display/DisplayBlock.kt +13 −8 Original line number Diff line number Diff line Loading @@ -38,14 +38,13 @@ class DisplayBlock(val injector: ConnectedDisplayInjector) : FrameLayout(injecto context.resources.getDimensionPixelSize(R.dimen.display_block_padding) private val paneBgColor = context.resources.getColor(R.color.display_topology_background_color) private var _displayId: Int? = null // This doesn't necessarily refer to the actual display this block represents. In case of // mirroring, it will be the id of the mirrored display private var displayIdToShowWallpaper: Int? = null /** Scale of the mirrored wallpaper to the actual wallpaper size. */ private var surfaceScale: Float? = null val displayId: Int? get() = _displayId // These are surfaces which must be removed from the display block hierarchy and released once // the new surface is put in place. This list can have more than one item because we may get // two reset calls before we get a single surfaceChange callback. Loading @@ -56,7 +55,7 @@ class DisplayBlock(val injector: ConnectedDisplayInjector) : FrameLayout(injecto @VisibleForTesting fun updateSurfaceView() { val displayId = _displayId ?: return val displayId = displayIdToShowWallpaper ?: return if (parent == null) { Log.i(TAG, "View for display $displayId has no parent - cancelling update") Loading Loading @@ -148,21 +147,27 @@ class DisplayBlock(val injector: ConnectedDisplayInjector) : FrameLayout(injecto /** * Sets position and size of the block given coordinates in pane space. * * @param displayId ID of display this block represents, needed for fetching wallpaper * @param displayIdToShowWallpaper ID of the display whose wallpaper would be projected on this * display block. * @param topLeft coordinates of top left corner of the block, not including highlight border * @param bottomRight coordinates of bottom right corner of the block, not including highlight * border * @param surfaceScale scale in pixels of the size of the wallpaper mirror to the actual * wallpaper on the screen - should be less than one to indicate scaling to smaller size */ fun reset(displayId: Int, topLeft: PointF, bottomRight: PointF, surfaceScale: Float) { fun reset( displayIdToShowWallpaper: Int, topLeft: PointF, bottomRight: PointF, surfaceScale: Float, ) { wallpaperSurface?.let { oldSurfaces.add(it) } injector.handler.removeCallbacks(updateSurfaceView) wallpaperSurface = null setHighlighted(false) positionInPane = topLeft _displayId = displayId this.displayIdToShowWallpaper = displayIdToShowWallpaper this.surfaceScale = surfaceScale val newWidth = (bottomRight.x - topLeft.x).toInt() Loading src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt +117 −33 Original line number Diff line number Diff line Loading @@ -19,8 +19,10 @@ package com.android.settings.connecteddevice.display import android.graphics.PointF import android.graphics.RectF import android.hardware.display.DisplayTopology import android.hardware.display.DisplayTopology.TreeNode import android.util.Log import android.util.Size import android.view.Display.DEFAULT_DISPLAY import android.view.MotionEvent import android.view.View import android.widget.FrameLayout Loading Loading @@ -202,15 +204,34 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : @VisibleForTesting var timesRefreshedBlocks = 0 /** * Updating DisplayTopology pane consists of multiple steps: * 1. Update hint text * 2. Prepare display blocks positioning * 3. Adjust display blocks bounds and scale within the pane * 4. Ensure wallpapers are revealed */ private fun applyTopology(topology: DisplayTopology) { topologyHint.text = context.getString(R.string.external_display_topology_hint) val oldBounds = topologyInfo?.positions val newBounds = buildList { val bounds = topology.absoluteBounds (0..bounds.size() - 1).forEach { add(Pair(bounds.keyAt(it), bounds.valueAt(it))) } // Step 1 val showStackedMirroringDisplay = isDisplayInMiroringMode(context) && injector.flags.showStackedMirroringDisplayConnectedDisplaySetting() topologyHint.text = if (showStackedMirroringDisplay) { "" } else { context.getString(R.string.external_display_topology_hint) } val idToNode = topology.allNodesIdMap() val logicalDisplaySizeFetcher = LogicalDisplaySizeFetcher(injector, idToNode) // Step 2 val oldBounds = topologyInfo?.positions val newBounds = if (showStackedMirroringDisplay) processDisplayBoundsMirroringMode(logicalDisplaySizeFetcher) else processDisplayBounds(topology) if ( oldBounds != null && oldBounds.size == newBounds.size && Loading @@ -221,17 +242,7 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : return } val recycleableBlocks = ArrayDeque<DisplayBlock>() for (i in 0..paneContent.childCount - 1) { recycleableBlocks.add(paneContent.getChildAt(i) as DisplayBlock) } val idToNode = topology.allNodesIdMap() val topologyLogicalDisplaySize = idToNode .filter { it.key != null && it.value != null } .map { it.key!! to Size(it.value.logicalWidth, it.value.logicalHeight) } .toMap() // Step 3 val scaling = TopologyScale( paneContent.width, Loading @@ -239,15 +250,26 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : maxEdgeLength = DisplayTopology.dpToPx(MAX_EDGE_LENGTH_DP, injector.densityDpi), newBounds.map { it.second }, ) setupDisplayPaneAndBlocks(scaling, newBounds, topologyLogicalDisplaySize) setupDisplayPaneAndBlocks( scaling, newBounds, logicalDisplaySizeFetcher, showStackedMirroringDisplay, ) topologyInfo = TopologyInfo(topology, scaling, newBounds) // Step 4 val displayIdsToRevealWallpaper = if (showStackedMirroringDisplay) setOf(DEFAULT_DISPLAY) else { idToNode.keys.toSet() } // Construct a map containing revealers that we want to keep (keepRevealing). Then create a // list comprised of the values of that map as well as new revealers (revealedWallpapers). val keepRevealing = buildMap<Int, RevealedWallpaper> { revealedWallpapers.forEach { r -> if (idToNode.containsKey(r.displayId)) { if (displayIdsToRevealWallpaper.contains(r.displayId)) { put(r.displayId, r) } else { r.viewManager.removeView(r.revealer) Loading @@ -255,16 +277,49 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : } } revealedWallpapers = idToNode.keys displayIdsToRevealWallpaper .map { keepRevealing.get(it) ?: injector.revealWallpaper(it) } .filterNotNull() .toList() } private fun processDisplayBounds(topology: DisplayTopology) = buildList { val bounds = topology.absoluteBounds (0..bounds.size() - 1).forEach { add(Pair(bounds.keyAt(it), bounds.valueAt(it))) } } private fun processDisplayBoundsMirroringMode( logicalDisplaySizeFetcher: LogicalDisplaySizeFetcher ): List<Pair<Int, RectF>> { val displayIds = injector.getAllDisplayIds().sortedBy { it } val bounds = mutableListOf<Pair<Int, RectF>>() val mirroringDiagonalStackOffsetPx = DisplayTopology.dpToPx(MIRRORING_DIAGONAL_STACK_OFFSET_DP, injector.densityDpi) // Displays are arranged 45 degrees diagonally, with DEFAULT_DISPLAY on the front and // leftmost, and other displays on the back, top-right of the display on the front. for (i in 0..displayIds.size - 1) { val displayId = displayIds[i] val offsetPx = mirroringDiagonalStackOffsetPx * i logicalDisplaySizeFetcher.get(displayId)?.let { bounds.add( Pair( displayId, RectF(offsetPx, -offsetPx, it.width + offsetPx, it.height - offsetPx), ) ) } } // Reverse the z-order to make the first added display (DEFAULT_DISPLAY) on the front. return bounds.reversed() } private fun setupDisplayPaneAndBlocks( scaling: TopologyScale, newBounds: List<Pair<Int, RectF>>, topologyLogicalDisplaySize: Map<Int, Size>, logicalDisplaySizeFetcher: LogicalDisplaySizeFetcher, isMirroring: Boolean, ) { // Resize pane holder paneHolder.layoutParams.let { Loading @@ -284,15 +339,20 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : val block = recycleableBlocks.removeFirstOrNull() ?: DisplayBlock(injector).apply { paneContent.addView(this) } // First check from DisplayTopology for quick lookup on logical display size. If display // is not in topology, then query from DisplayInfo. val logicalDisplaySize = topologyLogicalDisplaySize.get(id) ?: injector.getLogicalSize(id) logicalDisplaySize?.let { logicalDisplaySizeFetcher.get(id)?.let { val topLeft = scaling.displayToPaneCoor(pos.left, pos.top) val bottomRight = scaling.displayToPaneCoor(pos.right, pos.bottom) block.reset(id, topLeft, bottomRight, (bottomRight.x - topLeft.x) / it.width) block.reset( // Mirroring is only supported for DEFAULT_DISPLAY for now if (isMirroring) DEFAULT_DISPLAY else id, topLeft, bottomRight, (bottomRight.x - topLeft.x) / it.width, ) } if (isMirroring) { block.setOnTouchListener(null) } else { block.setOnTouchListener { view, ev -> when (ev.actionMasked) { MotionEvent.ACTION_DOWN -> onBlockTouchDown(id, pos, block, ev) Loading @@ -302,6 +362,7 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : } } } } paneContent.removeViews(newBounds.size, recycleableBlocks.size) timesRefreshedBlocks++ // Cancel the drag if one is in progress. Loading Loading @@ -412,9 +473,32 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : return true } /** * A simple wrapper class to fetch logical display size from either DisplayTopology or directly * from DisplayManager. This should used as a temporary variable only for the current * DisplayTopology update. */ private class LogicalDisplaySizeFetcher( val injector: ConnectedDisplayInjector, idToNode: Map<Int?, TreeNode?>, ) { private val topologyLogicalDisplaySize = idToNode .filter { it.key != null && it.value != null } .map { it.key!! to Size(it.value!!.logicalWidth, it.value!!.logicalHeight) } .toMap() fun get(id: Int): Size? { // First check from DisplayTopology for quick lookup on logical display size. If display // is not in topology, then query from DisplayInfo. return topologyLogicalDisplaySize.get(id) ?: injector.getLogicalSize(id) } } private companion object { private val MIN_EDGE_LENGTH_DP = 60f private val MAX_EDGE_LENGTH_DP = 256f private val MIRRORING_DIAGONAL_STACK_OFFSET_DP = 120f private val TAG = "DisplayTopologyPreference" } } tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt +86 −2 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.hardware.display.DisplayTopology import android.hardware.display.DisplayTopology.TreeNode.POSITION_BOTTOM import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP import android.provider.Settings import android.util.DisplayMetrics import android.util.Size import android.view.MotionEvent Loading @@ -33,6 +34,8 @@ import androidx.preference.PreferenceViewHolder import androidx.test.core.app.ApplicationProvider import androidx.test.core.view.MotionEventBuilder import com.android.settings.R import com.android.settings.flags.FakeFeatureFlagsImpl import com.android.settings.flags.Flags.FLAG_SHOW_STACKED_MIRRORING_DISPLAY_CONNECTED_DISPLAY_SETTING import com.google.common.truth.Truth.assertThat import java.util.function.Consumer import kotlin.math.abs Loading @@ -43,16 +46,20 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class DisplayTopologyPreferenceTest { val context = ApplicationProvider.getApplicationContext<Context>() val injector = TestInjector(context) val featureFlags = FakeFeatureFlagsImpl() val injector = TestInjector(context, featureFlags) val preference = DisplayTopologyPreference(injector) val rootView = View.inflate(context, preference.layoutResource, /* parent= */ null) val holder = PreferenceViewHolder.createInstanceForTests(rootView) init { preference.onBindViewHolder(holder) featureFlags.setFlag(FLAG_SHOW_STACKED_MIRRORING_DISPLAY_CONNECTED_DISPLAY_SETTING, true) } class TestInjector(context: Context) : ConnectedDisplayInjector(context) { class TestInjector(context: Context, featureFlags: FakeFeatureFlagsImpl) : ConnectedDisplayInjector(context) { var displaysSize = mutableMapOf<Int, Size>() var topology: DisplayTopology? = null Loading @@ -64,6 +71,8 @@ class DisplayTopologyPreferenceTest { topology = value } override val flags = DesktopExperienceFlags(featureFlags) /** A log of events related to wallpaper revealing. */ val revealLog = mutableListOf<String>() Loading @@ -73,6 +82,10 @@ class DisplayTopologyPreferenceTest { return displaysSize.get(displayId) } override fun getAllDisplayIds(): List<Int> { return displaysSize.keys.toList() } override fun registerTopologyListener(listener: Consumer<DisplayTopology>) { if (topologyListener != null) { throw IllegalStateException( Loading Loading @@ -228,6 +241,14 @@ class DisplayTopologyPreferenceTest { assertThat(block.selectionMarkerView.visibility).isEqualTo(vis) } fun setMirroringMode(enable: Boolean) { Settings.Secure.putInt( context.contentResolver, Settings.Secure.MIRROR_BUILT_IN_DISPLAY, if (enable) 1 else 0, ) } @Test fun twoDisplaysGenerateBlocks() { val (childBlock, rootBlock) = setupPaneWithTwoDisplays() Loading @@ -246,6 +267,36 @@ class DisplayTopologyPreferenceTest { .isEqualTo(context.getString(R.string.external_display_topology_hint)) } @Test fun twoDisplaysMirroringGenerateBlocks() { setMirroringMode(true) setupPaneWithTwoDisplays() val newDisplayId = 123 val newDisplaySize = Size(500, 500) injector.topology!!.addDisplay( newDisplayId, newDisplaySize.width, newDisplaySize.height, /* logicalDensity= */ 160, ) injector.displaysSize.put(newDisplayId, newDisplaySize) val paneChildren = getPaneChildren() assertThat(paneChildren).hasSize(2) for (i in 1..paneChildren.size - 1) { // Bounds are arranged 45 degrees diagonally from the top left corner, in a decreasing // X and increasing Y, since the backmost display will be the first on the list. val bounds = virtualBounds(paneChildren[i]) val prevBounds = virtualBounds(paneChildren[i - 1]) assertThat(bounds.left).isLessThan(prevBounds.left) assertThat(bounds.top).isGreaterThan(prevBounds.top) assertSelected(paneChildren[i], false) } assertThat(preference.topologyHint.text).isEqualTo("") } @Test fun dragDisplayDownward() { val (leftBlock, _) = setupPaneWithTwoDisplays() Loading Loading @@ -480,6 +531,39 @@ class DisplayTopologyPreferenceTest { assertThat(block.y).isWithin(0.01f).of(origY) } @Test fun cannotMoveDisplayMirroringMode() { setMirroringMode(true) setupPaneWithTwoDisplays() val paneChildren = getPaneChildren() assertThat(paneChildren).hasSize(2) for (block in paneChildren) { val origY = block.y block.dispatchTouchEvent( MotionEventBuilder.newBuilder() .setAction(MotionEvent.ACTION_DOWN) .setPointer(0f, 0f) .build() ) assertSelected(block, false) block.dispatchTouchEvent( MotionEventBuilder.newBuilder() .setAction(MotionEvent.ACTION_MOVE) .setPointer(0f, 30f) .build() ) assertThat(block.y).isWithin(0.01f).of(origY) block.dispatchTouchEvent( MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_UP).build() ) assertThat(block.y).isWithin(0.01f).of(origY) } } @Test fun updatedTopologyCancelsDragIfNonTrivialChange() { val (leftBlock, _) = setupPaneWithTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f) Loading Loading
src/com/android/settings/connecteddevice/display/ConnectedDisplayInjector.kt +12 −0 Original line number Diff line number Diff line Loading @@ -133,6 +133,7 @@ open class ConnectedDisplayInjector(open val context: Context?) { } /** * TODO(b/419742776): Unify this with #getAllDisplayIds * @return all displays including disabled. */ open fun getConnectedDisplays(): List<DisplayDevice> { Loading @@ -152,6 +153,17 @@ open class ConnectedDisplayInjector(open val context: Context?) { .toList() } /** * This method return all enabled display ids without further filtering * TODO(b/419742776): Unify this with #getConnectedDisplays * * @see getConnectedDisplays to specifically fetch all connected displays */ open fun getAllDisplayIds(): List<Int> { val dm = displayManager ?: return emptyList() return dm.getDisplays().map { it.displayId }.toList() } /** * @param displayId which must be returned * @return display object for the displayId, or null if display is not a connected display, Loading
src/com/android/settings/connecteddevice/display/DisplayBlock.kt +13 −8 Original line number Diff line number Diff line Loading @@ -38,14 +38,13 @@ class DisplayBlock(val injector: ConnectedDisplayInjector) : FrameLayout(injecto context.resources.getDimensionPixelSize(R.dimen.display_block_padding) private val paneBgColor = context.resources.getColor(R.color.display_topology_background_color) private var _displayId: Int? = null // This doesn't necessarily refer to the actual display this block represents. In case of // mirroring, it will be the id of the mirrored display private var displayIdToShowWallpaper: Int? = null /** Scale of the mirrored wallpaper to the actual wallpaper size. */ private var surfaceScale: Float? = null val displayId: Int? get() = _displayId // These are surfaces which must be removed from the display block hierarchy and released once // the new surface is put in place. This list can have more than one item because we may get // two reset calls before we get a single surfaceChange callback. Loading @@ -56,7 +55,7 @@ class DisplayBlock(val injector: ConnectedDisplayInjector) : FrameLayout(injecto @VisibleForTesting fun updateSurfaceView() { val displayId = _displayId ?: return val displayId = displayIdToShowWallpaper ?: return if (parent == null) { Log.i(TAG, "View for display $displayId has no parent - cancelling update") Loading Loading @@ -148,21 +147,27 @@ class DisplayBlock(val injector: ConnectedDisplayInjector) : FrameLayout(injecto /** * Sets position and size of the block given coordinates in pane space. * * @param displayId ID of display this block represents, needed for fetching wallpaper * @param displayIdToShowWallpaper ID of the display whose wallpaper would be projected on this * display block. * @param topLeft coordinates of top left corner of the block, not including highlight border * @param bottomRight coordinates of bottom right corner of the block, not including highlight * border * @param surfaceScale scale in pixels of the size of the wallpaper mirror to the actual * wallpaper on the screen - should be less than one to indicate scaling to smaller size */ fun reset(displayId: Int, topLeft: PointF, bottomRight: PointF, surfaceScale: Float) { fun reset( displayIdToShowWallpaper: Int, topLeft: PointF, bottomRight: PointF, surfaceScale: Float, ) { wallpaperSurface?.let { oldSurfaces.add(it) } injector.handler.removeCallbacks(updateSurfaceView) wallpaperSurface = null setHighlighted(false) positionInPane = topLeft _displayId = displayId this.displayIdToShowWallpaper = displayIdToShowWallpaper this.surfaceScale = surfaceScale val newWidth = (bottomRight.x - topLeft.x).toInt() Loading
src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt +117 −33 Original line number Diff line number Diff line Loading @@ -19,8 +19,10 @@ package com.android.settings.connecteddevice.display import android.graphics.PointF import android.graphics.RectF import android.hardware.display.DisplayTopology import android.hardware.display.DisplayTopology.TreeNode import android.util.Log import android.util.Size import android.view.Display.DEFAULT_DISPLAY import android.view.MotionEvent import android.view.View import android.widget.FrameLayout Loading Loading @@ -202,15 +204,34 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : @VisibleForTesting var timesRefreshedBlocks = 0 /** * Updating DisplayTopology pane consists of multiple steps: * 1. Update hint text * 2. Prepare display blocks positioning * 3. Adjust display blocks bounds and scale within the pane * 4. Ensure wallpapers are revealed */ private fun applyTopology(topology: DisplayTopology) { topologyHint.text = context.getString(R.string.external_display_topology_hint) val oldBounds = topologyInfo?.positions val newBounds = buildList { val bounds = topology.absoluteBounds (0..bounds.size() - 1).forEach { add(Pair(bounds.keyAt(it), bounds.valueAt(it))) } // Step 1 val showStackedMirroringDisplay = isDisplayInMiroringMode(context) && injector.flags.showStackedMirroringDisplayConnectedDisplaySetting() topologyHint.text = if (showStackedMirroringDisplay) { "" } else { context.getString(R.string.external_display_topology_hint) } val idToNode = topology.allNodesIdMap() val logicalDisplaySizeFetcher = LogicalDisplaySizeFetcher(injector, idToNode) // Step 2 val oldBounds = topologyInfo?.positions val newBounds = if (showStackedMirroringDisplay) processDisplayBoundsMirroringMode(logicalDisplaySizeFetcher) else processDisplayBounds(topology) if ( oldBounds != null && oldBounds.size == newBounds.size && Loading @@ -221,17 +242,7 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : return } val recycleableBlocks = ArrayDeque<DisplayBlock>() for (i in 0..paneContent.childCount - 1) { recycleableBlocks.add(paneContent.getChildAt(i) as DisplayBlock) } val idToNode = topology.allNodesIdMap() val topologyLogicalDisplaySize = idToNode .filter { it.key != null && it.value != null } .map { it.key!! to Size(it.value.logicalWidth, it.value.logicalHeight) } .toMap() // Step 3 val scaling = TopologyScale( paneContent.width, Loading @@ -239,15 +250,26 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : maxEdgeLength = DisplayTopology.dpToPx(MAX_EDGE_LENGTH_DP, injector.densityDpi), newBounds.map { it.second }, ) setupDisplayPaneAndBlocks(scaling, newBounds, topologyLogicalDisplaySize) setupDisplayPaneAndBlocks( scaling, newBounds, logicalDisplaySizeFetcher, showStackedMirroringDisplay, ) topologyInfo = TopologyInfo(topology, scaling, newBounds) // Step 4 val displayIdsToRevealWallpaper = if (showStackedMirroringDisplay) setOf(DEFAULT_DISPLAY) else { idToNode.keys.toSet() } // Construct a map containing revealers that we want to keep (keepRevealing). Then create a // list comprised of the values of that map as well as new revealers (revealedWallpapers). val keepRevealing = buildMap<Int, RevealedWallpaper> { revealedWallpapers.forEach { r -> if (idToNode.containsKey(r.displayId)) { if (displayIdsToRevealWallpaper.contains(r.displayId)) { put(r.displayId, r) } else { r.viewManager.removeView(r.revealer) Loading @@ -255,16 +277,49 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : } } revealedWallpapers = idToNode.keys displayIdsToRevealWallpaper .map { keepRevealing.get(it) ?: injector.revealWallpaper(it) } .filterNotNull() .toList() } private fun processDisplayBounds(topology: DisplayTopology) = buildList { val bounds = topology.absoluteBounds (0..bounds.size() - 1).forEach { add(Pair(bounds.keyAt(it), bounds.valueAt(it))) } } private fun processDisplayBoundsMirroringMode( logicalDisplaySizeFetcher: LogicalDisplaySizeFetcher ): List<Pair<Int, RectF>> { val displayIds = injector.getAllDisplayIds().sortedBy { it } val bounds = mutableListOf<Pair<Int, RectF>>() val mirroringDiagonalStackOffsetPx = DisplayTopology.dpToPx(MIRRORING_DIAGONAL_STACK_OFFSET_DP, injector.densityDpi) // Displays are arranged 45 degrees diagonally, with DEFAULT_DISPLAY on the front and // leftmost, and other displays on the back, top-right of the display on the front. for (i in 0..displayIds.size - 1) { val displayId = displayIds[i] val offsetPx = mirroringDiagonalStackOffsetPx * i logicalDisplaySizeFetcher.get(displayId)?.let { bounds.add( Pair( displayId, RectF(offsetPx, -offsetPx, it.width + offsetPx, it.height - offsetPx), ) ) } } // Reverse the z-order to make the first added display (DEFAULT_DISPLAY) on the front. return bounds.reversed() } private fun setupDisplayPaneAndBlocks( scaling: TopologyScale, newBounds: List<Pair<Int, RectF>>, topologyLogicalDisplaySize: Map<Int, Size>, logicalDisplaySizeFetcher: LogicalDisplaySizeFetcher, isMirroring: Boolean, ) { // Resize pane holder paneHolder.layoutParams.let { Loading @@ -284,15 +339,20 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : val block = recycleableBlocks.removeFirstOrNull() ?: DisplayBlock(injector).apply { paneContent.addView(this) } // First check from DisplayTopology for quick lookup on logical display size. If display // is not in topology, then query from DisplayInfo. val logicalDisplaySize = topologyLogicalDisplaySize.get(id) ?: injector.getLogicalSize(id) logicalDisplaySize?.let { logicalDisplaySizeFetcher.get(id)?.let { val topLeft = scaling.displayToPaneCoor(pos.left, pos.top) val bottomRight = scaling.displayToPaneCoor(pos.right, pos.bottom) block.reset(id, topLeft, bottomRight, (bottomRight.x - topLeft.x) / it.width) block.reset( // Mirroring is only supported for DEFAULT_DISPLAY for now if (isMirroring) DEFAULT_DISPLAY else id, topLeft, bottomRight, (bottomRight.x - topLeft.x) / it.width, ) } if (isMirroring) { block.setOnTouchListener(null) } else { block.setOnTouchListener { view, ev -> when (ev.actionMasked) { MotionEvent.ACTION_DOWN -> onBlockTouchDown(id, pos, block, ev) Loading @@ -302,6 +362,7 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : } } } } paneContent.removeViews(newBounds.size, recycleableBlocks.size) timesRefreshedBlocks++ // Cancel the drag if one is in progress. Loading Loading @@ -412,9 +473,32 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : return true } /** * A simple wrapper class to fetch logical display size from either DisplayTopology or directly * from DisplayManager. This should used as a temporary variable only for the current * DisplayTopology update. */ private class LogicalDisplaySizeFetcher( val injector: ConnectedDisplayInjector, idToNode: Map<Int?, TreeNode?>, ) { private val topologyLogicalDisplaySize = idToNode .filter { it.key != null && it.value != null } .map { it.key!! to Size(it.value!!.logicalWidth, it.value!!.logicalHeight) } .toMap() fun get(id: Int): Size? { // First check from DisplayTopology for quick lookup on logical display size. If display // is not in topology, then query from DisplayInfo. return topologyLogicalDisplaySize.get(id) ?: injector.getLogicalSize(id) } } private companion object { private val MIN_EDGE_LENGTH_DP = 60f private val MAX_EDGE_LENGTH_DP = 256f private val MIRRORING_DIAGONAL_STACK_OFFSET_DP = 120f private val TAG = "DisplayTopologyPreference" } }
tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt +86 −2 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.hardware.display.DisplayTopology import android.hardware.display.DisplayTopology.TreeNode.POSITION_BOTTOM import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP import android.provider.Settings import android.util.DisplayMetrics import android.util.Size import android.view.MotionEvent Loading @@ -33,6 +34,8 @@ import androidx.preference.PreferenceViewHolder import androidx.test.core.app.ApplicationProvider import androidx.test.core.view.MotionEventBuilder import com.android.settings.R import com.android.settings.flags.FakeFeatureFlagsImpl import com.android.settings.flags.Flags.FLAG_SHOW_STACKED_MIRRORING_DISPLAY_CONNECTED_DISPLAY_SETTING import com.google.common.truth.Truth.assertThat import java.util.function.Consumer import kotlin.math.abs Loading @@ -43,16 +46,20 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class DisplayTopologyPreferenceTest { val context = ApplicationProvider.getApplicationContext<Context>() val injector = TestInjector(context) val featureFlags = FakeFeatureFlagsImpl() val injector = TestInjector(context, featureFlags) val preference = DisplayTopologyPreference(injector) val rootView = View.inflate(context, preference.layoutResource, /* parent= */ null) val holder = PreferenceViewHolder.createInstanceForTests(rootView) init { preference.onBindViewHolder(holder) featureFlags.setFlag(FLAG_SHOW_STACKED_MIRRORING_DISPLAY_CONNECTED_DISPLAY_SETTING, true) } class TestInjector(context: Context) : ConnectedDisplayInjector(context) { class TestInjector(context: Context, featureFlags: FakeFeatureFlagsImpl) : ConnectedDisplayInjector(context) { var displaysSize = mutableMapOf<Int, Size>() var topology: DisplayTopology? = null Loading @@ -64,6 +71,8 @@ class DisplayTopologyPreferenceTest { topology = value } override val flags = DesktopExperienceFlags(featureFlags) /** A log of events related to wallpaper revealing. */ val revealLog = mutableListOf<String>() Loading @@ -73,6 +82,10 @@ class DisplayTopologyPreferenceTest { return displaysSize.get(displayId) } override fun getAllDisplayIds(): List<Int> { return displaysSize.keys.toList() } override fun registerTopologyListener(listener: Consumer<DisplayTopology>) { if (topologyListener != null) { throw IllegalStateException( Loading Loading @@ -228,6 +241,14 @@ class DisplayTopologyPreferenceTest { assertThat(block.selectionMarkerView.visibility).isEqualTo(vis) } fun setMirroringMode(enable: Boolean) { Settings.Secure.putInt( context.contentResolver, Settings.Secure.MIRROR_BUILT_IN_DISPLAY, if (enable) 1 else 0, ) } @Test fun twoDisplaysGenerateBlocks() { val (childBlock, rootBlock) = setupPaneWithTwoDisplays() Loading @@ -246,6 +267,36 @@ class DisplayTopologyPreferenceTest { .isEqualTo(context.getString(R.string.external_display_topology_hint)) } @Test fun twoDisplaysMirroringGenerateBlocks() { setMirroringMode(true) setupPaneWithTwoDisplays() val newDisplayId = 123 val newDisplaySize = Size(500, 500) injector.topology!!.addDisplay( newDisplayId, newDisplaySize.width, newDisplaySize.height, /* logicalDensity= */ 160, ) injector.displaysSize.put(newDisplayId, newDisplaySize) val paneChildren = getPaneChildren() assertThat(paneChildren).hasSize(2) for (i in 1..paneChildren.size - 1) { // Bounds are arranged 45 degrees diagonally from the top left corner, in a decreasing // X and increasing Y, since the backmost display will be the first on the list. val bounds = virtualBounds(paneChildren[i]) val prevBounds = virtualBounds(paneChildren[i - 1]) assertThat(bounds.left).isLessThan(prevBounds.left) assertThat(bounds.top).isGreaterThan(prevBounds.top) assertSelected(paneChildren[i], false) } assertThat(preference.topologyHint.text).isEqualTo("") } @Test fun dragDisplayDownward() { val (leftBlock, _) = setupPaneWithTwoDisplays() Loading Loading @@ -480,6 +531,39 @@ class DisplayTopologyPreferenceTest { assertThat(block.y).isWithin(0.01f).of(origY) } @Test fun cannotMoveDisplayMirroringMode() { setMirroringMode(true) setupPaneWithTwoDisplays() val paneChildren = getPaneChildren() assertThat(paneChildren).hasSize(2) for (block in paneChildren) { val origY = block.y block.dispatchTouchEvent( MotionEventBuilder.newBuilder() .setAction(MotionEvent.ACTION_DOWN) .setPointer(0f, 0f) .build() ) assertSelected(block, false) block.dispatchTouchEvent( MotionEventBuilder.newBuilder() .setAction(MotionEvent.ACTION_MOVE) .setPointer(0f, 30f) .build() ) assertThat(block.y).isWithin(0.01f).of(origY) block.dispatchTouchEvent( MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_UP).build() ) assertThat(block.y).isWithin(0.01f).of(origY) } } @Test fun updatedTopologyCancelsDragIfNonTrivialChange() { val (leftBlock, _) = setupPaneWithTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f) Loading