Merge "Retain media controls longer" into tm-qpr-dev

This commit is contained in:
Beth Thibodeau
2023-01-23 20:05:24 +00:00
committed by Android (Google) Code Review
6 changed files with 322 additions and 50 deletions

View File

@@ -355,6 +355,9 @@ object Flags {
val MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE = val MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE =
unreleasedFlag(912, "media_ttt_dismiss_gesture", teamfood = true) unreleasedFlag(912, "media_ttt_dismiss_gesture", teamfood = true)
// TODO(b/266157412): Tracking Bug
val MEDIA_RETAIN_SESSIONS = unreleasedFlag(913, "media_retain_sessions")
// 1000 - dock // 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")

View File

@@ -303,6 +303,7 @@ class MediaDataManager(
mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
updateState(key, state) updateState(key, state)
} }
mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) }
mediaResumeListener.setManager(this) mediaResumeListener.setManager(this)
mediaDataFilter.mediaDataManager = this mediaDataFilter.mediaDataManager = this
@@ -1289,27 +1290,94 @@ class MediaDataManager(
fun onNotificationRemoved(key: String) { fun onNotificationRemoved(key: String) {
Assert.isMainThread() Assert.isMainThread()
val removed = mediaEntries.remove(key) val removed = mediaEntries.remove(key) ?: return
if (useMediaResumption && removed?.resumeAction != null && removed.isLocalSession()) {
Log.d(TAG, "Not removing $key because resumable") if (useMediaResumption && removed.resumeAction != null && removed.isLocalSession()) {
convertToResumePlayer(removed)
} else if (mediaFlags.isRetainingPlayersEnabled()) {
handlePossibleRemoval(removed, notificationRemoved = true)
} else {
notifyMediaDataRemoved(key)
logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
}
}
private fun onSessionDestroyed(key: String) {
if (!mediaFlags.isRetainingPlayersEnabled()) return
if (DEBUG) Log.d(TAG, "session destroyed for $key")
val entry = mediaEntries.remove(key) ?: return
// Clear token since the session is no longer valid
val updated = entry.copy(token = null)
handlePossibleRemoval(updated)
}
/**
* Convert to resume state if the player is no longer valid and active, then notify listeners
* that the data was updated. Does not convert to resume state if the player is still valid, or
* if it was removed before becoming inactive. (Assumes that [removed] was removed from
* [mediaEntries] before this function was called)
*/
private fun handlePossibleRemoval(removed: MediaData, notificationRemoved: Boolean = false) {
val key = removed.notificationKey!!
val hasSession = removed.token != null
if (hasSession && removed.semanticActions != null) {
// The app was using session actions, and the session is still valid: keep player
if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key")
mediaEntries.put(key, removed)
notifyMediaDataLoaded(key, key, removed)
} else if (!notificationRemoved && removed.semanticActions == null) {
// The app was using notification actions, and notif wasn't removed yet: keep player
if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key")
mediaEntries.put(key, removed)
notifyMediaDataLoaded(key, key, removed)
} else if (removed.active) {
// This player was still active - it didn't last long enough to time out: remove
if (DEBUG) Log.d(TAG, "Removing still-active player $key")
notifyMediaDataRemoved(key)
logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
} else {
// Convert to resume
if (DEBUG) {
Log.d(
TAG,
"Notification ($notificationRemoved) and/or session " +
"($hasSession) gone for inactive player $key"
)
}
convertToResumePlayer(removed)
}
}
/** Set the given [MediaData] as a resume state player and notify listeners */
private fun convertToResumePlayer(data: MediaData) {
val key = data.notificationKey!!
if (DEBUG) Log.d(TAG, "Converting $key to resume")
// Move to resume key (aka package name) if that key doesn't already exist. // Move to resume key (aka package name) if that key doesn't already exist.
val resumeAction = getResumeMediaAction(removed.resumeAction!!) val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) }
val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList()
val launcherIntent =
context.packageManager.getLaunchIntentForPackage(data.packageName)?.let {
PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE)
}
val updated = val updated =
removed.copy( data.copy(
token = null, token = null,
actions = listOf(resumeAction), actions = actions,
semanticActions = MediaButton(playOrPause = resumeAction), semanticActions = MediaButton(playOrPause = resumeAction),
actionsToShowInCompact = listOf(0), actionsToShowInCompact = listOf(0),
active = false, active = false,
resumption = true, resumption = true,
isPlaying = false, isPlaying = false,
isClearable = true isClearable = true,
clickIntent = launcherIntent,
) )
val pkg = removed.packageName val pkg = data.packageName
val migrate = mediaEntries.put(pkg, updated) == null val migrate = mediaEntries.put(pkg, updated) == null
// Notify listeners of "new" controls when migrating or removed and update when not // Notify listeners of "new" controls when migrating or removed and update when not
Log.d(TAG, "migrating? $migrate from $key -> $pkg")
if (migrate) { if (migrate) {
notifyMediaDataLoaded(pkg, key, updated) notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated)
} else { } else {
// Since packageName is used for the key of the resumption controls, it is // Since packageName is used for the key of the resumption controls, it is
// possible that another notification has already been reused for the resumption // possible that another notification has already been reused for the resumption
@@ -1317,15 +1385,9 @@ class MediaDataManager(
// packageName, just remove it and then send a update to the existing resumption // packageName, just remove it and then send a update to the existing resumption
// controls. // controls.
notifyMediaDataRemoved(key) notifyMediaDataRemoved(key)
notifyMediaDataLoaded(pkg, pkg, updated) notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated)
} }
logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId) logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
return
}
if (removed != null) {
notifyMediaDataRemoved(key)
logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
}
} }
fun setMediaResumptionEnabled(isEnabled: Boolean) { fun setMediaResumptionEnabled(isEnabled: Boolean) {

View File

@@ -71,6 +71,12 @@ constructor(
*/ */
lateinit var stateCallback: (String, PlaybackState) -> Unit lateinit var stateCallback: (String, PlaybackState) -> Unit
/**
* Callback representing that the [MediaSession] for an active control has been destroyed
* @param key Media control unique identifier
*/
lateinit var sessionCallback: (String) -> Unit
init { init {
statusBarStateController.addCallback( statusBarStateController.addCallback(
object : StatusBarStateController.StateListener { object : StatusBarStateController.StateListener {
@@ -211,6 +217,7 @@ constructor(
} else { } else {
// For active controls, if the session is destroyed, clean up everything since we // For active controls, if the session is destroyed, clean up everything since we
// will need to recreate it if this key is updated later // will need to recreate it if this key is updated later
sessionCallback.invoke(key)
destroy() destroy()
} }
} }

View File

@@ -45,4 +45,10 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
/** Check whether we show explicit indicator on UMO */ /** Check whether we show explicit indicator on UMO */
fun isExplicitIndicatorEnabled() = featureFlags.isEnabled(Flags.MEDIA_EXPLICIT_INDICATOR) fun isExplicitIndicatorEnabled() = featureFlags.isEnabled(Flags.MEDIA_EXPLICIT_INDICATOR)
/**
* If true, keep active media controls for the lifetime of the MediaSession, regardless of
* whether the underlying notification was dismissed
*/
fun isRetainingPlayersEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_SESSIONS)
} }

View File

@@ -134,7 +134,8 @@ class MediaDataManagerTest : SysuiTestCase() {
private val clock = FakeSystemClock() private val clock = FakeSystemClock()
@Mock private lateinit var tunerService: TunerService @Mock private lateinit var tunerService: TunerService
@Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable> @Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
@Captor lateinit var callbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit> @Captor lateinit var stateCallbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
@Captor lateinit var sessionCallbackCaptor: ArgumentCaptor<(String) -> Unit>
@Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig> @Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig>
private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20) private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
@@ -184,6 +185,8 @@ class MediaDataManagerTest : SysuiTestCase() {
) )
verify(tunerService) verify(tunerService)
.addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)) .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor)
verify(mediaTimeoutListener).sessionCallback = capture(sessionCallbackCaptor)
session = MediaSession(context, "MediaDataManagerTestSession") session = MediaSession(context, "MediaDataManagerTestSession")
mediaNotification = mediaNotification =
SbnBuilder().run { SbnBuilder().run {
@@ -230,6 +233,7 @@ class MediaDataManagerTest : SysuiTestCase() {
whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L) whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false) whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true) whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true)
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false)
whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
} }
@@ -547,6 +551,7 @@ class MediaDataManagerTest : SysuiTestCase() {
mediaDataManager.onNotificationAdded(KEY_2, mediaNotification) mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(2) assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(2) assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
verify(listener) verify(listener)
.onMediaDataLoaded( .onMediaDataLoaded(
eq(KEY), eq(KEY),
@@ -558,9 +563,21 @@ class MediaDataManagerTest : SysuiTestCase() {
) )
val data = mediaDataCaptor.value val data = mediaDataCaptor.value
assertThat(data.resumption).isFalse() assertThat(data.resumption).isFalse()
val resumableData = data.copy(resumeAction = Runnable {})
mediaDataManager.onMediaDataLoaded(KEY, null, resumableData) verify(listener)
mediaDataManager.onMediaDataLoaded(KEY_2, null, resumableData) .onMediaDataLoaded(
eq(KEY_2),
eq(null),
capture(mediaDataCaptor),
eq(true),
eq(0),
eq(false)
)
val data2 = mediaDataCaptor.value
assertThat(data2.resumption).isFalse()
mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
mediaDataManager.onMediaDataLoaded(KEY_2, null, data2.copy(resumeAction = Runnable {}))
reset(listener) reset(listener)
// WHEN the first is removed // WHEN the first is removed
mediaDataManager.onNotificationRemoved(KEY) mediaDataManager.onNotificationRemoved(KEY)
@@ -1310,11 +1327,10 @@ class MediaDataManagerTest : SysuiTestCase() {
fun testPlaybackStateChange_keyExists_callsListener() { fun testPlaybackStateChange_keyExists_callsListener() {
// Notification has been added // Notification has been added
addNotificationAndLoad() addNotificationAndLoad()
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
// Callback gets an updated state // Callback gets an updated state
val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
callbackCaptor.value.invoke(KEY, state) stateCallbackCaptor.value.invoke(KEY, state)
// Listener is notified of updated state // Listener is notified of updated state
verify(listener) verify(listener)
@@ -1332,11 +1348,10 @@ class MediaDataManagerTest : SysuiTestCase() {
@Test @Test
fun testPlaybackStateChange_keyDoesNotExist_doesNothing() { fun testPlaybackStateChange_keyDoesNotExist_doesNothing() {
val state = PlaybackState.Builder().build() val state = PlaybackState.Builder().build()
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
// No media added with this key // No media added with this key
callbackCaptor.value.invoke(KEY, state) stateCallbackCaptor.value.invoke(KEY, state)
verify(listener, never()) verify(listener, never())
.onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
} }
@@ -1352,10 +1367,9 @@ class MediaDataManagerTest : SysuiTestCase() {
// And then get a state update // And then get a state update
val state = PlaybackState.Builder().build() val state = PlaybackState.Builder().build()
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
// Then no changes are made // Then no changes are made
callbackCaptor.value.invoke(KEY, state) stateCallbackCaptor.value.invoke(KEY, state)
verify(listener, never()) verify(listener, never())
.onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
} }
@@ -1367,8 +1381,7 @@ class MediaDataManagerTest : SysuiTestCase() {
whenever(controller.playbackState).thenReturn(state) whenever(controller.playbackState).thenReturn(state)
addNotificationAndLoad() addNotificationAndLoad()
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) stateCallbackCaptor.value.invoke(KEY, state)
callbackCaptor.value.invoke(KEY, state)
verify(listener) verify(listener)
.onMediaDataLoaded( .onMediaDataLoaded(
@@ -1410,8 +1423,7 @@ class MediaDataManagerTest : SysuiTestCase() {
backgroundExecutor.runAllReady() backgroundExecutor.runAllReady()
foregroundExecutor.runAllReady() foregroundExecutor.runAllReady()
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) stateCallbackCaptor.value.invoke(PACKAGE_NAME, state)
callbackCaptor.value.invoke(PACKAGE_NAME, state)
verify(listener) verify(listener)
.onMediaDataLoaded( .onMediaDataLoaded(
@@ -1436,8 +1448,7 @@ class MediaDataManagerTest : SysuiTestCase() {
.build() .build()
addNotificationAndLoad() addNotificationAndLoad()
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) stateCallbackCaptor.value.invoke(KEY, state)
callbackCaptor.value.invoke(KEY, state)
verify(listener) verify(listener)
.onMediaDataLoaded( .onMediaDataLoaded(
@@ -1485,6 +1496,177 @@ class MediaDataManagerTest : SysuiTestCase() {
assertThat(mediaDataCaptor.value.isClearable).isFalse() assertThat(mediaDataCaptor.value.isClearable).isFalse()
} }
@Test
fun testRetain_notifPlayer_notifRemoved_setToResume() {
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
// When a media control based on notification is added, times out, and then removed
addNotificationAndLoad()
mediaDataManager.setTimedOut(KEY, timedOut = true)
assertThat(mediaDataCaptor.value.active).isFalse()
mediaDataManager.onNotificationRemoved(KEY)
// It is converted to a resume player
verify(listener)
.onMediaDataLoaded(
eq(PACKAGE_NAME),
eq(KEY),
capture(mediaDataCaptor),
eq(true),
eq(0),
eq(false)
)
assertThat(mediaDataCaptor.value.resumption).isTrue()
assertThat(mediaDataCaptor.value.active).isFalse()
verify(logger)
.logActiveConvertedToResume(
anyInt(),
eq(PACKAGE_NAME),
eq(mediaDataCaptor.value.instanceId)
)
}
@Test
fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() {
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
// When a media control based on notification is added and times out
addNotificationAndLoad()
mediaDataManager.setTimedOut(KEY, timedOut = true)
assertThat(mediaDataCaptor.value.active).isFalse()
// and then the session is destroyed
sessionCallbackCaptor.value.invoke(KEY)
// It remains as a regular player
verify(listener, never()).onMediaDataRemoved(eq(KEY))
verify(listener, never())
.onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
}
@Test
fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() {
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
// When a media control based on notification is added and then removed, without timing out
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
mediaDataManager.onNotificationRemoved(KEY)
// It is fully removed
verify(listener).onMediaDataRemoved(eq(KEY))
verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
verify(listener, never())
.onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
}
@Test
fun testRetain_canResume_removeWhileActive_setToResume() {
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
// When a media control that supports resumption is added
addNotificationAndLoad()
val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {})
mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable)
// And then removed while still active
mediaDataManager.onNotificationRemoved(KEY)
// It is converted to a resume player
verify(listener)
.onMediaDataLoaded(
eq(PACKAGE_NAME),
eq(KEY),
capture(mediaDataCaptor),
eq(true),
eq(0),
eq(false)
)
assertThat(mediaDataCaptor.value.resumption).isTrue()
assertThat(mediaDataCaptor.value.active).isFalse()
verify(logger)
.logActiveConvertedToResume(
anyInt(),
eq(PACKAGE_NAME),
eq(mediaDataCaptor.value.instanceId)
)
}
@Test
fun testRetain_sessionPlayer_notifRemoved_doesNotChange() {
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
addPlaybackStateAction()
// When a media control with PlaybackState actions is added, times out,
// and then the notification is removed
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
mediaDataManager.setTimedOut(KEY, timedOut = true)
mediaDataManager.onNotificationRemoved(KEY)
// It remains as a regular player
verify(listener, never()).onMediaDataRemoved(eq(KEY))
verify(listener, never())
.onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
}
@Test
fun testRetain_sessionPlayer_sessionDestroyed_setToResume() {
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
addPlaybackStateAction()
// When a media control with PlaybackState actions is added, times out,
// and then the session is destroyed
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
mediaDataManager.setTimedOut(KEY, timedOut = true)
sessionCallbackCaptor.value.invoke(KEY)
// It is converted to a resume player
verify(listener)
.onMediaDataLoaded(
eq(PACKAGE_NAME),
eq(KEY),
capture(mediaDataCaptor),
eq(true),
eq(0),
eq(false)
)
assertThat(mediaDataCaptor.value.resumption).isTrue()
assertThat(mediaDataCaptor.value.active).isFalse()
verify(logger)
.logActiveConvertedToResume(
anyInt(),
eq(PACKAGE_NAME),
eq(mediaDataCaptor.value.instanceId)
)
}
@Test
fun testRetain_sessionPlayer_destroyedWhileActive_fullyRemoved() {
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
addPlaybackStateAction()
// When a media control using session actions is added, and then the session is destroyed
// without timing out first
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
sessionCallbackCaptor.value.invoke(KEY)
// It is fully removed
verify(listener).onMediaDataRemoved(eq(KEY))
verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
verify(listener, never())
.onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
}
/** Helper function to add a media notification and capture the resulting MediaData */ /** Helper function to add a media notification and capture the resulting MediaData */
private fun addNotificationAndLoad() { private fun addNotificationAndLoad() {
mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onNotificationAdded(KEY, mediaNotification)
@@ -1500,4 +1682,12 @@ class MediaDataManagerTest : SysuiTestCase() {
eq(false) eq(false)
) )
} }
/** Helper function to set up a PlaybackState with action */
private fun addPlaybackStateAction() {
val stateActions = PlaybackState.ACTION_PLAY_PAUSE
val stateBuilder = PlaybackState.Builder().setActions(stateActions)
stateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 1.0f)
whenever(controller.playbackState).thenReturn(stateBuilder.build())
}
} }

View File

@@ -72,6 +72,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
private lateinit var executor: FakeExecutor private lateinit var executor: FakeExecutor
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
@Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
@Mock private lateinit var sessionCallback: (String) -> Unit
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback> @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
@Captor @Captor
private lateinit var dozingCallbackCaptor: private lateinit var dozingCallbackCaptor:
@@ -99,6 +100,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
) )
mediaTimeoutListener.timeoutCallback = timeoutCallback mediaTimeoutListener.timeoutCallback = timeoutCallback
mediaTimeoutListener.stateCallback = stateCallback mediaTimeoutListener.stateCallback = stateCallback
mediaTimeoutListener.sessionCallback = sessionCallback
// Create a media session and notification for testing. // Create a media session and notification for testing.
metadataBuilder = metadataBuilder =
@@ -284,6 +286,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
verify(mediaController).unregisterCallback(anyObject()) verify(mediaController).unregisterCallback(anyObject())
assertThat(executor.numPending()).isEqualTo(0) assertThat(executor.numPending()).isEqualTo(0)
verify(logger).logSessionDestroyed(eq(KEY)) verify(logger).logSessionDestroyed(eq(KEY))
verify(sessionCallback).invoke(eq(KEY))
} }
@Test @Test
@@ -322,6 +325,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// THEN the controller is unregistered, but the timeout is still scheduled // THEN the controller is unregistered, but the timeout is still scheduled
verify(mediaController).unregisterCallback(anyObject()) verify(mediaController).unregisterCallback(anyObject())
assertThat(executor.numPending()).isEqualTo(1) assertThat(executor.numPending()).isEqualTo(1)
verify(sessionCallback, never()).invoke(eq(KEY))
} }
@Test @Test