Loading res/drawable/display_block_background.xml +0 −10 Original line number Diff line number Diff line Loading @@ -17,16 +17,6 @@ <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <!-- A non-rounded rectangle is needed to prevent the enclosed image from leaking out behind the rounded corners. --> <item> <shape android:shape="rectangle"> <stroke android:color="@color/display_topology_background_color" android:width="@dimen/display_block_padding" /> </shape> </item> <item> <shape android:shape="rectangle"> <stroke Loading src/com/android/settings/connecteddevice/display/ConnectedDisplayInjector.kt +8 −0 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ import android.os.RemoteException import android.os.SystemProperties import android.util.DisplayMetrics import android.util.Log import android.util.Size import android.view.Display import android.view.Display.INVALID_DISPLAY import android.view.DisplayInfo Loading Loading @@ -290,6 +291,13 @@ open class ConnectedDisplayInjector(open val context: Context?) { } } open fun getLogicalSize(displayId: Int): Size? { val display = displayManager?.getDisplay(displayId)?: return null val displayInfo = DisplayInfo() display.getDisplayInfo(displayInfo) return Size(displayInfo.logicalWidth, displayInfo.logicalHeight) } open fun registerTopologyListener(listener: Consumer<DisplayTopology>) { val executor = context?.mainExecutor if (executor != null) displayManager?.registerTopologyListener(executor, listener) Loading src/com/android/settings/connecteddevice/display/DisplayBlock.kt +12 −6 Original line number Diff line number Diff line Loading @@ -16,26 +16,25 @@ package com.android.settings.connecteddevice.display import com.android.settings.R import android.content.Context import android.graphics.Bitmap import android.graphics.Color import android.graphics.Outline import android.graphics.PointF import android.util.Log import android.view.SurfaceControl import android.view.SurfaceHolder import android.view.SurfaceView import android.view.View import android.view.ViewOutlineProvider import android.widget.FrameLayout import androidx.annotation.VisibleForTesting import com.android.settings.R /** Represents a draggable block in the topology pane. */ class DisplayBlock(val injector: ConnectedDisplayInjector) : FrameLayout(injector.context!!) { @VisibleForTesting val mHighlightPx = context.resources.getDimensionPixelSize( R.dimen.display_block_highlight_width) val cornerRadiusPx = context.resources.getDimensionPixelSize( R.dimen.display_block_corner_radius) private var mDisplayId: Int? = null Loading Loading @@ -183,6 +182,13 @@ class DisplayBlock(val injector: ConnectedDisplayInjector) : FrameLayout(injecto mWallpaperView.layoutParams = it } mWallpaperView.outlineProvider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx.toFloat()) } } mWallpaperView.clipToOutline = true // The other two child views are MATCH_PARENT by default so will resize to fill up the // FrameLayout. } Loading src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt +55 −38 Original line number Diff line number Diff line Loading @@ -16,27 +16,21 @@ package com.android.settings.connecteddevice.display import com.android.settings.R import com.android.settingslib.widget.GroupSectionDividerMixin import android.content.Context import android.graphics.Bitmap import android.graphics.PointF import android.graphics.RectF import android.hardware.display.DisplayTopology import android.util.DisplayMetrics import android.util.Log import android.util.Size import android.view.MotionEvent import android.view.View import android.widget.FrameLayout import android.widget.TextView import androidx.annotation.VisibleForTesting import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.android.settings.R import com.android.settingslib.widget.GroupSectionDividerMixin import java.util.function.Consumer import kotlin.math.abs /** Loading Loading @@ -213,11 +207,42 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) recycleableBlocks.add(mPaneContent.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() val scaling = TopologyScale( mPaneContent.width, minEdgeLength = DisplayTopology.dpToPx(60f, injector.densityDpi), maxEdgeLength = DisplayTopology.dpToPx(256f, injector.densityDpi), newBounds.map { it.second }.toList()) minEdgeLength = DisplayTopology.dpToPx(MIN_EDGE_LENGTH_DP, injector.densityDpi), maxEdgeLength = DisplayTopology.dpToPx(MAX_EDGE_LENGTH_DP, injector.densityDpi), newBounds.map { it.second }) setupDisplayPaneAndBlocks(scaling, newBounds, topologyLogicalDisplaySize) mTopologyInfo = TopologyInfo(topology, scaling, newBounds) // 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 (mRevealedWallpapers). val keepRevealing = buildMap<Int, RevealedWallpaper> { mRevealedWallpapers.forEach { r -> if (idToNode.containsKey(r.displayId)) { put(r.displayId, r) } else { r.viewManager.removeView(r.revealer) } } } mRevealedWallpapers = idToNode.keys .map { keepRevealing.get(it) ?: injector.revealWallpaper(it) } .filterNotNull() .toList() } private fun setupDisplayPaneAndBlocks( scaling: TopologyScale, newBounds: List<Pair<Int, RectF>>, topologyLogicalDisplaySize: Map<Int, Size> ) { // Resize pane holder mPaneHolder.layoutParams.let { val newHeight = scaling.paneHeight.toInt() if (it.height != newHeight) { Loading @@ -226,16 +251,25 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) } } val idToNode = topology.allNodesIdMap() // Setup display blocks val recycleableBlocks = ArrayDeque<DisplayBlock>() for (i in 0..mPaneContent.childCount - 1) { recycleableBlocks.add(mPaneContent.getChildAt(i) as DisplayBlock) } newBounds.forEach { (id, pos) -> val block = recycleableBlocks.removeFirstOrNull() ?: DisplayBlock(injector).apply { mPaneContent.addView(this) } idToNode.get(id)?.let { // 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 { 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.logicalWidth) block.reset( id, topLeft, bottomRight, (bottomRight.x - topLeft.x) / it.width ) } block.setOnTouchListener { view, ev -> when (ev.actionMasked) { Loading @@ -248,25 +282,6 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) } mPaneContent.removeViews(newBounds.size, recycleableBlocks.size) mTimesRefreshedBlocks++ mTopologyInfo = TopologyInfo(topology, scaling, newBounds) // 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 (mRevealedWallpapers). val keepRevealing = buildMap<Int, RevealedWallpaper> { mRevealedWallpapers.forEach { r -> if (idToNode.containsKey(r.displayId)) { put(r.displayId, r) } else { r.viewManager.removeView(r.revealer) } } } mRevealedWallpapers = idToNode.keys .map { keepRevealing.get(it) ?: injector.revealWallpaper(it) } .filterNotNull() .toList() // Cancel the drag if one is in progress. mDrag = null } Loading Loading @@ -351,7 +366,9 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) return true } companion object { val TAG = "DisplayTopologyPreference" private companion object { private val MIN_EDGE_LENGTH_DP = 60f private val MAX_EDGE_LENGTH_DP = 256f private val TAG = "DisplayTopologyPreference" } } tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt +53 −40 Original line number Diff line number Diff line Loading @@ -16,15 +16,14 @@ package com.android.settings.connecteddevice.display 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.content.Context import android.graphics.Color import android.graphics.RectF 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.util.DisplayMetrics import android.util.Size import android.view.MotionEvent import android.view.View import android.view.ViewGroup Loading @@ -33,14 +32,10 @@ import android.widget.FrameLayout import androidx.preference.PreferenceViewHolder import androidx.test.core.app.ApplicationProvider import androidx.test.core.view.MotionEventBuilder import com.android.settings.R import com.google.common.truth.Truth.assertThat import java.util.function.Consumer import kotlin.math.abs import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner Loading @@ -58,6 +53,7 @@ class DisplayTopologyPreferenceTest { } class TestInjector(context : Context) : ConnectedDisplayInjector(context) { var displaysSize = mutableMapOf<Int, Size>() var topology: DisplayTopology? = null var topologyListener: Consumer<DisplayTopology>? = null Loading @@ -73,6 +69,10 @@ class DisplayTopologyPreferenceTest { override val densityDpi = DisplayMetrics.DENSITY_DEFAULT override fun getLogicalSize(displayId: Int): Size? { return displaysSize.get(displayId) } override fun registerTopologyListener(listener: Consumer<DisplayTopology>) { if (topologyListener != null) { throw IllegalStateException( Loading Loading @@ -144,28 +144,30 @@ class DisplayTopologyPreferenceTest { .map { preference.mPaneContent.getChildAt(it) as DisplayBlock } .toList() fun singleDisplayTopology(): DisplayTopology { fun setupSingleDisplay() { val primaryId = 22; val root = DisplayTopology.TreeNode( primaryId, /* logicalWidth= */ 200, /* logicalHeight= */ 160, primaryId, DISPLAY_SIZE_1.width, DISPLAY_SIZE_1.height, /* logicalDensity= */ 160, POSITION_LEFT, /* offset= */ 0f) return DisplayTopology(root, primaryId) injector.topology = DisplayTopology(root, primaryId) injector.displaysSize = mutableMapOf(Pair(primaryId, DISPLAY_SIZE_1)) } fun twoDisplayTopology(childPosition: Int, childOffset: Float): DisplayTopology { fun setupTwoDisplays(childPosition: Int, childOffset: Float) { val primaryId = 1 val child = DisplayTopology.TreeNode( /* displayId= */ 42, /* logicalWidth= */ 100, /* logicalHeight= */ 80, /* displayId= */ 42, DISPLAY_SIZE_2.width, DISPLAY_SIZE_2.height, /* logicalDensity= */ 160, childPosition, childOffset) val root = DisplayTopology.TreeNode( primaryId, /* logicalWidth= */ 200, /* logicalHeight= */ 160, primaryId, DISPLAY_SIZE_1.width, DISPLAY_SIZE_1.height, /* logicalDensity= */ 160, POSITION_LEFT, /* offset= */ 0f) root.addChild(child) return DisplayTopology(root, primaryId) injector.topology = DisplayTopology(root, primaryId) injector.displaysSize = mutableMapOf(Pair(primaryId, DISPLAY_SIZE_1), Pair(42, DISPLAY_SIZE_2)) } /** Uses the topology in the injector to populate and prepare the pane for interaction. */ Loading @@ -187,9 +189,9 @@ class DisplayTopologyPreferenceTest { * Sets up a simple topology in the pane with two displays. Returns the left-hand display and * right-hand display in order in a list. The right-hand display is the root. */ fun setupTwoDisplays(childPosition: Int = POSITION_LEFT, childOffset: Float = 42f): fun setupPaneWithTwoDisplays(childPosition: Int = POSITION_LEFT, childOffset: Float = 42f): List<DisplayBlock> { injector.topology = twoDisplayTopology(childPosition, childOffset) setupTwoDisplays(childPosition, childOffset) preparePane() Loading @@ -210,7 +212,7 @@ class DisplayTopologyPreferenceTest { @Test fun twoDisplaysGenerateBlocks() { val (childBlock, rootBlock) = setupTwoDisplays() val (childBlock, rootBlock) = setupPaneWithTwoDisplays() val childBounds = virtualBounds(childBlock) val rootBounds = virtualBounds(rootBlock) Loading @@ -230,7 +232,7 @@ class DisplayTopologyPreferenceTest { @Test fun dragDisplayDownward() { val (leftBlock, _) = setupTwoDisplays() val (leftBlock, _) = setupPaneWithTwoDisplays() preference.mTimesRefreshedBlocks = 0 Loading Loading @@ -263,7 +265,7 @@ class DisplayTopologyPreferenceTest { @Test fun dragRootDisplayToNewSide() { val (leftBlock, rightBlock) = setupTwoDisplays() val (leftBlock, rightBlock) = setupPaneWithTwoDisplays() val leftBounds = virtualBounds(leftBlock) assertThat(injector.revealLog).containsExactly( Loading Loading @@ -317,7 +319,7 @@ class DisplayTopologyPreferenceTest { @Test fun noRefreshForUnmovingDrag() { val (leftBlock, rightBlock) = setupTwoDisplays() val (leftBlock, rightBlock) = setupPaneWithTwoDisplays() preference.mTimesRefreshedBlocks = 0 Loading @@ -341,13 +343,16 @@ class DisplayTopologyPreferenceTest { @Test fun keepOriginalViewsWhenAddingMore() { setupTwoDisplays() setupPaneWithTwoDisplays() assertThat(injector.revealLog).containsExactly( "revealWallpaper invoked for display 1", "revealWallpaper invoked for display 42") injector.revealLog.clear() val childrenBefore = getPaneChildren() injector.topology!!.addDisplay(/* displayId= */ 101, 320, 240, /* logicalDensity= */ 160) val newDisplayId = 101 val newDisplaySize = Size(320, 240) injector.topology!!.addDisplay(newDisplayId, newDisplaySize.width, newDisplaySize.height, /* logicalDensity= */ 160) injector.displaysSize.put(newDisplayId, newDisplaySize) preference.refreshPane() assertThat(injector.revealLog).containsExactly("revealWallpaper invoked for display 101") injector.revealLog.clear() Loading @@ -358,7 +363,7 @@ class DisplayTopologyPreferenceTest { assertThat(childrenAfter.subList(0, 2)).isEqualTo(childrenBefore) assertThat(injector.topology!!.removeDisplay(42)).isTrue() assertThat(injector.topology!!.removeDisplay(101)).isTrue() assertThat(injector.topology!!.removeDisplay(newDisplayId)).isTrue() preference.refreshPane() assertThat(injector.revealLog).containsExactly( "removed wallpaper revealer for display 42", Loading @@ -367,12 +372,15 @@ class DisplayTopologyPreferenceTest { @Test fun applyNewTopologyViaListenerUpdate() { setupTwoDisplays() setupPaneWithTwoDisplays() preference.mTimesRefreshedBlocks = 0 val newDisplayId = 8008 val newDisplaySize = Size(300, 320) val newTopology = injector.topology!!.copy() newTopology.addDisplay(/* displayId= */ 8008, /* logicalWidth= */ 300, /* logicalHeight= */ 320, /* logicalDensity= */ 160) newTopology.addDisplay(newDisplayId, newDisplaySize.width, newDisplaySize.height, /* logicalDensity= */ 160) injector.displaysSize.put(newDisplayId, newDisplaySize) injector.topology = newTopology injector.topologyListener!!.accept(newTopology) Loading @@ -382,7 +390,7 @@ class DisplayTopologyPreferenceTest { assertThat(paneChildren).hasSize(3) // Look for a display with the same unusual aspect ratio as the one we've added. val expectedAspectRatio = 300f/320f val expectedAspectRatio = newDisplaySize.width.toFloat() / newDisplaySize.height.toFloat() assertThat(paneChildren .map { virtualBounds(it) } .map { it.width() / it.height() } Loading @@ -392,11 +400,11 @@ class DisplayTopologyPreferenceTest { @Test fun ignoreListenerUpdateOfUnchangedTopology() { injector.topology = twoDisplayTopology(POSITION_TOP, /* offset= */ 12.0f) setupTwoDisplays(POSITION_TOP, /* offset= */ 12.0f) preparePane() preference.mTimesRefreshedBlocks = 0 injector.topology = twoDisplayTopology(POSITION_TOP, /* offset= */ 12.1f) setupTwoDisplays(POSITION_TOP, /* offset= */ 12.1f) injector.topologyListener!!.accept(injector.topology!!) assertThat(preference.mTimesRefreshedBlocks).isEqualTo(0) Loading @@ -404,7 +412,7 @@ class DisplayTopologyPreferenceTest { @Test fun cannotMoveSingleDisplay() { injector.topology = singleDisplayTopology() setupSingleDisplay() preparePane() val paneChildren = getPaneChildren() Loading Loading @@ -434,7 +442,7 @@ class DisplayTopologyPreferenceTest { @Test fun updatedTopologyCancelsDragIfNonTrivialChange() { val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f) val (leftBlock, _) = setupPaneWithTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f) assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(143.76f) Loading @@ -449,7 +457,7 @@ class DisplayTopologyPreferenceTest { assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(173.76f) // Offset is only different by 0.5 dp, so the drag will not cancel. injector.topology = twoDisplayTopology(POSITION_LEFT, /* childOffset= */ 41.5f) setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 41.5f) injector.topologyListener!!.accept(injector.topology!!) assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(173.76f) Loading @@ -460,7 +468,7 @@ class DisplayTopologyPreferenceTest { .build()) assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(193.76f) injector.topology = twoDisplayTopology(POSITION_LEFT, /* childOffset= */ 20f) setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 20f) injector.topologyListener!!.accept(injector.topology!!) assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(115.60f) Loading @@ -474,7 +482,7 @@ class DisplayTopologyPreferenceTest { @Test fun highlightDuringDrag() { val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f) val (leftBlock, _) = setupPaneWithTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f) assertSelected(leftBlock, false) leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder() Loading Loading @@ -509,7 +517,7 @@ class DisplayTopologyPreferenceTest { @Test fun accidentalDrag_LittleAndBriefEnoughToBeAccidental() { val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, childOffset = 42f) val (leftBlock, _) = setupPaneWithTwoDisplays(POSITION_LEFT, childOffset = 42f) val startTime = 424242L val startX = leftBlock.x val startY = leftBlock.y Loading @@ -534,7 +542,7 @@ class DisplayTopologyPreferenceTest { @Test fun accidentalDrag_TooFarToBeAccidentalXAxis() { val (topBlock, _) = setupTwoDisplays(POSITION_TOP, childOffset = -42f) val (topBlock, _) = setupPaneWithTwoDisplays(POSITION_TOP, childOffset = -42f) val startTime = 88888L val startX = topBlock.x Loading @@ -550,7 +558,7 @@ class DisplayTopologyPreferenceTest { @Test fun accidentalDrag_TooFarToBeAccidentalYAxis() { val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, childOffset = 42f) val (leftBlock, _) = setupPaneWithTwoDisplays(POSITION_LEFT, childOffset = 42f) val startTime = 88888L val startY = leftBlock.y Loading @@ -566,7 +574,7 @@ class DisplayTopologyPreferenceTest { @Test fun accidentalDrag_TooSlowToBeAccidental() { val (topBlock, _) = setupTwoDisplays(POSITION_TOP, childOffset = -42f) val (topBlock, _) = setupPaneWithTwoDisplays(POSITION_TOP, childOffset = -42f) val startTime = 88888L val startX = topBlock.x Loading @@ -579,4 +587,9 @@ class DisplayTopologyPreferenceTest { assertThat(topBlock.x).isNotEqualTo(startX) assertThat(preference.mTimesRefreshedBlocks).isEqualTo(1) } private companion object { private val DISPLAY_SIZE_1 = Size(200, 160) private val DISPLAY_SIZE_2 = Size(100, 80) } } Loading
res/drawable/display_block_background.xml +0 −10 Original line number Diff line number Diff line Loading @@ -17,16 +17,6 @@ <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <!-- A non-rounded rectangle is needed to prevent the enclosed image from leaking out behind the rounded corners. --> <item> <shape android:shape="rectangle"> <stroke android:color="@color/display_topology_background_color" android:width="@dimen/display_block_padding" /> </shape> </item> <item> <shape android:shape="rectangle"> <stroke Loading
src/com/android/settings/connecteddevice/display/ConnectedDisplayInjector.kt +8 −0 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ import android.os.RemoteException import android.os.SystemProperties import android.util.DisplayMetrics import android.util.Log import android.util.Size import android.view.Display import android.view.Display.INVALID_DISPLAY import android.view.DisplayInfo Loading Loading @@ -290,6 +291,13 @@ open class ConnectedDisplayInjector(open val context: Context?) { } } open fun getLogicalSize(displayId: Int): Size? { val display = displayManager?.getDisplay(displayId)?: return null val displayInfo = DisplayInfo() display.getDisplayInfo(displayInfo) return Size(displayInfo.logicalWidth, displayInfo.logicalHeight) } open fun registerTopologyListener(listener: Consumer<DisplayTopology>) { val executor = context?.mainExecutor if (executor != null) displayManager?.registerTopologyListener(executor, listener) Loading
src/com/android/settings/connecteddevice/display/DisplayBlock.kt +12 −6 Original line number Diff line number Diff line Loading @@ -16,26 +16,25 @@ package com.android.settings.connecteddevice.display import com.android.settings.R import android.content.Context import android.graphics.Bitmap import android.graphics.Color import android.graphics.Outline import android.graphics.PointF import android.util.Log import android.view.SurfaceControl import android.view.SurfaceHolder import android.view.SurfaceView import android.view.View import android.view.ViewOutlineProvider import android.widget.FrameLayout import androidx.annotation.VisibleForTesting import com.android.settings.R /** Represents a draggable block in the topology pane. */ class DisplayBlock(val injector: ConnectedDisplayInjector) : FrameLayout(injector.context!!) { @VisibleForTesting val mHighlightPx = context.resources.getDimensionPixelSize( R.dimen.display_block_highlight_width) val cornerRadiusPx = context.resources.getDimensionPixelSize( R.dimen.display_block_corner_radius) private var mDisplayId: Int? = null Loading Loading @@ -183,6 +182,13 @@ class DisplayBlock(val injector: ConnectedDisplayInjector) : FrameLayout(injecto mWallpaperView.layoutParams = it } mWallpaperView.outlineProvider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx.toFloat()) } } mWallpaperView.clipToOutline = true // The other two child views are MATCH_PARENT by default so will resize to fill up the // FrameLayout. } Loading
src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt +55 −38 Original line number Diff line number Diff line Loading @@ -16,27 +16,21 @@ package com.android.settings.connecteddevice.display import com.android.settings.R import com.android.settingslib.widget.GroupSectionDividerMixin import android.content.Context import android.graphics.Bitmap import android.graphics.PointF import android.graphics.RectF import android.hardware.display.DisplayTopology import android.util.DisplayMetrics import android.util.Log import android.util.Size import android.view.MotionEvent import android.view.View import android.widget.FrameLayout import android.widget.TextView import androidx.annotation.VisibleForTesting import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.android.settings.R import com.android.settingslib.widget.GroupSectionDividerMixin import java.util.function.Consumer import kotlin.math.abs /** Loading Loading @@ -213,11 +207,42 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) recycleableBlocks.add(mPaneContent.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() val scaling = TopologyScale( mPaneContent.width, minEdgeLength = DisplayTopology.dpToPx(60f, injector.densityDpi), maxEdgeLength = DisplayTopology.dpToPx(256f, injector.densityDpi), newBounds.map { it.second }.toList()) minEdgeLength = DisplayTopology.dpToPx(MIN_EDGE_LENGTH_DP, injector.densityDpi), maxEdgeLength = DisplayTopology.dpToPx(MAX_EDGE_LENGTH_DP, injector.densityDpi), newBounds.map { it.second }) setupDisplayPaneAndBlocks(scaling, newBounds, topologyLogicalDisplaySize) mTopologyInfo = TopologyInfo(topology, scaling, newBounds) // 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 (mRevealedWallpapers). val keepRevealing = buildMap<Int, RevealedWallpaper> { mRevealedWallpapers.forEach { r -> if (idToNode.containsKey(r.displayId)) { put(r.displayId, r) } else { r.viewManager.removeView(r.revealer) } } } mRevealedWallpapers = idToNode.keys .map { keepRevealing.get(it) ?: injector.revealWallpaper(it) } .filterNotNull() .toList() } private fun setupDisplayPaneAndBlocks( scaling: TopologyScale, newBounds: List<Pair<Int, RectF>>, topologyLogicalDisplaySize: Map<Int, Size> ) { // Resize pane holder mPaneHolder.layoutParams.let { val newHeight = scaling.paneHeight.toInt() if (it.height != newHeight) { Loading @@ -226,16 +251,25 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) } } val idToNode = topology.allNodesIdMap() // Setup display blocks val recycleableBlocks = ArrayDeque<DisplayBlock>() for (i in 0..mPaneContent.childCount - 1) { recycleableBlocks.add(mPaneContent.getChildAt(i) as DisplayBlock) } newBounds.forEach { (id, pos) -> val block = recycleableBlocks.removeFirstOrNull() ?: DisplayBlock(injector).apply { mPaneContent.addView(this) } idToNode.get(id)?.let { // 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 { 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.logicalWidth) block.reset( id, topLeft, bottomRight, (bottomRight.x - topLeft.x) / it.width ) } block.setOnTouchListener { view, ev -> when (ev.actionMasked) { Loading @@ -248,25 +282,6 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) } mPaneContent.removeViews(newBounds.size, recycleableBlocks.size) mTimesRefreshedBlocks++ mTopologyInfo = TopologyInfo(topology, scaling, newBounds) // 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 (mRevealedWallpapers). val keepRevealing = buildMap<Int, RevealedWallpaper> { mRevealedWallpapers.forEach { r -> if (idToNode.containsKey(r.displayId)) { put(r.displayId, r) } else { r.viewManager.removeView(r.revealer) } } } mRevealedWallpapers = idToNode.keys .map { keepRevealing.get(it) ?: injector.revealWallpaper(it) } .filterNotNull() .toList() // Cancel the drag if one is in progress. mDrag = null } Loading Loading @@ -351,7 +366,9 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) return true } companion object { val TAG = "DisplayTopologyPreference" private companion object { private val MIN_EDGE_LENGTH_DP = 60f private val MAX_EDGE_LENGTH_DP = 256f private val TAG = "DisplayTopologyPreference" } }
tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt +53 −40 Original line number Diff line number Diff line Loading @@ -16,15 +16,14 @@ package com.android.settings.connecteddevice.display 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.content.Context import android.graphics.Color import android.graphics.RectF 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.util.DisplayMetrics import android.util.Size import android.view.MotionEvent import android.view.View import android.view.ViewGroup Loading @@ -33,14 +32,10 @@ import android.widget.FrameLayout import androidx.preference.PreferenceViewHolder import androidx.test.core.app.ApplicationProvider import androidx.test.core.view.MotionEventBuilder import com.android.settings.R import com.google.common.truth.Truth.assertThat import java.util.function.Consumer import kotlin.math.abs import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner Loading @@ -58,6 +53,7 @@ class DisplayTopologyPreferenceTest { } class TestInjector(context : Context) : ConnectedDisplayInjector(context) { var displaysSize = mutableMapOf<Int, Size>() var topology: DisplayTopology? = null var topologyListener: Consumer<DisplayTopology>? = null Loading @@ -73,6 +69,10 @@ class DisplayTopologyPreferenceTest { override val densityDpi = DisplayMetrics.DENSITY_DEFAULT override fun getLogicalSize(displayId: Int): Size? { return displaysSize.get(displayId) } override fun registerTopologyListener(listener: Consumer<DisplayTopology>) { if (topologyListener != null) { throw IllegalStateException( Loading Loading @@ -144,28 +144,30 @@ class DisplayTopologyPreferenceTest { .map { preference.mPaneContent.getChildAt(it) as DisplayBlock } .toList() fun singleDisplayTopology(): DisplayTopology { fun setupSingleDisplay() { val primaryId = 22; val root = DisplayTopology.TreeNode( primaryId, /* logicalWidth= */ 200, /* logicalHeight= */ 160, primaryId, DISPLAY_SIZE_1.width, DISPLAY_SIZE_1.height, /* logicalDensity= */ 160, POSITION_LEFT, /* offset= */ 0f) return DisplayTopology(root, primaryId) injector.topology = DisplayTopology(root, primaryId) injector.displaysSize = mutableMapOf(Pair(primaryId, DISPLAY_SIZE_1)) } fun twoDisplayTopology(childPosition: Int, childOffset: Float): DisplayTopology { fun setupTwoDisplays(childPosition: Int, childOffset: Float) { val primaryId = 1 val child = DisplayTopology.TreeNode( /* displayId= */ 42, /* logicalWidth= */ 100, /* logicalHeight= */ 80, /* displayId= */ 42, DISPLAY_SIZE_2.width, DISPLAY_SIZE_2.height, /* logicalDensity= */ 160, childPosition, childOffset) val root = DisplayTopology.TreeNode( primaryId, /* logicalWidth= */ 200, /* logicalHeight= */ 160, primaryId, DISPLAY_SIZE_1.width, DISPLAY_SIZE_1.height, /* logicalDensity= */ 160, POSITION_LEFT, /* offset= */ 0f) root.addChild(child) return DisplayTopology(root, primaryId) injector.topology = DisplayTopology(root, primaryId) injector.displaysSize = mutableMapOf(Pair(primaryId, DISPLAY_SIZE_1), Pair(42, DISPLAY_SIZE_2)) } /** Uses the topology in the injector to populate and prepare the pane for interaction. */ Loading @@ -187,9 +189,9 @@ class DisplayTopologyPreferenceTest { * Sets up a simple topology in the pane with two displays. Returns the left-hand display and * right-hand display in order in a list. The right-hand display is the root. */ fun setupTwoDisplays(childPosition: Int = POSITION_LEFT, childOffset: Float = 42f): fun setupPaneWithTwoDisplays(childPosition: Int = POSITION_LEFT, childOffset: Float = 42f): List<DisplayBlock> { injector.topology = twoDisplayTopology(childPosition, childOffset) setupTwoDisplays(childPosition, childOffset) preparePane() Loading @@ -210,7 +212,7 @@ class DisplayTopologyPreferenceTest { @Test fun twoDisplaysGenerateBlocks() { val (childBlock, rootBlock) = setupTwoDisplays() val (childBlock, rootBlock) = setupPaneWithTwoDisplays() val childBounds = virtualBounds(childBlock) val rootBounds = virtualBounds(rootBlock) Loading @@ -230,7 +232,7 @@ class DisplayTopologyPreferenceTest { @Test fun dragDisplayDownward() { val (leftBlock, _) = setupTwoDisplays() val (leftBlock, _) = setupPaneWithTwoDisplays() preference.mTimesRefreshedBlocks = 0 Loading Loading @@ -263,7 +265,7 @@ class DisplayTopologyPreferenceTest { @Test fun dragRootDisplayToNewSide() { val (leftBlock, rightBlock) = setupTwoDisplays() val (leftBlock, rightBlock) = setupPaneWithTwoDisplays() val leftBounds = virtualBounds(leftBlock) assertThat(injector.revealLog).containsExactly( Loading Loading @@ -317,7 +319,7 @@ class DisplayTopologyPreferenceTest { @Test fun noRefreshForUnmovingDrag() { val (leftBlock, rightBlock) = setupTwoDisplays() val (leftBlock, rightBlock) = setupPaneWithTwoDisplays() preference.mTimesRefreshedBlocks = 0 Loading @@ -341,13 +343,16 @@ class DisplayTopologyPreferenceTest { @Test fun keepOriginalViewsWhenAddingMore() { setupTwoDisplays() setupPaneWithTwoDisplays() assertThat(injector.revealLog).containsExactly( "revealWallpaper invoked for display 1", "revealWallpaper invoked for display 42") injector.revealLog.clear() val childrenBefore = getPaneChildren() injector.topology!!.addDisplay(/* displayId= */ 101, 320, 240, /* logicalDensity= */ 160) val newDisplayId = 101 val newDisplaySize = Size(320, 240) injector.topology!!.addDisplay(newDisplayId, newDisplaySize.width, newDisplaySize.height, /* logicalDensity= */ 160) injector.displaysSize.put(newDisplayId, newDisplaySize) preference.refreshPane() assertThat(injector.revealLog).containsExactly("revealWallpaper invoked for display 101") injector.revealLog.clear() Loading @@ -358,7 +363,7 @@ class DisplayTopologyPreferenceTest { assertThat(childrenAfter.subList(0, 2)).isEqualTo(childrenBefore) assertThat(injector.topology!!.removeDisplay(42)).isTrue() assertThat(injector.topology!!.removeDisplay(101)).isTrue() assertThat(injector.topology!!.removeDisplay(newDisplayId)).isTrue() preference.refreshPane() assertThat(injector.revealLog).containsExactly( "removed wallpaper revealer for display 42", Loading @@ -367,12 +372,15 @@ class DisplayTopologyPreferenceTest { @Test fun applyNewTopologyViaListenerUpdate() { setupTwoDisplays() setupPaneWithTwoDisplays() preference.mTimesRefreshedBlocks = 0 val newDisplayId = 8008 val newDisplaySize = Size(300, 320) val newTopology = injector.topology!!.copy() newTopology.addDisplay(/* displayId= */ 8008, /* logicalWidth= */ 300, /* logicalHeight= */ 320, /* logicalDensity= */ 160) newTopology.addDisplay(newDisplayId, newDisplaySize.width, newDisplaySize.height, /* logicalDensity= */ 160) injector.displaysSize.put(newDisplayId, newDisplaySize) injector.topology = newTopology injector.topologyListener!!.accept(newTopology) Loading @@ -382,7 +390,7 @@ class DisplayTopologyPreferenceTest { assertThat(paneChildren).hasSize(3) // Look for a display with the same unusual aspect ratio as the one we've added. val expectedAspectRatio = 300f/320f val expectedAspectRatio = newDisplaySize.width.toFloat() / newDisplaySize.height.toFloat() assertThat(paneChildren .map { virtualBounds(it) } .map { it.width() / it.height() } Loading @@ -392,11 +400,11 @@ class DisplayTopologyPreferenceTest { @Test fun ignoreListenerUpdateOfUnchangedTopology() { injector.topology = twoDisplayTopology(POSITION_TOP, /* offset= */ 12.0f) setupTwoDisplays(POSITION_TOP, /* offset= */ 12.0f) preparePane() preference.mTimesRefreshedBlocks = 0 injector.topology = twoDisplayTopology(POSITION_TOP, /* offset= */ 12.1f) setupTwoDisplays(POSITION_TOP, /* offset= */ 12.1f) injector.topologyListener!!.accept(injector.topology!!) assertThat(preference.mTimesRefreshedBlocks).isEqualTo(0) Loading @@ -404,7 +412,7 @@ class DisplayTopologyPreferenceTest { @Test fun cannotMoveSingleDisplay() { injector.topology = singleDisplayTopology() setupSingleDisplay() preparePane() val paneChildren = getPaneChildren() Loading Loading @@ -434,7 +442,7 @@ class DisplayTopologyPreferenceTest { @Test fun updatedTopologyCancelsDragIfNonTrivialChange() { val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f) val (leftBlock, _) = setupPaneWithTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f) assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(143.76f) Loading @@ -449,7 +457,7 @@ class DisplayTopologyPreferenceTest { assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(173.76f) // Offset is only different by 0.5 dp, so the drag will not cancel. injector.topology = twoDisplayTopology(POSITION_LEFT, /* childOffset= */ 41.5f) setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 41.5f) injector.topologyListener!!.accept(injector.topology!!) assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(173.76f) Loading @@ -460,7 +468,7 @@ class DisplayTopologyPreferenceTest { .build()) assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(193.76f) injector.topology = twoDisplayTopology(POSITION_LEFT, /* childOffset= */ 20f) setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 20f) injector.topologyListener!!.accept(injector.topology!!) assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(115.60f) Loading @@ -474,7 +482,7 @@ class DisplayTopologyPreferenceTest { @Test fun highlightDuringDrag() { val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f) val (leftBlock, _) = setupPaneWithTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f) assertSelected(leftBlock, false) leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder() Loading Loading @@ -509,7 +517,7 @@ class DisplayTopologyPreferenceTest { @Test fun accidentalDrag_LittleAndBriefEnoughToBeAccidental() { val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, childOffset = 42f) val (leftBlock, _) = setupPaneWithTwoDisplays(POSITION_LEFT, childOffset = 42f) val startTime = 424242L val startX = leftBlock.x val startY = leftBlock.y Loading @@ -534,7 +542,7 @@ class DisplayTopologyPreferenceTest { @Test fun accidentalDrag_TooFarToBeAccidentalXAxis() { val (topBlock, _) = setupTwoDisplays(POSITION_TOP, childOffset = -42f) val (topBlock, _) = setupPaneWithTwoDisplays(POSITION_TOP, childOffset = -42f) val startTime = 88888L val startX = topBlock.x Loading @@ -550,7 +558,7 @@ class DisplayTopologyPreferenceTest { @Test fun accidentalDrag_TooFarToBeAccidentalYAxis() { val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, childOffset = 42f) val (leftBlock, _) = setupPaneWithTwoDisplays(POSITION_LEFT, childOffset = 42f) val startTime = 88888L val startY = leftBlock.y Loading @@ -566,7 +574,7 @@ class DisplayTopologyPreferenceTest { @Test fun accidentalDrag_TooSlowToBeAccidental() { val (topBlock, _) = setupTwoDisplays(POSITION_TOP, childOffset = -42f) val (topBlock, _) = setupPaneWithTwoDisplays(POSITION_TOP, childOffset = -42f) val startTime = 88888L val startX = topBlock.x Loading @@ -579,4 +587,9 @@ class DisplayTopologyPreferenceTest { assertThat(topBlock.x).isNotEqualTo(startX) assertThat(preference.mTimesRefreshedBlocks).isEqualTo(1) } private companion object { private val DISPLAY_SIZE_1 = Size(200, 160) private val DISPLAY_SIZE_2 = Size(100, 80) } }