[Media TTT] Use a listener pattern to notify about view removals. am: 5a7e9e6349

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21020063

Change-Id: Ib292e14c52e26fbf5eb676ecd9f6ac5300b65b26
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Caitlin Shkuratov
2023-01-19 22:42:57 +00:00
committed by Automerger Merge Worker
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.MediaTttLogger
import com.android.systemui.media.taptotransfer.common.MediaTttUtils import com.android.systemui.media.taptotransfer.common.MediaTttUtils
import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.temporarydisplay.ViewPriority
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem
@@ -54,6 +55,7 @@ constructor(
private var displayedState: ChipStateSender? = null private var displayedState: ChipStateSender? = null
// A map to store current chip state per id. // 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 var stateMap: MutableMap<String, ChipStateSender> = mutableMapOf()
private val commandQueueCallbacks = private val commandQueueCallbacks =
@@ -102,10 +104,9 @@ constructor(
} }
uiEventLogger.logSenderStateChange(chipState) uiEventLogger.logSenderStateChange(chipState)
stateMap.put(routeInfo.id, chipState)
if (chipState == ChipStateSender.FAR_FROM_RECEIVER) { if (chipState == ChipStateSender.FAR_FROM_RECEIVER) {
// No need to store the state since it is the default state // 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 // Return early if we're not displaying a chip anyway
val currentDisplayedState = displayedState ?: return val currentDisplayedState = displayedState ?: return
@@ -126,7 +127,9 @@ constructor(
displayedState = null displayedState = null
chipbarCoordinator.removeView(routeInfo.id, removalReason) chipbarCoordinator.removeView(routeInfo.id, removalReason)
} else { } else {
stateMap[routeInfo.id] = chipState
displayedState = chipState displayedState = chipState
chipbarCoordinator.registerListener(displayListener)
chipbarCoordinator.displayView( chipbarCoordinator.displayView(
createChipbarInfo( createChipbarInfo(
chipState, chipState,
@@ -135,7 +138,7 @@ constructor(
context, context,
logger, logger,
) )
) { stateMap.remove(routeInfo.id) } )
} }
} }
@@ -225,4 +228,14 @@ constructor(
onClickListener, 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) 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]. * Displays the view with the provided [newInfo].
* *
* This method handles inflating and attaching the view, then delegates to [updateView] to * This method handles inflating and attaching the view, then delegates to [updateView] to
* display the correct information in the view. * display the correct information in the view.
* @param onViewTimeout a runnable that runs after the view timeout.
*/ */
@Synchronized @Synchronized
fun displayView(newInfo: T, onViewTimeout: Runnable? = null) { fun displayView(newInfo: T) {
val timeout = accessibilityManager.getRecommendedTimeoutMillis( val timeout = accessibilityManager.getRecommendedTimeoutMillis(
newInfo.timeoutMs, newInfo.timeoutMs,
// Not all views have controls so FLAG_CONTENT_CONTROLS might be superfluous, but // 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) logger.logViewUpdate(newInfo)
currentDisplayInfo.info = newInfo currentDisplayInfo.info = newInfo
currentDisplayInfo.timeExpirationMillis = timeExpirationMillis currentDisplayInfo.timeExpirationMillis = timeExpirationMillis
updateTimeout(currentDisplayInfo, timeout, onViewTimeout) updateTimeout(currentDisplayInfo, timeout)
updateView(newInfo, view) updateView(newInfo, view)
return return
} }
val newDisplayInfo = DisplayInfo( val newDisplayInfo = DisplayInfo(
info = newInfo, info = newInfo,
onViewTimeout = onViewTimeout,
timeExpirationMillis = timeExpirationMillis, timeExpirationMillis = timeExpirationMillis,
// Null values will be updated to non-null if/when this view actually gets displayed // Null values will be updated to non-null if/when this view actually gets displayed
view = null, view = null,
@@ -196,7 +206,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
private fun showNewView(newDisplayInfo: DisplayInfo, timeout: Int) { private fun showNewView(newDisplayInfo: DisplayInfo, timeout: Int) {
logger.logViewAddition(newDisplayInfo.info) logger.logViewAddition(newDisplayInfo.info)
createAndAcquireWakeLock(newDisplayInfo) createAndAcquireWakeLock(newDisplayInfo)
updateTimeout(newDisplayInfo, timeout, newDisplayInfo.onViewTimeout) updateTimeout(newDisplayInfo, timeout)
inflateAndUpdateView(newDisplayInfo) 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. * 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. * @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( val cancelViewTimeout = mainExecutor.executeDelayed(
{ {
removeView(displayInfo.info.id, REMOVAL_REASON_TIMEOUT) removeView(displayInfo.info.id, REMOVAL_REASON_TIMEOUT)
onViewTimeout?.run()
}, },
timeout.toLong() timeout.toLong()
) )
displayInfo.onViewTimeout = onViewTimeout
// Cancel old view timeout and re-set it. // Cancel old view timeout and re-set it.
displayInfo.cancelViewTimeout?.run() displayInfo.cancelViewTimeout?.run()
displayInfo.cancelViewTimeout = cancelViewTimeout 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 // event comes in while this view is animating out, we still display the new view
// appropriately. // appropriately.
activeViews.remove(displayInfo) activeViews.remove(displayInfo)
listeners.forEach {
it.onInfoPermanentlyRemoved(id)
}
// No need to time the view out since it's already gone // No need to time the view out since it's already gone
displayInfo.cancelViewTimeout?.run() displayInfo.cancelViewTimeout?.run()
@@ -380,6 +390,9 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
invalidViews.forEach { invalidViews.forEach {
activeViews.remove(it) activeViews.remove(it)
logger.logViewExpiration(it.info) 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() 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. */ /** A container for all the display-related state objects. */
inner class DisplayInfo( inner class DisplayInfo(
/** /**
@@ -460,11 +482,6 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
*/ */
var wakeLock: WakeLock?, var wakeLock: WakeLock?,
/**
* See [displayView].
*/
var onViewTimeout: Runnable?,
/** /**
* A runnable that, when run, will cancel this view's timeout. * 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.CommandQueue
import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController 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.ChipbarCoordinator
import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger
import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any 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.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.util.view.ViewUtil import com.android.systemui.util.view.ViewUtil
import com.android.systemui.util.wakelock.WakeLockFake import com.android.systemui.util.wakelock.WakeLockFake
@@ -61,6 +65,7 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.atLeast
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.reset import org.mockito.Mockito.reset
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
@@ -161,9 +166,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
) )
underTest.start() underTest.start()
val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) setCommandQueueCallback()
verify(commandQueue).addCallback(callbackCaptor.capture())
commandQueueCallback = callbackCaptor.value!!
} }
@Test @Test
@@ -920,6 +923,172 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
verify(windowManager).removeView(any()) 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 { private fun getChipbarView(): ViewGroup {
val viewCaptor = ArgumentCaptor.forClass(View::class.java) val viewCaptor = ArgumentCaptor.forClass(View::class.java)
verify(windowManager).addView(viewCaptor.capture(), any()) verify(windowManager).addView(viewCaptor.capture(), any())
@@ -960,8 +1129,16 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
null 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 APP_NAME = "Fake app name"
private const val OTHER_DEVICE_NAME = "My Tablet" private const val OTHER_DEVICE_NAME = "My Tablet"
private const val BLANK_DEVICE_NAME = " " private const val BLANK_DEVICE_NAME = " "
@@ -969,13 +1146,13 @@ private const val PACKAGE_NAME = "com.android.systemui"
private const val TIMEOUT = 10000 private const val TIMEOUT = 10000
private val routeInfo = private val routeInfo =
MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) MediaRoute2Info.Builder(DEFAULT_ID, OTHER_DEVICE_NAME)
.addFeature("feature") .addFeature("feature")
.setClientPackageName(PACKAGE_NAME) .setClientPackageName(PACKAGE_NAME)
.build() .build()
private val routeInfoWithBlankDeviceName = private val routeInfoWithBlankDeviceName =
MediaRoute2Info.Builder("id", BLANK_DEVICE_NAME) MediaRoute2Info.Builder(DEFAULT_ID, BLANK_DEVICE_NAME)
.addFeature("feature") .addFeature("feature")
.setClientPackageName(PACKAGE_NAME) .setClientPackageName(PACKAGE_NAME)
.build() .build()

View File

@@ -159,7 +159,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.displayView(getState()) underTest.displayView(getState())
assertThat(fakeWakeLock.isHeld).isTrue() assertThat(fakeWakeLock.isHeld).isTrue()
underTest.removeView("id", "test reason") underTest.removeView(DEFAULT_ID, "test reason")
assertThat(fakeWakeLock.isHeld).isFalse() assertThat(fakeWakeLock.isHeld).isFalse()
} }
@@ -175,6 +175,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test @Test
fun displayView_twiceWithDifferentIds_oldViewRemovedNewViewAdded() { fun displayView_twiceWithDifferentIds_oldViewRemovedNewViewAdded() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "name", name = "name",
@@ -199,10 +201,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("First Fake Window Title") assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("First Fake Window Title")
assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title") assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
verify(windowManager).removeView(viewCaptor.allValues[0]) 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 @Test
fun displayView_viewDoesNotDisappearsBeforeTimeout() { fun displayView_viewDoesNotDisappearsBeforeTimeout() {
val listener = registerListener()
val state = getState() val state = getState()
underTest.displayView(state) underTest.displayView(state)
reset(windowManager) reset(windowManager)
@@ -210,10 +217,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeClock.advanceTime(TIMEOUT_MS - 1) fakeClock.advanceTime(TIMEOUT_MS - 1)
verify(windowManager, never()).removeView(any()) verify(windowManager, never()).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEmpty()
} }
@Test @Test
fun displayView_viewDisappearsAfterTimeout() { fun displayView_viewDisappearsAfterTimeout() {
val listener = registerListener()
val state = getState() val state = getState()
underTest.displayView(state) underTest.displayView(state)
reset(windowManager) reset(windowManager)
@@ -221,10 +231,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeClock.advanceTime(TIMEOUT_MS + 1) fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
} }
@Test @Test
fun displayView_calledAgainBeforeTimeout_timeoutReset() { fun displayView_calledAgainBeforeTimeout_timeoutReset() {
val listener = registerListener()
// First, display the view // First, display the view
val state = getState() val state = getState()
underTest.displayView(state) underTest.displayView(state)
@@ -239,10 +252,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// Verify we didn't hide the view // Verify we didn't hide the view
verify(windowManager, never()).removeView(any()) verify(windowManager, never()).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEmpty()
} }
@Test @Test
fun displayView_calledAgainBeforeTimeout_eventuallyTimesOut() { fun displayView_calledAgainBeforeTimeout_eventuallyTimesOut() {
val listener = registerListener()
// First, display the view // First, display the view
val state = getState() val state = getState()
underTest.displayView(state) underTest.displayView(state)
@@ -255,6 +271,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeClock.advanceTime(TIMEOUT_MS + 1) fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
} }
@Test @Test
@@ -270,26 +287,10 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name") 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 @Test
fun multipleViewsWithDifferentIds_moreRecentReplacesOlder() { fun multipleViewsWithDifferentIds_moreRecentReplacesOlder() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "name", name = "name",
@@ -315,10 +316,16 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title") assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
verify(windowManager).removeView(viewCaptor.allValues[0]) verify(windowManager).removeView(viewCaptor.allValues[0])
verify(configurationController, never()).removeCallback(any()) 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 @Test
fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() { fun multipleViewsWithDifferentIds_newViewRemoved_previousViewIsDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1")) underTest.displayView(ViewInfo("First name", id = "id1"))
verify(windowManager).addView(any(), any()) verify(windowManager).addView(any(), any())
@@ -329,24 +336,35 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
verify(windowManager).addView(any(), any()) verify(windowManager).addView(any(), any())
reset(windowManager) reset(windowManager)
assertThat(listener.permanentlyRemovedIds).isEmpty()
// WHEN the current view is removed
underTest.removeView("id2", "test reason") underTest.removeView("id2", "test reason")
// THEN it's correctly removed
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("id2")
// And the previous view is correctly added
verify(windowManager).addView(any(), any()) verify(windowManager).addView(any(), any())
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1") assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
// WHEN the previous view times out
reset(windowManager) reset(windowManager)
fakeClock.advanceTime(TIMEOUT_MS + 1) fakeClock.advanceTime(TIMEOUT_MS + 1)
// THEN it is also removed
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0) assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id2", "id1"))
} }
@Test @Test
fun multipleViewsWithDifferentIds_oldViewRemoved_recentViewIsDisplayed() { fun multipleViewsWithDifferentIds_oldViewRemoved_recentViewIsDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1")) underTest.displayView(ViewInfo("First name", id = "id1"))
verify(windowManager).addView(any(), any()) verify(windowManager).addView(any(), any())
@@ -361,7 +379,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// WHEN an old view is removed // WHEN an old view is removed
underTest.removeView("id1", "test reason") 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()) verify(windowManager, never()).removeView(any())
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2") assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
@@ -372,10 +391,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0) assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id1", "id2"))
} }
@Test @Test
fun multipleViewsWithDifferentIds_threeDifferentViews_recentActiveViewIsDisplayed() { fun multipleViewsWithDifferentIds_threeDifferentViews_recentActiveViewIsDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1")) underTest.displayView(ViewInfo("First name", id = "id1"))
underTest.displayView(ViewInfo("Second name", id = "id2")) underTest.displayView(ViewInfo("Second name", id = "id2"))
underTest.displayView(ViewInfo("Third name", id = "id3")) underTest.displayView(ViewInfo("Third name", id = "id3"))
@@ -387,6 +409,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.removeView("id3", "test reason") underTest.removeView("id3", "test reason")
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3"))
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2") assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
verify(configurationController, never()).removeCallback(any()) verify(configurationController, never()).removeCallback(any())
@@ -395,6 +418,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.removeView("id2", "test reason") underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3", "id2"))
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1") assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
verify(configurationController, never()).removeCallback(any()) verify(configurationController, never()).removeCallback(any())
@@ -403,6 +427,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeClock.advanceTime(TIMEOUT_MS + 1) fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3", "id2", "id1"))
assertThat(underTest.activeViews.size).isEqualTo(0) assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
} }
@@ -438,6 +463,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test @Test
fun multipleViews_mostRecentViewRemoved_otherViewsTimedOutAndNotDisplayed() { fun multipleViews_mostRecentViewRemoved_otherViewsTimedOutAndNotDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000)) underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
fakeClock.advanceTime(1000) fakeClock.advanceTime(1000)
underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 4000)) underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 4000))
@@ -451,10 +478,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager, never()).addView(any(), any()) verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews.size).isEqualTo(0) assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2", "id3")
} }
@Test @Test
fun multipleViews_mostRecentViewRemoved_viewWithShortTimeLeftNotDisplayed() { fun multipleViews_mostRecentViewRemoved_viewWithShortTimeLeftNotDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000)) underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
fakeClock.advanceTime(1000) fakeClock.advanceTime(1000)
underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 2500)) underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 2500))
@@ -467,10 +497,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager, never()).addView(any(), any()) verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews.size).isEqualTo(0) assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2")
} }
@Test @Test
fun lowerThenHigherPriority_higherReplacesLower() { fun lowerThenHigherPriority_higherReplacesLower() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "normal", name = "normal",
@@ -499,10 +532,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title") assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title")
verify(configurationController, never()).removeCallback(any()) 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 @Test
fun lowerThenHigherPriority_lowerPriorityRedisplayed() { fun lowerThenHigherPriority_lowerPriorityRedisplayed() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "normal", name = "normal",
@@ -537,6 +575,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// THEN the normal view is re-displayed // THEN the normal view is re-displayed
verify(windowManager).removeView(viewCaptor.allValues[1]) verify(windowManager).removeView(viewCaptor.allValues[1])
assertThat(listener.permanentlyRemovedIds).containsExactly("critical")
verify(windowManager).addView(any(), capture(windowParamsCaptor)) verify(windowManager).addView(any(), capture(windowParamsCaptor))
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
verify(configurationController, never()).removeCallback(any()) verify(configurationController, never()).removeCallback(any())
@@ -544,6 +583,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test @Test
fun lowerThenHigherPriority_lowerPriorityNotRedisplayedBecauseTimedOut() { fun lowerThenHigherPriority_lowerPriorityNotRedisplayedBecauseTimedOut() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "normal", name = "normal",
@@ -573,6 +614,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager, never()).addView(any(), any()) verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews).isEmpty() assertThat(underTest.activeViews).isEmpty()
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("critical", "normal")
} }
@Test @Test
@@ -609,6 +651,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test @Test
fun higherThenLowerPriority_lowerEventuallyDisplayed() { fun higherThenLowerPriority_lowerEventuallyDisplayed() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "critical", name = "critical",
@@ -644,6 +688,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// THEN the second normal view is displayed // THEN the second normal view is displayed
verify(windowManager).removeView(viewCaptor.value) verify(windowManager).removeView(viewCaptor.value)
assertThat(listener.permanentlyRemovedIds).containsExactly("critical")
verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
assertThat(underTest.activeViews.size).isEqualTo(1) assertThat(underTest.activeViews.size).isEqualTo(1)
@@ -652,6 +697,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test @Test
fun higherThenLowerPriority_lowerNotDisplayedBecauseTimedOut() { fun higherThenLowerPriority_lowerNotDisplayedBecauseTimedOut() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "critical", name = "critical",
@@ -691,10 +738,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager, never()).addView(any(), any()) verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews).isEmpty() assertThat(underTest.activeViews).isEmpty()
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("critical", "normal")
} }
@Test @Test
fun criticalThenNewCritical_newCriticalDisplayed() { fun criticalThenNewCritical_newCriticalDisplayed() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "critical 1", name = "critical 1",
@@ -724,10 +774,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 2") assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 2")
assertThat(underTest.activeViews.size).isEqualTo(2) assertThat(underTest.activeViews.size).isEqualTo(2)
verify(configurationController, never()).removeCallback(any()) 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 @Test
fun normalThenNewNormal_newNormalDisplayed() { fun normalThenNewNormal_newNormalDisplayed() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "normal 1", name = "normal 1",
@@ -757,6 +812,9 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 2") assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 2")
assertThat(underTest.activeViews.size).isEqualTo(2) assertThat(underTest.activeViews.size).isEqualTo(2)
verify(configurationController, never()).removeCallback(any()) 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 @Test
@@ -957,25 +1015,103 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
} }
@Test @Test
fun removeView_viewRemovedAndRemovalLogged() { fun removeView_viewRemovedAndRemovalLoggedAndListenerNotified() {
val listener = registerListener()
// First, add the view // First, add the view
underTest.displayView(getState()) underTest.displayView(getState())
// Then, remove it // Then, remove it
val reason = "test reason" val reason = "test reason"
val deviceId = "id" underTest.removeView(DEFAULT_ID, reason)
underTest.removeView(deviceId, reason)
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
verify(logger).logViewRemoval(deviceId, reason) verify(logger).logViewRemoval(DEFAULT_ID, reason)
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
} }
@Test @Test
fun removeView_noAdd_viewNotRemoved() { fun removeView_noAdd_viewNotRemovedAndListenerNotNotified() {
val listener = registerListener()
underTest.removeView("id", "reason") underTest.removeView("id", "reason")
verify(windowManager, never()).removeView(any()) 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) private fun getState(name: String = "name") = ViewInfo(name)
@@ -1030,9 +1166,17 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
override val windowTitle: String = "Window Title", override val windowTitle: String = "Window Title",
override val wakeReason: String = "WAKE_REASON", override val wakeReason: String = "WAKE_REASON",
override val timeoutMs: Int = TIMEOUT_MS.toInt(), override val timeoutMs: Int = TIMEOUT_MS.toInt(),
override val id: String = "id", override val id: String = DEFAULT_ID,
override val priority: ViewPriority = ViewPriority.NORMAL, override val priority: ViewPriority = ViewPriority.NORMAL,
) : TemporaryViewInfo() ) : 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 TIMEOUT_MS = 10000L
private const val DEFAULT_ID = "defaultId"