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 androidx.preference.PreferenceViewHolder
|
||||||
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.function.Consumer
|
||||||
|
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -210,6 +212,8 @@ class DisplayTopologyPreference(context : Context)
|
|||||||
*/
|
*/
|
||||||
private var mPaneNeedsRefresh = false
|
private var mPaneNeedsRefresh = false
|
||||||
|
|
||||||
|
private val mTopologyListener = Consumer<DisplayTopology> { applyTopology(it) }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
layoutResource = R.layout.display_topology_preference
|
layoutResource = R.layout.display_topology_preference
|
||||||
|
|
||||||
@@ -238,10 +242,17 @@ class DisplayTopologyPreference(context : Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttached() {
|
override fun onAttached() {
|
||||||
|
super.onAttached()
|
||||||
// We don't know if topology changes happened when we were detached, as it is impossible to
|
// 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
|
// listen at that time (we must remove listeners when detaching). Setting this flag makes
|
||||||
// the following onGlobalLayout call refresh the pane.
|
// the following onGlobalLayout call refresh the pane.
|
||||||
mPaneNeedsRefresh = true
|
mPaneNeedsRefresh = true
|
||||||
|
injector.registerTopologyListener(mTopologyListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetached() {
|
||||||
|
super.onDetached()
|
||||||
|
injector.unregisterTopologyListener(mTopologyListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGlobalLayout() {
|
override fun onGlobalLayout() {
|
||||||
@@ -265,6 +276,14 @@ class DisplayTopologyPreference(context : Context)
|
|||||||
|
|
||||||
open val wallpaper : Drawable
|
open val wallpaper : Drawable
|
||||||
get() = WallpaperManager.getInstance(context).drawable ?: ColorDrawable(Color.BLACK)
|
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 mTopologyInfo : TopologyInfo? = null
|
||||||
private var mDrag : BlockDrag? = null
|
private var mDrag : BlockDrag? = null
|
||||||
|
|
||||||
@VisibleForTesting fun refreshPane() {
|
private fun sameDisplayPosition(a: RectF, b: RectF): Boolean {
|
||||||
val recycleableBlocks = ArrayDeque<DisplayBlock>()
|
// Comparing in display coordinates, so a 1 pixel difference will be less than one dp in
|
||||||
for (i in 0..mPaneContent.childCount-1) {
|
// pane coordinates. Canceling the drag and refreshing the pane will not change the apparent
|
||||||
recycleableBlocks.add(mPaneContent.getChildAt(i) as DisplayBlock)
|
// 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
|
val topology = injector.displayTopology
|
||||||
if (topology == null) {
|
if (topology == null) {
|
||||||
// This occurs when no topology is active.
|
// This occurs when no topology is active.
|
||||||
@@ -309,18 +334,39 @@ class DisplayTopologyPreference(context : Context)
|
|||||||
mTopologyInfo = null
|
mTopologyInfo = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyTopology(topology)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting var mTimesReceivedSameTopology = 0
|
||||||
|
|
||||||
|
private fun applyTopology(topology: DisplayTopology) {
|
||||||
mTopologyHint.text = context.getString(R.string.external_display_topology_hint)
|
mTopologyHint.text = context.getString(R.string.external_display_topology_hint)
|
||||||
|
|
||||||
val blocksPos = buildList {
|
val oldBounds = mTopologyInfo?.positions
|
||||||
|
val newBounds = buildList {
|
||||||
val bounds = topology.absoluteBounds
|
val bounds = topology.absoluteBounds
|
||||||
(0..bounds.size()-1).forEach {
|
(0..bounds.size()-1).forEach {
|
||||||
add(Pair(bounds.keyAt(it), bounds.valueAt(it)))
|
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(
|
val scaling = TopologyScale(
|
||||||
mPaneContent.width, minEdgeLength = 60, maxBlockRatio = 0.12f,
|
mPaneContent.width, minEdgeLength = 60, maxBlockRatio = 0.12f,
|
||||||
blocksPos.map { it.second }.toList())
|
newBounds.map { it.second }.toList())
|
||||||
mPaneHolder.layoutParams.let {
|
mPaneHolder.layoutParams.let {
|
||||||
val newHeight = scaling.paneHeight.toInt()
|
val newHeight = scaling.paneHeight.toInt()
|
||||||
if (it.height != newHeight) {
|
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 {
|
val block = recycleableBlocks.removeFirstOrNull() ?: DisplayBlock(context).apply {
|
||||||
// We need a separate wallpaper Drawable for each display block, since each needs to
|
// We need a separate wallpaper Drawable for each display block, since each needs to
|
||||||
// be drawn at a separate size.
|
// 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(
|
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_BOTTOM
|
||||||
import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT
|
import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT
|
||||||
|
import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
@@ -34,6 +35,10 @@ import androidx.test.core.view.MotionEventBuilder
|
|||||||
import com.android.settings.R
|
import com.android.settings.R
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
||||||
|
import java.util.function.Consumer
|
||||||
|
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
@@ -54,8 +59,9 @@ class DisplayTopologyPreferenceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TestInjector(context : Context) : DisplayTopologyPreference.Injector(context) {
|
class TestInjector(context : Context) : DisplayTopologyPreference.Injector(context) {
|
||||||
var topology : DisplayTopology? = null
|
var topology: DisplayTopology? = null
|
||||||
var systemWallpaper : Drawable? = null
|
var systemWallpaper: Drawable? = null
|
||||||
|
var topologyListener: Consumer<DisplayTopology>? = null
|
||||||
|
|
||||||
override var displayTopology : DisplayTopology?
|
override var displayTopology : DisplayTopology?
|
||||||
get() = topology
|
get() = topology
|
||||||
@@ -63,6 +69,21 @@ class DisplayTopologyPreferenceTest {
|
|||||||
|
|
||||||
override val wallpaper : Drawable
|
override val wallpaper : Drawable
|
||||||
get() = systemWallpaper!!
|
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
|
@Test
|
||||||
@@ -81,20 +102,21 @@ class DisplayTopologyPreferenceTest {
|
|||||||
.map { preference.mPaneContent.getChildAt(it) as DisplayBlock }
|
.map { preference.mPaneContent.getChildAt(it) as DisplayBlock }
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
/**
|
fun twoDisplayTopology(childPosition: Int, childOffset: Float): DisplayTopology {
|
||||||
* Sets up a simple topology in the pane with two displays. Returns the left-hand display and
|
val primaryId = 1
|
||||||
* right-hand display in order in a list. The right-hand display is the root.
|
|
||||||
*/
|
|
||||||
fun setupTwoDisplays(): List<DisplayBlock> {
|
|
||||||
val child = DisplayTopology.TreeNode(
|
val child = DisplayTopology.TreeNode(
|
||||||
/* displayId= */ 42, /* width= */ 100f, /* height= */ 80f,
|
/* displayId= */ 42, /* width= */ 100f, /* height= */ 80f,
|
||||||
POSITION_LEFT, /* offset= */ 42f)
|
childPosition, childOffset)
|
||||||
val root = DisplayTopology.TreeNode(
|
val root = DisplayTopology.TreeNode(
|
||||||
/* displayId= */ 0, /* width= */ 200f, /* height= */ 160f,
|
primaryId, /* width= */ 200f, /* height= */ 160f, POSITION_LEFT, /* offset= */ 0f)
|
||||||
POSITION_LEFT, /* offset= */ 0f)
|
|
||||||
root.addChild(child)
|
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.
|
// This layoutParams needs to be non-null for the global layout handler.
|
||||||
preference.mPaneHolder.layoutParams = FrameLayout.LayoutParams(
|
preference.mPaneHolder.layoutParams = FrameLayout.LayoutParams(
|
||||||
/* width= */ 640, /* height= */ 480)
|
/* width= */ 640, /* height= */ 480)
|
||||||
@@ -106,6 +128,17 @@ class DisplayTopologyPreferenceTest {
|
|||||||
|
|
||||||
preference.onAttached()
|
preference.onAttached()
|
||||||
preference.onGlobalLayout()
|
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()
|
val paneChildren = getPaneChildren()
|
||||||
assertThat(paneChildren).hasSize(2)
|
assertThat(paneChildren).hasSize(2)
|
||||||
@@ -219,4 +252,78 @@ class DisplayTopologyPreferenceTest {
|
|||||||
assertThat(childrenAfter).hasSize(3)
|
assertThat(childrenAfter).hasSize(3)
|
||||||
assertThat(childrenAfter.subList(0, 2)).isEqualTo(childrenBefore)
|
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