Merge "Retain media controls longer" into tm-qpr-dev am: d2909c6cdd
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21049026 Change-Id: I46ba40a0dbad8d5c1a8127026af5ea615ecbece9 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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,45 +1290,106 @@ 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()) {
|
||||||
// Move to resume key (aka package name) if that key doesn't already exist.
|
convertToResumePlayer(removed)
|
||||||
val resumeAction = getResumeMediaAction(removed.resumeAction!!)
|
} else if (mediaFlags.isRetainingPlayersEnabled()) {
|
||||||
val updated =
|
handlePossibleRemoval(removed, notificationRemoved = true)
|
||||||
removed.copy(
|
} else {
|
||||||
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) {
|
|
||||||
notifyMediaDataRemoved(key)
|
notifyMediaDataRemoved(key)
|
||||||
logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
|
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) {
|
fun setMediaResumptionEnabled(isEnabled: Boolean) {
|
||||||
if (useMediaResumption == isEnabled) {
|
if (useMediaResumption == isEnabled) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user