[Media TTT] Use a listener pattern to notify about view removals.

In order to handle the swipe-to-dismiss gesture correctly, we need to
notify MediaTttSenderCoordinator about both a view timing out, and a
view being removed due to swipe. So, having the `onViewTimeout` Runnable
will no longer work.

This CL instead uses a listener interface to notify
`MediaTttSenderCoordinator` whenever the view has timed out / been
removed. A future CL will add the swipe-to-dismiss gesture. That gesture
will also trigger this new listener method, so that
MediaTttSenderCoordinator always stays up-to-date.

Bug: 262584940
Test: manual: verify media ttt chipbars with different IDs still works
(id1=triggered, then id2=almostClose. When id2 times out, id1 is
redisplayed, then also times out)
Test: atest MediaTttSenderCoordinatorTest
TemporaryViewDisplayControllerTest

Change-Id: I73c6235718f3b660ea416ca459ae777d8624a7be
This commit is contained in:
Caitlin Shkuratov
2023-01-13 20:07:31 +00:00
parent a2d811ed1e
commit 5a7e9e6349
4 changed files with 400 additions and 49 deletions

View File

@@ -30,6 +30,7 @@ import com.android.systemui.media.taptotransfer.MediaTttFlags
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.media.taptotransfer.common.MediaTttUtils
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
import com.android.systemui.temporarydisplay.ViewPriority
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem
@@ -54,6 +55,7 @@ constructor(
private var displayedState: ChipStateSender? = null
// A map to store current chip state per id.
// TODO(b/265455911): Log whenever we add or remove from the store.
private var stateMap: MutableMap<String, ChipStateSender> = mutableMapOf()
private val commandQueueCallbacks =
@@ -102,10 +104,9 @@ constructor(
}
uiEventLogger.logSenderStateChange(chipState)
stateMap.put(routeInfo.id, chipState)
if (chipState == ChipStateSender.FAR_FROM_RECEIVER) {
// No need to store the state since it is the default state
stateMap.remove(routeInfo.id)
removeIdFromStore(routeInfo.id)
// Return early if we're not displaying a chip anyway
val currentDisplayedState = displayedState ?: return
@@ -126,7 +127,9 @@ constructor(
displayedState = null
chipbarCoordinator.removeView(routeInfo.id, removalReason)
} else {
stateMap[routeInfo.id] = chipState
displayedState = chipState
chipbarCoordinator.registerListener(displayListener)
chipbarCoordinator.displayView(
createChipbarInfo(
chipState,
@@ -135,7 +138,7 @@ constructor(
context,
logger,
)
) { stateMap.remove(routeInfo.id) }
)
}
}
@@ -225,4 +228,14 @@ constructor(
onClickListener,
)
}
private val displayListener =
TemporaryViewDisplayController.Listener { id -> removeIdFromStore(id) }
private fun removeIdFromStore(id: String) {
stateMap.remove(id)
if (stateMap.isEmpty()) {
chipbarCoordinator.unregisterListener(displayListener)
}
}
}

View File

@@ -119,15 +119,26 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
dumpManager.registerNormalDumpable(this)
}
private val listeners: MutableSet<Listener> = mutableSetOf()
/** Registers a listener. */
fun registerListener(listener: Listener) {
listeners.add(listener)
}
/** Unregisters a listener. */
fun unregisterListener(listener: Listener) {
listeners.remove(listener)
}
/**
* Displays the view with the provided [newInfo].
*
* This method handles inflating and attaching the view, then delegates to [updateView] to
* display the correct information in the view.
* @param onViewTimeout a runnable that runs after the view timeout.
*/
@Synchronized
fun displayView(newInfo: T, onViewTimeout: Runnable? = null) {
fun displayView(newInfo: T) {
val timeout = accessibilityManager.getRecommendedTimeoutMillis(
newInfo.timeoutMs,
// Not all views have controls so FLAG_CONTENT_CONTROLS might be superfluous, but
@@ -146,14 +157,13 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
logger.logViewUpdate(newInfo)
currentDisplayInfo.info = newInfo
currentDisplayInfo.timeExpirationMillis = timeExpirationMillis
updateTimeout(currentDisplayInfo, timeout, onViewTimeout)
updateTimeout(currentDisplayInfo, timeout)
updateView(newInfo, view)
return
}
val newDisplayInfo = DisplayInfo(
info = newInfo,
onViewTimeout = onViewTimeout,
timeExpirationMillis = timeExpirationMillis,
// Null values will be updated to non-null if/when this view actually gets displayed
view = null,
@@ -196,7 +206,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
private fun showNewView(newDisplayInfo: DisplayInfo, timeout: Int) {
logger.logViewAddition(newDisplayInfo.info)
createAndAcquireWakeLock(newDisplayInfo)
updateTimeout(newDisplayInfo, timeout, newDisplayInfo.onViewTimeout)
updateTimeout(newDisplayInfo, timeout)
inflateAndUpdateView(newDisplayInfo)
}
@@ -227,19 +237,16 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
/**
* Creates a runnable that will remove [displayInfo] in [timeout] ms from now.
*
* @param onViewTimeout an optional runnable that will be run if the view times out.
* @return a runnable that, when run, will *cancel* the view's timeout.
*/
private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int, onViewTimeout: Runnable?) {
private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int) {
val cancelViewTimeout = mainExecutor.executeDelayed(
{
removeView(displayInfo.info.id, REMOVAL_REASON_TIMEOUT)
onViewTimeout?.run()
},
timeout.toLong()
)
displayInfo.onViewTimeout = onViewTimeout
// Cancel old view timeout and re-set it.
displayInfo.cancelViewTimeout?.run()
displayInfo.cancelViewTimeout = cancelViewTimeout
@@ -317,6 +324,9 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
// event comes in while this view is animating out, we still display the new view
// appropriately.
activeViews.remove(displayInfo)
listeners.forEach {
it.onInfoPermanentlyRemoved(id)
}
// No need to time the view out since it's already gone
displayInfo.cancelViewTimeout?.run()
@@ -380,6 +390,9 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
invalidViews.forEach {
activeViews.remove(it)
logger.logViewExpiration(it.info)
listeners.forEach { listener ->
listener.onInfoPermanentlyRemoved(it.info.id)
}
}
}
@@ -436,6 +449,15 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
onAnimationEnd.run()
}
/** A listener interface to be notified of various view events. */
fun interface Listener {
/**
* Called whenever a [DisplayInfo] with the given [id] has been removed and will never be
* displayed again (unless another call to [updateView] is made).
*/
fun onInfoPermanentlyRemoved(id: String)
}
/** A container for all the display-related state objects. */
inner class DisplayInfo(
/**
@@ -460,11 +482,6 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
*/
var wakeLock: WakeLock?,
/**
* See [displayView].
*/
var onViewTimeout: Runnable?,
/**
* A runnable that, when run, will cancel this view's timeout.
*

View File

@@ -45,13 +45,17 @@ import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger
import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.util.view.ViewUtil
import com.android.systemui.util.wakelock.WakeLockFake
@@ -61,6 +65,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.atLeast
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
@@ -161,9 +166,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
)
underTest.start()
val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
verify(commandQueue).addCallback(callbackCaptor.capture())
commandQueueCallback = callbackCaptor.value!!
setCommandQueueCallback()
}
@Test
@@ -920,6 +923,172 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
verify(windowManager).removeView(any())
}
@Test
fun newState_viewListenerRegistered() {
val mockChipbarCoordinator = mock<ChipbarCoordinator>()
underTest =
MediaTttSenderCoordinator(
mockChipbarCoordinator,
commandQueue,
context,
logger,
mediaTttFlags,
uiEventLogger,
)
underTest.start()
// Re-set the command queue callback since we've created a new [MediaTttSenderCoordinator]
// with a new callback.
setCommandQueueCallback()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
routeInfo,
null,
)
verify(mockChipbarCoordinator).registerListener(any())
}
@Test
fun onInfoPermanentlyRemoved_viewListenerUnregistered() {
val mockChipbarCoordinator = mock<ChipbarCoordinator>()
underTest =
MediaTttSenderCoordinator(
mockChipbarCoordinator,
commandQueue,
context,
logger,
mediaTttFlags,
uiEventLogger,
)
underTest.start()
setCommandQueueCallback()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
routeInfo,
null,
)
val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor))
// WHEN the listener is notified that the view has been removed
listenerCaptor.value.onInfoPermanentlyRemoved(DEFAULT_ID)
// THEN the media coordinator unregisters the listener
verify(mockChipbarCoordinator).unregisterListener(listenerCaptor.value)
}
@Test
fun onInfoPermanentlyRemoved_wrongId_viewListenerNotUnregistered() {
val mockChipbarCoordinator = mock<ChipbarCoordinator>()
underTest =
MediaTttSenderCoordinator(
mockChipbarCoordinator,
commandQueue,
context,
logger,
mediaTttFlags,
uiEventLogger,
)
underTest.start()
setCommandQueueCallback()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
routeInfo,
null,
)
val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor))
// WHEN the listener is notified that a different view has been removed
listenerCaptor.value.onInfoPermanentlyRemoved("differentViewId")
// THEN the media coordinator doesn't unregister the listener
verify(mockChipbarCoordinator, never()).unregisterListener(listenerCaptor.value)
}
@Test
fun farFromReceiverState_viewListenerUnregistered() {
val mockChipbarCoordinator = mock<ChipbarCoordinator>()
underTest =
MediaTttSenderCoordinator(
mockChipbarCoordinator,
commandQueue,
context,
logger,
mediaTttFlags,
uiEventLogger,
)
underTest.start()
setCommandQueueCallback()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
routeInfo,
null,
)
val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor))
// WHEN we go to the FAR_FROM_RECEIVER state
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
routeInfo,
null
)
// THEN the media coordinator unregisters the listener
verify(mockChipbarCoordinator).unregisterListener(listenerCaptor.value)
}
@Test
fun statesWithDifferentIds_onInfoPermanentlyRemovedForOneId_viewListenerNotUnregistered() {
val mockChipbarCoordinator = mock<ChipbarCoordinator>()
underTest =
MediaTttSenderCoordinator(
mockChipbarCoordinator,
commandQueue,
context,
logger,
mediaTttFlags,
uiEventLogger,
)
underTest.start()
setCommandQueueCallback()
// WHEN there are two different media transfers with different IDs
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
MediaRoute2Info.Builder("route1", OTHER_DEVICE_NAME)
.addFeature("feature")
.setClientPackageName(PACKAGE_NAME)
.build(),
null,
)
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
MediaRoute2Info.Builder("route2", OTHER_DEVICE_NAME)
.addFeature("feature")
.setClientPackageName(PACKAGE_NAME)
.build(),
null,
)
val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
verify(mockChipbarCoordinator, atLeast(1)).registerListener(capture(listenerCaptor))
// THEN one of them is removed
listenerCaptor.value.onInfoPermanentlyRemoved("route1")
// THEN the media coordinator doesn't unregister the listener (since route2 is still active)
verify(mockChipbarCoordinator, never()).unregisterListener(listenerCaptor.value)
}
private fun getChipbarView(): ViewGroup {
val viewCaptor = ArgumentCaptor.forClass(View::class.java)
verify(windowManager).addView(viewCaptor.capture(), any())
@@ -960,8 +1129,16 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
null
)
}
private fun setCommandQueueCallback() {
val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>()
verify(commandQueue).addCallback(capture(callbackCaptor))
commandQueueCallback = callbackCaptor.value
reset(commandQueue)
}
}
private const val DEFAULT_ID = "defaultId"
private const val APP_NAME = "Fake app name"
private const val OTHER_DEVICE_NAME = "My Tablet"
private const val BLANK_DEVICE_NAME = " "
@@ -969,13 +1146,13 @@ private const val PACKAGE_NAME = "com.android.systemui"
private const val TIMEOUT = 10000
private val routeInfo =
MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME)
MediaRoute2Info.Builder(DEFAULT_ID, OTHER_DEVICE_NAME)
.addFeature("feature")
.setClientPackageName(PACKAGE_NAME)
.build()
private val routeInfoWithBlankDeviceName =
MediaRoute2Info.Builder("id", BLANK_DEVICE_NAME)
MediaRoute2Info.Builder(DEFAULT_ID, BLANK_DEVICE_NAME)
.addFeature("feature")
.setClientPackageName(PACKAGE_NAME)
.build()

View File

@@ -159,7 +159,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.displayView(getState())
assertThat(fakeWakeLock.isHeld).isTrue()
underTest.removeView("id", "test reason")
underTest.removeView(DEFAULT_ID, "test reason")
assertThat(fakeWakeLock.isHeld).isFalse()
}
@@ -175,6 +175,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test
fun displayView_twiceWithDifferentIds_oldViewRemovedNewViewAdded() {
val listener = registerListener()
underTest.displayView(
ViewInfo(
name = "name",
@@ -199,10 +201,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("First Fake Window Title")
assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
verify(windowManager).removeView(viewCaptor.allValues[0])
// Since the controller is still storing the older view in case it'll get re-displayed
// later, the listener shouldn't be notified
assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun displayView_viewDoesNotDisappearsBeforeTimeout() {
val listener = registerListener()
val state = getState()
underTest.displayView(state)
reset(windowManager)
@@ -210,10 +217,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeClock.advanceTime(TIMEOUT_MS - 1)
verify(windowManager, never()).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun displayView_viewDisappearsAfterTimeout() {
val listener = registerListener()
val state = getState()
underTest.displayView(state)
reset(windowManager)
@@ -221,10 +231,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
}
@Test
fun displayView_calledAgainBeforeTimeout_timeoutReset() {
val listener = registerListener()
// First, display the view
val state = getState()
underTest.displayView(state)
@@ -239,10 +252,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// Verify we didn't hide the view
verify(windowManager, never()).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun displayView_calledAgainBeforeTimeout_eventuallyTimesOut() {
val listener = registerListener()
// First, display the view
val state = getState()
underTest.displayView(state)
@@ -255,6 +271,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
}
@Test
@@ -270,26 +287,10 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
}
@Test
fun viewUpdatedWithNewOnViewTimeoutRunnable_newRunnableUsed() {
var runnable1Run = false
underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) {
runnable1Run = true
}
var runnable2Run = false
underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) {
runnable2Run = true
}
fakeClock.advanceTime(TIMEOUT_MS + 1)
assertThat(runnable1Run).isFalse()
assertThat(runnable2Run).isTrue()
}
@Test
fun multipleViewsWithDifferentIds_moreRecentReplacesOlder() {
val listener = registerListener()
underTest.displayView(
ViewInfo(
name = "name",
@@ -315,10 +316,16 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
verify(windowManager).removeView(viewCaptor.allValues[0])
verify(configurationController, never()).removeCallback(any())
// Since the controller is still storing the older view in case it'll get re-displayed
// later, the listener shouldn't be notified
assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() {
fun multipleViewsWithDifferentIds_newViewRemoved_previousViewIsDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1"))
verify(windowManager).addView(any(), any())
@@ -329,24 +336,35 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager).removeView(any())
verify(windowManager).addView(any(), any())
reset(windowManager)
assertThat(listener.permanentlyRemovedIds).isEmpty()
// WHEN the current view is removed
underTest.removeView("id2", "test reason")
// THEN it's correctly removed
verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("id2")
// And the previous view is correctly added
verify(windowManager).addView(any(), any())
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
// WHEN the previous view times out
reset(windowManager)
fakeClock.advanceTime(TIMEOUT_MS + 1)
// THEN it is also removed
verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id2", "id1"))
}
@Test
fun multipleViewsWithDifferentIds_oldViewRemoved_recentViewIsDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1"))
verify(windowManager).addView(any(), any())
@@ -361,7 +379,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// WHEN an old view is removed
underTest.removeView("id1", "test reason")
// THEN we don't update anything
// THEN we don't update anything except the listener
assertThat(listener.permanentlyRemovedIds).containsExactly("id1")
verify(windowManager, never()).removeView(any())
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
@@ -372,10 +391,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id1", "id2"))
}
@Test
fun multipleViewsWithDifferentIds_threeDifferentViews_recentActiveViewIsDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1"))
underTest.displayView(ViewInfo("Second name", id = "id2"))
underTest.displayView(ViewInfo("Third name", id = "id3"))
@@ -387,6 +409,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.removeView("id3", "test reason")
verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3"))
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
verify(configurationController, never()).removeCallback(any())
@@ -395,6 +418,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3", "id2"))
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
verify(configurationController, never()).removeCallback(any())
@@ -403,6 +427,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3", "id2", "id1"))
assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
}
@@ -438,6 +463,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test
fun multipleViews_mostRecentViewRemoved_otherViewsTimedOutAndNotDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
fakeClock.advanceTime(1000)
underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 4000))
@@ -451,10 +478,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2", "id3")
}
@Test
fun multipleViews_mostRecentViewRemoved_viewWithShortTimeLeftNotDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
fakeClock.advanceTime(1000)
underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 2500))
@@ -467,10 +497,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2")
}
@Test
fun lowerThenHigherPriority_higherReplacesLower() {
val listener = registerListener()
underTest.displayView(
ViewInfo(
name = "normal",
@@ -499,10 +532,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title")
verify(configurationController, never()).removeCallback(any())
// Since the controller is still storing the older view in case it'll get re-displayed
// later, the listener shouldn't be notified
assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun lowerThenHigherPriority_lowerPriorityRedisplayed() {
val listener = registerListener()
underTest.displayView(
ViewInfo(
name = "normal",
@@ -537,6 +575,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// THEN the normal view is re-displayed
verify(windowManager).removeView(viewCaptor.allValues[1])
assertThat(listener.permanentlyRemovedIds).containsExactly("critical")
verify(windowManager).addView(any(), capture(windowParamsCaptor))
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
verify(configurationController, never()).removeCallback(any())
@@ -544,6 +583,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test
fun lowerThenHigherPriority_lowerPriorityNotRedisplayedBecauseTimedOut() {
val listener = registerListener()
underTest.displayView(
ViewInfo(
name = "normal",
@@ -573,6 +614,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews).isEmpty()
verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("critical", "normal")
}
@Test
@@ -609,6 +651,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test
fun higherThenLowerPriority_lowerEventuallyDisplayed() {
val listener = registerListener()
underTest.displayView(
ViewInfo(
name = "critical",
@@ -644,6 +688,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// THEN the second normal view is displayed
verify(windowManager).removeView(viewCaptor.value)
assertThat(listener.permanentlyRemovedIds).containsExactly("critical")
verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
assertThat(underTest.activeViews.size).isEqualTo(1)
@@ -652,6 +697,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test
fun higherThenLowerPriority_lowerNotDisplayedBecauseTimedOut() {
val listener = registerListener()
underTest.displayView(
ViewInfo(
name = "critical",
@@ -691,10 +738,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews).isEmpty()
verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("critical", "normal")
}
@Test
fun criticalThenNewCritical_newCriticalDisplayed() {
val listener = registerListener()
underTest.displayView(
ViewInfo(
name = "critical 1",
@@ -724,10 +774,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 2")
assertThat(underTest.activeViews.size).isEqualTo(2)
verify(configurationController, never()).removeCallback(any())
// Since the controller is still storing the older view in case it'll get re-displayed
// later, the listener shouldn't be notified
assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun normalThenNewNormal_newNormalDisplayed() {
val listener = registerListener()
underTest.displayView(
ViewInfo(
name = "normal 1",
@@ -757,6 +812,9 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 2")
assertThat(underTest.activeViews.size).isEqualTo(2)
verify(configurationController, never()).removeCallback(any())
// Since the controller is still storing the older view in case it'll get re-displayed
// later, the listener shouldn't be notified
assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
@@ -957,25 +1015,103 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
}
@Test
fun removeView_viewRemovedAndRemovalLogged() {
fun removeView_viewRemovedAndRemovalLoggedAndListenerNotified() {
val listener = registerListener()
// First, add the view
underTest.displayView(getState())
// Then, remove it
val reason = "test reason"
val deviceId = "id"
underTest.removeView(deviceId, reason)
underTest.removeView(DEFAULT_ID, reason)
verify(windowManager).removeView(any())
verify(logger).logViewRemoval(deviceId, reason)
verify(logger).logViewRemoval(DEFAULT_ID, reason)
verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
}
@Test
fun removeView_noAdd_viewNotRemoved() {
fun removeView_noAdd_viewNotRemovedAndListenerNotNotified() {
val listener = registerListener()
underTest.removeView("id", "reason")
verify(windowManager, never()).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun listenerRegistered_notifiedOnRemoval() {
val listener = registerListener()
underTest.displayView(getState())
underTest.removeView(DEFAULT_ID, "reason")
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
}
@Test
fun listenerRegistered_notifiedOnTimedOutEvenWhenNotDisplayed() {
val listener = registerListener()
underTest.displayView(
ViewInfo(
id = "id1",
name = "name1",
timeoutMs = 3000,
),
)
// Display a second view
underTest.displayView(
ViewInfo(
id = "id2",
name = "name2",
timeoutMs = 2500,
),
)
// WHEN the second view times out
fakeClock.advanceTime(2501)
// THEN the listener is notified of both IDs, since id2 timed out and id1 doesn't have
// enough time left to be redisplayed
assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2")
}
@Test
fun multipleListeners_allNotified() {
val listener1 = registerListener()
val listener2 = registerListener()
val listener3 = registerListener()
underTest.displayView(getState())
underTest.removeView(DEFAULT_ID, "reason")
assertThat(listener1.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
assertThat(listener2.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
assertThat(listener3.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
}
@Test
fun sameListenerRegisteredMultipleTimes_onlyNotifiedOnce() {
val listener = registerListener()
underTest.registerListener(listener)
underTest.registerListener(listener)
underTest.displayView(getState())
underTest.removeView(DEFAULT_ID, "reason")
assertThat(listener.permanentlyRemovedIds).hasSize(1)
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
}
private fun registerListener(): Listener {
return Listener().also {
underTest.registerListener(it)
}
}
private fun getState(name: String = "name") = ViewInfo(name)
@@ -1030,9 +1166,17 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
override val windowTitle: String = "Window Title",
override val wakeReason: String = "WAKE_REASON",
override val timeoutMs: Int = TIMEOUT_MS.toInt(),
override val id: String = "id",
override val id: String = DEFAULT_ID,
override val priority: ViewPriority = ViewPriority.NORMAL,
) : TemporaryViewInfo()
inner class Listener : TemporaryViewDisplayController.Listener {
val permanentlyRemovedIds = mutableListOf<String>()
override fun onInfoPermanentlyRemoved(id: String) {
permanentlyRemovedIds.add(id)
}
}
}
private const val TIMEOUT_MS = 10000L
private const val DEFAULT_ID = "defaultId"