Merge "Retain media controls longer" into tm-qpr-dev am: d2909c6cdd am: ba6f9d2424
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21049026 Change-Id: I310b08af6b0fe8b20ee09680e579ad940609ba69 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -354,6 +354,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")
|
||||
|
||||
|
||||
@@ -304,6 +304,7 @@ class MediaDataManager(
|
||||
mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
|
||||
updateState(key, state)
|
||||
}
|
||||
mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) }
|
||||
mediaResumeListener.setManager(this)
|
||||
mediaDataFilter.mediaDataManager = this
|
||||
|
||||
@@ -1292,45 +1293,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