Retain media controls longer
When the flag is enabled, apps which have not added support for resumption via MediaBrowserService will still be set to a resume state when the underlying notification (for apps using notification-based actions) or session (for apps using PlaybackState actions) have gone away, if this occurs after they've already timed out. These resume controls will not have a play button, but just open the app's launcher intent when tapped. If the control hadn't timed out yet, it will be removed from the carousel as before. For apps which do implement long-term resumption, they will continue to convert into resume state regardless of whether the active player had already timed out. Test: atest com.android.systemui.media.controls Bug: 264691138 Change-Id: I13a43bfc2eca342000726e696afe77706947d774
This commit is contained in:
@@ -349,6 +349,9 @@ object Flags {
|
||||
val MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE =
|
||||
unreleasedFlag(912, "media_ttt_dismiss_gesture", teamfood = true)
|
||||
|
||||
// TODO(b/266157412): Tracking Bug
|
||||
val MEDIA_RETAIN_SESSIONS = unreleasedFlag(913, "media_retain_sessions")
|
||||
|
||||
// 1000 - dock
|
||||
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
|
||||
|
||||
|
||||
@@ -303,6 +303,7 @@ class MediaDataManager(
|
||||
mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
|
||||
updateState(key, state)
|
||||
}
|
||||
mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) }
|
||||
mediaResumeListener.setManager(this)
|
||||
mediaDataFilter.mediaDataManager = this
|
||||
|
||||
@@ -1289,45 +1290,106 @@ class MediaDataManager(
|
||||
|
||||
fun onNotificationRemoved(key: String) {
|
||||
Assert.isMainThread()
|
||||
val removed = mediaEntries.remove(key)
|
||||
if (useMediaResumption && removed?.resumeAction != null && removed.isLocalSession()) {
|
||||
Log.d(TAG, "Not removing $key because resumable")
|
||||
// Move to resume key (aka package name) if that key doesn't already exist.
|
||||
val resumeAction = getResumeMediaAction(removed.resumeAction!!)
|
||||
val updated =
|
||||
removed.copy(
|
||||
token = null,
|
||||
actions = listOf(resumeAction),
|
||||
semanticActions = MediaButton(playOrPause = resumeAction),
|
||||
actionsToShowInCompact = listOf(0),
|
||||
active = false,
|
||||
resumption = true,
|
||||
isPlaying = false,
|
||||
isClearable = true
|
||||
)
|
||||
val pkg = removed.packageName
|
||||
val migrate = mediaEntries.put(pkg, updated) == null
|
||||
// Notify listeners of "new" controls when migrating or removed and update when not
|
||||
if (migrate) {
|
||||
notifyMediaDataLoaded(pkg, key, updated)
|
||||
} else {
|
||||
// Since packageName is used for the key of the resumption controls, it is
|
||||
// possible that another notification has already been reused for the resumption
|
||||
// controls of this package. In this case, rather than renaming this player as
|
||||
// packageName, just remove it and then send a update to the existing resumption
|
||||
// controls.
|
||||
notifyMediaDataRemoved(key)
|
||||
notifyMediaDataLoaded(pkg, pkg, updated)
|
||||
}
|
||||
logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
|
||||
return
|
||||
}
|
||||
if (removed != null) {
|
||||
val removed = mediaEntries.remove(key) ?: return
|
||||
|
||||
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.
|
||||
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 =
|
||||
data.copy(
|
||||
token = null,
|
||||
actions = actions,
|
||||
semanticActions = MediaButton(playOrPause = resumeAction),
|
||||
actionsToShowInCompact = listOf(0),
|
||||
active = false,
|
||||
resumption = true,
|
||||
isPlaying = false,
|
||||
isClearable = true,
|
||||
clickIntent = launcherIntent,
|
||||
)
|
||||
val pkg = data.packageName
|
||||
val migrate = mediaEntries.put(pkg, updated) == null
|
||||
// Notify listeners of "new" controls when migrating or removed and update when not
|
||||
Log.d(TAG, "migrating? $migrate from $key -> $pkg")
|
||||
if (migrate) {
|
||||
notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated)
|
||||
} else {
|
||||
// Since packageName is used for the key of the resumption controls, it is
|
||||
// possible that another notification has already been reused for the resumption
|
||||
// controls of this package. In this case, rather than renaming this player as
|
||||
// packageName, just remove it and then send a update to the existing resumption
|
||||
// controls.
|
||||
notifyMediaDataRemoved(key)
|
||||
notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated)
|
||||
}
|
||||
logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
|
||||
}
|
||||
|
||||
fun setMediaResumptionEnabled(isEnabled: Boolean) {
|
||||
if (useMediaResumption == isEnabled) {
|
||||
return
|
||||
|
||||
@@ -71,6 +71,12 @@ constructor(
|
||||
*/
|
||||
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 {
|
||||
statusBarStateController.addCallback(
|
||||
object : StatusBarStateController.StateListener {
|
||||
@@ -211,6 +217,7 @@ constructor(
|
||||
} else {
|
||||
// For active controls, if the session is destroyed, clean up everything since we
|
||||
// will need to recreate it if this key is updated later
|
||||
sessionCallback.invoke(key)
|
||||
destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,10 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
|
||||
|
||||
/** Check whether we show explicit indicator on UMO */
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -134,7 +134,8 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
private val clock = FakeSystemClock()
|
||||
@Mock private lateinit var tunerService: TunerService
|
||||
@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>
|
||||
|
||||
private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
|
||||
@@ -184,6 +185,8 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
)
|
||||
verify(tunerService)
|
||||
.addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
|
||||
verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor)
|
||||
verify(mediaTimeoutListener).sessionCallback = capture(sessionCallbackCaptor)
|
||||
session = MediaSession(context, "MediaDataManagerTestSession")
|
||||
mediaNotification =
|
||||
SbnBuilder().run {
|
||||
@@ -230,6 +233,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
|
||||
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
|
||||
whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true)
|
||||
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false)
|
||||
whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
|
||||
}
|
||||
|
||||
@@ -547,6 +551,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
|
||||
|
||||
verify(listener)
|
||||
.onMediaDataLoaded(
|
||||
eq(KEY),
|
||||
@@ -558,9 +563,21 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
)
|
||||
val data = mediaDataCaptor.value
|
||||
assertThat(data.resumption).isFalse()
|
||||
val resumableData = data.copy(resumeAction = Runnable {})
|
||||
mediaDataManager.onMediaDataLoaded(KEY, null, resumableData)
|
||||
mediaDataManager.onMediaDataLoaded(KEY_2, null, resumableData)
|
||||
|
||||
verify(listener)
|
||||
.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)
|
||||
// WHEN the first is removed
|
||||
mediaDataManager.onNotificationRemoved(KEY)
|
||||
@@ -1310,11 +1327,10 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
fun testPlaybackStateChange_keyExists_callsListener() {
|
||||
// Notification has been added
|
||||
addNotificationAndLoad()
|
||||
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
|
||||
|
||||
// Callback gets an updated state
|
||||
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
|
||||
verify(listener)
|
||||
@@ -1332,11 +1348,10 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun testPlaybackStateChange_keyDoesNotExist_doesNothing() {
|
||||
val state = PlaybackState.Builder().build()
|
||||
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
|
||||
|
||||
// No media added with this key
|
||||
|
||||
callbackCaptor.value.invoke(KEY, state)
|
||||
stateCallbackCaptor.value.invoke(KEY, state)
|
||||
verify(listener, never())
|
||||
.onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
|
||||
}
|
||||
@@ -1352,10 +1367,9 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
|
||||
// And then get a state update
|
||||
val state = PlaybackState.Builder().build()
|
||||
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
|
||||
|
||||
// Then no changes are made
|
||||
callbackCaptor.value.invoke(KEY, state)
|
||||
stateCallbackCaptor.value.invoke(KEY, state)
|
||||
verify(listener, never())
|
||||
.onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
|
||||
}
|
||||
@@ -1367,8 +1381,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
whenever(controller.playbackState).thenReturn(state)
|
||||
|
||||
addNotificationAndLoad()
|
||||
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
|
||||
callbackCaptor.value.invoke(KEY, state)
|
||||
stateCallbackCaptor.value.invoke(KEY, state)
|
||||
|
||||
verify(listener)
|
||||
.onMediaDataLoaded(
|
||||
@@ -1410,8 +1423,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
backgroundExecutor.runAllReady()
|
||||
foregroundExecutor.runAllReady()
|
||||
|
||||
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
|
||||
callbackCaptor.value.invoke(PACKAGE_NAME, state)
|
||||
stateCallbackCaptor.value.invoke(PACKAGE_NAME, state)
|
||||
|
||||
verify(listener)
|
||||
.onMediaDataLoaded(
|
||||
@@ -1436,8 +1448,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
.build()
|
||||
|
||||
addNotificationAndLoad()
|
||||
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
|
||||
callbackCaptor.value.invoke(KEY, state)
|
||||
stateCallbackCaptor.value.invoke(KEY, state)
|
||||
|
||||
verify(listener)
|
||||
.onMediaDataLoaded(
|
||||
@@ -1485,6 +1496,177 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
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 */
|
||||
private fun addNotificationAndLoad() {
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
@@ -1500,4 +1682,12 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
private lateinit var executor: FakeExecutor
|
||||
@Mock private lateinit var timeoutCallback: (String, Boolean) -> 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 dozingCallbackCaptor:
|
||||
@@ -99,6 +100,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
)
|
||||
mediaTimeoutListener.timeoutCallback = timeoutCallback
|
||||
mediaTimeoutListener.stateCallback = stateCallback
|
||||
mediaTimeoutListener.sessionCallback = sessionCallback
|
||||
|
||||
// Create a media session and notification for testing.
|
||||
metadataBuilder =
|
||||
@@ -284,6 +286,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
verify(mediaController).unregisterCallback(anyObject())
|
||||
assertThat(executor.numPending()).isEqualTo(0)
|
||||
verify(logger).logSessionDestroyed(eq(KEY))
|
||||
verify(sessionCallback).invoke(eq(KEY))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -322,6 +325,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
// THEN the controller is unregistered, but the timeout is still scheduled
|
||||
verify(mediaController).unregisterCallback(anyObject())
|
||||
assertThat(executor.numPending()).isEqualTo(1)
|
||||
verify(sessionCallback, never()).invoke(eq(KEY))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user