Merge "Use topology listener to detect changes" into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
926f75ec5f
@@ -45,7 +45,9 @@ import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
|
||||
import java.util.Locale
|
||||
import java.util.function.Consumer
|
||||
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -210,6 +212,8 @@ class DisplayTopologyPreference(context : Context)
|
||||
*/
|
||||
private var mPaneNeedsRefresh = false
|
||||
|
||||
private val mTopologyListener = Consumer<DisplayTopology> { applyTopology(it) }
|
||||
|
||||
init {
|
||||
layoutResource = R.layout.display_topology_preference
|
||||
|
||||
@@ -238,10 +242,17 @@ class DisplayTopologyPreference(context : Context)
|
||||
}
|
||||
|
||||
override fun onAttached() {
|
||||
super.onAttached()
|
||||
// We don't know if topology changes happened when we were detached, as it is impossible to
|
||||
// listen at that time (we must remove listeners when detaching). Setting this flag makes
|
||||
// the following onGlobalLayout call refresh the pane.
|
||||
mPaneNeedsRefresh = true
|
||||
injector.registerTopologyListener(mTopologyListener)
|
||||
}
|
||||
|
||||
override fun onDetached() {
|
||||
super.onDetached()
|
||||
injector.unregisterTopologyListener(mTopologyListener)
|
||||
}
|
||||
|
||||
override fun onGlobalLayout() {
|
||||
@@ -265,6 +276,14 @@ class DisplayTopologyPreference(context : Context)
|
||||
|
||||
open val wallpaper : Drawable
|
||||
get() = WallpaperManager.getInstance(context).drawable ?: ColorDrawable(Color.BLACK)
|
||||
|
||||
open fun registerTopologyListener(listener: Consumer<DisplayTopology>) {
|
||||
displayManager.registerTopologyListener(context.mainExecutor, listener)
|
||||
}
|
||||
|
||||
open fun unregisterTopologyListener(listener: Consumer<DisplayTopology>) {
|
||||
displayManager.unregisterTopologyListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,12 +313,18 @@ class DisplayTopologyPreference(context : Context)
|
||||
private var mTopologyInfo : TopologyInfo? = null
|
||||
private var mDrag : BlockDrag? = null
|
||||
|
||||
@VisibleForTesting fun refreshPane() {
|
||||
val recycleableBlocks = ArrayDeque<DisplayBlock>()
|
||||
for (i in 0..mPaneContent.childCount-1) {
|
||||
recycleableBlocks.add(mPaneContent.getChildAt(i) as DisplayBlock)
|
||||
}
|
||||
private fun sameDisplayPosition(a: RectF, b: RectF): Boolean {
|
||||
// Comparing in display coordinates, so a 1 pixel difference will be less than one dp in
|
||||
// pane coordinates. Canceling the drag and refreshing the pane will not change the apparent
|
||||
// position of displays in the pane.
|
||||
val EPSILON = 1f
|
||||
return EPSILON > abs(a.left - b.left) &&
|
||||
EPSILON > abs(a.right - b.right) &&
|
||||
EPSILON > abs(a.top - b.top) &&
|
||||
EPSILON > abs(a.bottom - b.bottom)
|
||||
}
|
||||
|
||||
@VisibleForTesting fun refreshPane() {
|
||||
val topology = injector.displayTopology
|
||||
if (topology == null) {
|
||||
// This occurs when no topology is active.
|
||||
@@ -309,18 +334,39 @@ class DisplayTopologyPreference(context : Context)
|
||||
mTopologyInfo = null
|
||||
return
|
||||
}
|
||||
|
||||
applyTopology(topology)
|
||||
}
|
||||
|
||||
@VisibleForTesting var mTimesReceivedSameTopology = 0
|
||||
|
||||
private fun applyTopology(topology: DisplayTopology) {
|
||||
mTopologyHint.text = context.getString(R.string.external_display_topology_hint)
|
||||
|
||||
val blocksPos = buildList {
|
||||
val oldBounds = mTopologyInfo?.positions
|
||||
val newBounds = buildList {
|
||||
val bounds = topology.absoluteBounds
|
||||
(0..bounds.size()-1).forEach {
|
||||
add(Pair(bounds.keyAt(it), bounds.valueAt(it)))
|
||||
}
|
||||
}
|
||||
|
||||
if (oldBounds != null && oldBounds.size == newBounds.size &&
|
||||
oldBounds.zip(newBounds).all { (old, new) ->
|
||||
old.first == new.first && sameDisplayPosition(old.second, new.second)
|
||||
}) {
|
||||
mTimesReceivedSameTopology++
|
||||
return
|
||||
}
|
||||
|
||||
val recycleableBlocks = ArrayDeque<DisplayBlock>()
|
||||
for (i in 0..mPaneContent.childCount-1) {
|
||||
recycleableBlocks.add(mPaneContent.getChildAt(i) as DisplayBlock)
|
||||
}
|
||||
|
||||
val scaling = TopologyScale(
|
||||
mPaneContent.width, minEdgeLength = 60, maxBlockRatio = 0.12f,
|
||||
blocksPos.map { it.second }.toList())
|
||||
newBounds.map { it.second }.toList())
|
||||
mPaneHolder.layoutParams.let {
|
||||
val newHeight = scaling.paneHeight.toInt()
|
||||
if (it.height != newHeight) {
|
||||
@@ -329,7 +375,7 @@ class DisplayTopologyPreference(context : Context)
|
||||
}
|
||||
}
|
||||
|
||||
blocksPos.forEach { (id, pos) ->
|
||||
newBounds.forEach { (id, pos) ->
|
||||
val block = recycleableBlocks.removeFirstOrNull() ?: DisplayBlock(context).apply {
|
||||
// We need a separate wallpaper Drawable for each display block, since each needs to
|
||||
// be drawn at a separate size.
|
||||
@@ -348,9 +394,12 @@ class DisplayTopologyPreference(context : Context)
|
||||
}
|
||||
}
|
||||
}
|
||||
mPaneContent.removeViews(blocksPos.size, recycleableBlocks.size)
|
||||
mPaneContent.removeViews(newBounds.size, recycleableBlocks.size)
|
||||
|
||||
mTopologyInfo = TopologyInfo(topology, scaling, blocksPos)
|
||||
mTopologyInfo = TopologyInfo(topology, scaling, newBounds)
|
||||
|
||||
// Cancel the drag if one is in progress.
|
||||
mDrag = null
|
||||
}
|
||||
|
||||
private fun onBlockTouchDown(
|
||||
|
@@ -18,6 +18,7 @@ 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
|
||||
@@ -34,6 +35,10 @@ 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
|
||||
@@ -54,8 +59,9 @@ class DisplayTopologyPreferenceTest {
|
||||
}
|
||||
|
||||
class TestInjector(context : Context) : DisplayTopologyPreference.Injector(context) {
|
||||
var topology : DisplayTopology? = null
|
||||
var systemWallpaper : Drawable? = null
|
||||
var topology: DisplayTopology? = null
|
||||
var systemWallpaper: Drawable? = null
|
||||
var topologyListener: Consumer<DisplayTopology>? = null
|
||||
|
||||
override var displayTopology : DisplayTopology?
|
||||
get() = topology
|
||||
@@ -63,6 +69,21 @@ class DisplayTopologyPreferenceTest {
|
||||
|
||||
override val wallpaper : Drawable
|
||||
get() = systemWallpaper!!
|
||||
|
||||
override fun registerTopologyListener(listener: Consumer<DisplayTopology>) {
|
||||
if (topologyListener != null) {
|
||||
throw IllegalStateException(
|
||||
"already have a listener registered: ${topologyListener}")
|
||||
}
|
||||
topologyListener = listener
|
||||
}
|
||||
|
||||
override fun unregisterTopologyListener(listener: Consumer<DisplayTopology>) {
|
||||
if (topologyListener != listener) {
|
||||
throw IllegalStateException("no such listener registered: ${listener}")
|
||||
}
|
||||
topologyListener = null
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -81,20 +102,21 @@ class DisplayTopologyPreferenceTest {
|
||||
.map { preference.mPaneContent.getChildAt(it) as DisplayBlock }
|
||||
.toList()
|
||||
|
||||
/**
|
||||
* 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(): List<DisplayBlock> {
|
||||
fun twoDisplayTopology(childPosition: Int, childOffset: Float): DisplayTopology {
|
||||
val primaryId = 1
|
||||
|
||||
val child = DisplayTopology.TreeNode(
|
||||
/* displayId= */ 42, /* width= */ 100f, /* height= */ 80f,
|
||||
POSITION_LEFT, /* offset= */ 42f)
|
||||
childPosition, childOffset)
|
||||
val root = DisplayTopology.TreeNode(
|
||||
/* displayId= */ 0, /* width= */ 200f, /* height= */ 160f,
|
||||
POSITION_LEFT, /* offset= */ 0f)
|
||||
primaryId, /* width= */ 200f, /* height= */ 160f, POSITION_LEFT, /* offset= */ 0f)
|
||||
root.addChild(child)
|
||||
injector.topology = DisplayTopology(root, /*primaryDisplayId=*/ 0)
|
||||
|
||||
return DisplayTopology(root, primaryId)
|
||||
}
|
||||
|
||||
/** Uses the topology in the injector to populate and prepare the pane for interaction. */
|
||||
fun preparePane() {
|
||||
// This layoutParams needs to be non-null for the global layout handler.
|
||||
preference.mPaneHolder.layoutParams = FrameLayout.LayoutParams(
|
||||
/* width= */ 640, /* height= */ 480)
|
||||
@@ -106,6 +128,17 @@ class DisplayTopologyPreferenceTest {
|
||||
|
||||
preference.onAttached()
|
||||
preference.onGlobalLayout()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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):
|
||||
List<DisplayBlock> {
|
||||
injector.topology = twoDisplayTopology(childPosition, childOffset)
|
||||
|
||||
preparePane()
|
||||
|
||||
val paneChildren = getPaneChildren()
|
||||
assertThat(paneChildren).hasSize(2)
|
||||
@@ -219,4 +252,78 @@ class DisplayTopologyPreferenceTest {
|
||||
assertThat(childrenAfter).hasSize(3)
|
||||
assertThat(childrenAfter.subList(0, 2)).isEqualTo(childrenBefore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun applyNewTopologyViaListenerUpdate() {
|
||||
setupTwoDisplays()
|
||||
val newTopology = injector.topology!!.copy()
|
||||
newTopology.addDisplay(/* displayId= */ 8008, /* width= */ 300f, /* height= */ 320f)
|
||||
|
||||
injector.topology = newTopology
|
||||
injector.topologyListener!!.accept(newTopology)
|
||||
|
||||
assertThat(preference.mTimesReceivedSameTopology).isEqualTo(0)
|
||||
val paneChildren = getPaneChildren()
|
||||
assertThat(paneChildren).hasSize(3)
|
||||
|
||||
// Look for a display with the same unusual aspect ratio as the one we've added.
|
||||
val expectedAspectRatio = 300f/320f
|
||||
assertThat(paneChildren
|
||||
.map { (it.layoutParams.width.toFloat() + BLOCK_PADDING*2) /
|
||||
(it.layoutParams.height.toFloat() + BLOCK_PADDING*2) }
|
||||
.filter { abs(it - expectedAspectRatio) < 0.001f }
|
||||
).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoreListenerUpdateOfUnchangedTopology() {
|
||||
injector.topology = twoDisplayTopology(POSITION_TOP, /* offset= */ 12.0f)
|
||||
preparePane()
|
||||
|
||||
assertThat(preference.mTimesReceivedSameTopology).isEqualTo(0)
|
||||
injector.topology = twoDisplayTopology(POSITION_TOP, /* offset= */ 12.1f)
|
||||
injector.topologyListener!!.accept(injector.topology!!)
|
||||
|
||||
assertThat(preference.mTimesReceivedSameTopology).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updatedTopologyCancelsDragIfNonTrivialChange() {
|
||||
val (leftBlock, rightBlock) = setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f)
|
||||
|
||||
assertThat(leftBlock.unpaddedY).isWithin(0.01f).of(142.17f)
|
||||
|
||||
leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
|
||||
.setAction(MotionEvent.ACTION_DOWN)
|
||||
.setPointer(0f, 0f)
|
||||
.build())
|
||||
leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
|
||||
.setAction(MotionEvent.ACTION_MOVE)
|
||||
.setPointer(0f, 30f)
|
||||
.build())
|
||||
assertThat(leftBlock.unpaddedY).isWithin(0.01f).of(172.17f)
|
||||
|
||||
// Offset is only different by 0.5 dp, so the drag will not cancel.
|
||||
injector.topology = twoDisplayTopology(POSITION_LEFT, /* childOffset= */ 41.5f)
|
||||
injector.topologyListener!!.accept(injector.topology!!)
|
||||
|
||||
assertThat(leftBlock.unpaddedY).isWithin(0.01f).of(172.17f)
|
||||
// Move block farther downward.
|
||||
leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
|
||||
.setAction(MotionEvent.ACTION_MOVE)
|
||||
.setPointer(0f, 50f)
|
||||
.build())
|
||||
assertThat(leftBlock.unpaddedY).isWithin(0.01f).of(192.17f)
|
||||
|
||||
injector.topology = twoDisplayTopology(POSITION_LEFT, /* childOffset= */ 20f)
|
||||
injector.topologyListener!!.accept(injector.topology!!)
|
||||
|
||||
assertThat(leftBlock.unpaddedY).isWithin(0.01f).of(125.67f)
|
||||
// Another move in the opposite direction should not move the left block.
|
||||
leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
|
||||
.setAction(MotionEvent.ACTION_MOVE)
|
||||
.setPointer(0f, -20f)
|
||||
.build())
|
||||
assertThat(leftBlock.unpaddedY).isWithin(0.01f).of(125.67f)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user