[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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user