Add flag to allow remote sessions to be resumable
If the flag is enabled, media which is location PLAYBACK_CAST_LOCAL will be allowed to persist in the resume state (under the same conditions as local media controls). RCNs will still not be allowed to resume. Flag: MEDIA_REMOTE_RESUME Test: atest MediaDataManagerTest MediaResumeListenerTest Bug: 269637129 Bug: 270437894 Change-Id: Idd0313c0c49f768453e781853edf7f818d6b8ab0
This commit is contained in:
@@ -367,6 +367,9 @@ object Flags {
|
||||
// TODO(b/267166152) : Tracking Bug
|
||||
val MEDIA_RETAIN_RECOMMENDATIONS = unreleasedFlag(916, "media_retain_recommendations")
|
||||
|
||||
// TODO(b/270437894): Tracking Bug
|
||||
val MEDIA_REMOTE_RESUME = unreleasedFlag(917, "media_remote_resume")
|
||||
|
||||
// 1000 - dock
|
||||
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
|
||||
|
||||
|
||||
@@ -1339,10 +1339,13 @@ class MediaDataManager(
|
||||
fun onNotificationRemoved(key: String) {
|
||||
Assert.isMainThread()
|
||||
val removed = mediaEntries.remove(key) ?: return
|
||||
|
||||
val isEligibleForResume =
|
||||
removed.isLocalSession() ||
|
||||
(mediaFlags.isRemoteResumeAllowed() &&
|
||||
removed.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE)
|
||||
if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) {
|
||||
logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
|
||||
} else if (useMediaResumption && removed.resumeAction != null && removed.isLocalSession()) {
|
||||
} else if (useMediaResumption && removed.resumeAction != null && isEligibleForResume) {
|
||||
convertToResumePlayer(key, removed)
|
||||
} else if (mediaFlags.isRetainingPlayersEnabled()) {
|
||||
handlePossibleRemoval(key, removed, notificationRemoved = true)
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.android.systemui.dump.DumpManager
|
||||
import com.android.systemui.media.controls.models.player.MediaData
|
||||
import com.android.systemui.media.controls.pipeline.MediaDataManager
|
||||
import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT
|
||||
import com.android.systemui.media.controls.util.MediaFlags
|
||||
import com.android.systemui.settings.UserTracker
|
||||
import com.android.systemui.tuner.TunerService
|
||||
import com.android.systemui.util.Utils
|
||||
@@ -63,7 +64,8 @@ constructor(
|
||||
private val tunerService: TunerService,
|
||||
private val mediaBrowserFactory: ResumeMediaBrowserFactory,
|
||||
dumpManager: DumpManager,
|
||||
private val systemClock: SystemClock
|
||||
private val systemClock: SystemClock,
|
||||
private val mediaFlags: MediaFlags,
|
||||
) : MediaDataManager.Listener, Dumpable {
|
||||
|
||||
private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
|
||||
@@ -231,7 +233,11 @@ constructor(
|
||||
mediaBrowser = null
|
||||
}
|
||||
// If we don't have a resume action, check if we haven't already
|
||||
if (data.resumeAction == null && !data.hasCheckedForResume && data.isLocalSession()) {
|
||||
val isEligibleForResume =
|
||||
data.isLocalSession() ||
|
||||
(mediaFlags.isRemoteResumeAllowed() &&
|
||||
data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE)
|
||||
if (data.resumeAction == null && !data.hasCheckedForResume && isEligibleForResume) {
|
||||
// TODO also check for a media button receiver intended for restarting (b/154127084)
|
||||
Log.d(TAG, "Checking for service component for " + data.packageName)
|
||||
val pm = context.packageManager
|
||||
|
||||
@@ -61,4 +61,7 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
|
||||
|
||||
/** If true, do not automatically dismiss the recommendation card */
|
||||
fun isPersistentSsCardEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_RECOMMENDATIONS)
|
||||
|
||||
/** Check whether we allow remote media to generate resume controls */
|
||||
fun isRemoteResumeAllowed() = featureFlags.isEnabled(Flags.MEDIA_REMOTE_RESUME)
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
@Mock private lateinit var logger: MediaUiEventLogger
|
||||
lateinit var mediaDataManager: MediaDataManager
|
||||
lateinit var mediaNotification: StatusBarNotification
|
||||
lateinit var remoteCastNotification: StatusBarNotification
|
||||
@Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
|
||||
private val clock = FakeSystemClock()
|
||||
@Mock private lateinit var tunerService: TunerService
|
||||
@@ -205,6 +206,20 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
}
|
||||
build()
|
||||
}
|
||||
remoteCastNotification =
|
||||
SbnBuilder().run {
|
||||
setPkg(SYSTEM_PACKAGE_NAME)
|
||||
modifyNotification(context).also {
|
||||
it.setSmallIcon(android.R.drawable.ic_media_pause)
|
||||
it.setStyle(
|
||||
MediaStyle().apply {
|
||||
setMediaSession(session.sessionToken)
|
||||
setRemotePlaybackInfo("Remote device", 0, null)
|
||||
}
|
||||
)
|
||||
}
|
||||
build()
|
||||
}
|
||||
metadataBuilder =
|
||||
MediaMetadata.Builder().apply {
|
||||
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
|
||||
@@ -244,6 +259,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true)
|
||||
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false)
|
||||
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
|
||||
whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(false)
|
||||
whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
|
||||
whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false)
|
||||
}
|
||||
@@ -404,33 +420,8 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun testOnNotificationAdded_isRcn_markedRemote() {
|
||||
val rcn =
|
||||
SbnBuilder().run {
|
||||
setPkg(SYSTEM_PACKAGE_NAME)
|
||||
modifyNotification(context).also {
|
||||
it.setSmallIcon(android.R.drawable.ic_media_pause)
|
||||
it.setStyle(
|
||||
MediaStyle().apply {
|
||||
setMediaSession(session.sessionToken)
|
||||
setRemotePlaybackInfo("Remote device", 0, null)
|
||||
}
|
||||
)
|
||||
}
|
||||
build()
|
||||
}
|
||||
addNotificationAndLoad(remoteCastNotification)
|
||||
|
||||
mediaDataManager.onNotificationAdded(KEY, rcn)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(listener)
|
||||
.onMediaDataLoaded(
|
||||
eq(KEY),
|
||||
eq(null),
|
||||
capture(mediaDataCaptor),
|
||||
eq(true),
|
||||
eq(0),
|
||||
eq(false)
|
||||
)
|
||||
assertThat(mediaDataCaptor.value!!.playbackLocation)
|
||||
.isEqualTo(MediaData.PLAYBACK_CAST_REMOTE)
|
||||
verify(logger)
|
||||
@@ -646,6 +637,56 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
verify(listener).onMediaDataRemoved(eq(KEY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnNotificationRemoved_withResumption_isRemoteAndRemoteAllowed() {
|
||||
// With the flag enabled to allow remote media to resume
|
||||
whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true)
|
||||
|
||||
// GIVEN that the manager has a notification with a resume action, but is not local
|
||||
whenever(controller.metadata).thenReturn(metadataBuilder.build())
|
||||
whenever(playbackInfo.playbackType)
|
||||
.thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
|
||||
addNotificationAndLoad()
|
||||
val data = mediaDataCaptor.value
|
||||
val dataRemoteWithResume =
|
||||
data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
|
||||
mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
|
||||
|
||||
// WHEN the notification is removed
|
||||
mediaDataManager.onNotificationRemoved(KEY)
|
||||
|
||||
// THEN the media data is converted to a resume state
|
||||
verify(listener)
|
||||
.onMediaDataLoaded(
|
||||
eq(PACKAGE_NAME),
|
||||
eq(KEY),
|
||||
capture(mediaDataCaptor),
|
||||
eq(true),
|
||||
eq(0),
|
||||
eq(false)
|
||||
)
|
||||
assertThat(mediaDataCaptor.value.resumption).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnNotificationRemoved_withResumption_isRcnAndRemoteAllowed() {
|
||||
// With the flag enabled to allow remote media to resume
|
||||
whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true)
|
||||
|
||||
// GIVEN that the manager has a remote cast notification
|
||||
addNotificationAndLoad(remoteCastNotification)
|
||||
val data = mediaDataCaptor.value
|
||||
assertThat(data.playbackLocation).isEqualTo(MediaData.PLAYBACK_CAST_REMOTE)
|
||||
val dataRemoteWithResume = data.copy(resumeAction = Runnable {})
|
||||
mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
|
||||
|
||||
// WHEN the RCN is removed
|
||||
mediaDataManager.onNotificationRemoved(KEY)
|
||||
|
||||
// THEN the media data is removed
|
||||
verify(listener).onMediaDataRemoved(eq(KEY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnNotificationRemoved_withResumption_tooManyPlayers() {
|
||||
// Given the maximum number of resume controls already
|
||||
@@ -1526,22 +1567,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
)
|
||||
|
||||
// update to remote cast
|
||||
val rcn =
|
||||
SbnBuilder().run {
|
||||
setPkg(SYSTEM_PACKAGE_NAME) // System package
|
||||
modifyNotification(context).also {
|
||||
it.setSmallIcon(android.R.drawable.ic_media_pause)
|
||||
it.setStyle(
|
||||
MediaStyle().apply {
|
||||
setMediaSession(session.sessionToken)
|
||||
setRemotePlaybackInfo("Remote device", 0, null)
|
||||
}
|
||||
)
|
||||
}
|
||||
build()
|
||||
}
|
||||
|
||||
mediaDataManager.onNotificationAdded(KEY, rcn)
|
||||
mediaDataManager.onNotificationAdded(KEY, remoteCastNotification)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(logger)
|
||||
@@ -1911,9 +1937,14 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
verify(listener).onMediaDataRemoved(eq(KEY))
|
||||
}
|
||||
|
||||
/** Helper function to add a media notification and capture the resulting MediaData */
|
||||
/** Helper function to add a basic media notification and capture the resulting MediaData */
|
||||
private fun addNotificationAndLoad() {
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
addNotificationAndLoad(mediaNotification)
|
||||
}
|
||||
|
||||
/** Helper function to add the given notification and capture the resulting MediaData */
|
||||
private fun addNotificationAndLoad(sbn: StatusBarNotification) {
|
||||
mediaDataManager.onNotificationAdded(KEY, sbn)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
verify(listener)
|
||||
|
||||
@@ -38,6 +38,7 @@ import com.android.systemui.media.controls.models.player.MediaData
|
||||
import com.android.systemui.media.controls.models.player.MediaDeviceData
|
||||
import com.android.systemui.media.controls.pipeline.MediaDataManager
|
||||
import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT
|
||||
import com.android.systemui.media.controls.util.MediaFlags
|
||||
import com.android.systemui.settings.UserTracker
|
||||
import com.android.systemui.tuner.TunerService
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
@@ -92,6 +93,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
@Mock private lateinit var mockContext: Context
|
||||
@Mock private lateinit var pendingIntent: PendingIntent
|
||||
@Mock private lateinit var dumpManager: DumpManager
|
||||
@Mock private lateinit var mediaFlags: MediaFlags
|
||||
|
||||
@Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
|
||||
@Captor lateinit var actionCaptor: ArgumentCaptor<Runnable>
|
||||
@@ -134,6 +136,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
whenever(mockContext.packageManager).thenReturn(context.packageManager)
|
||||
whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
|
||||
whenever(mockContext.userId).thenReturn(context.userId)
|
||||
whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(false)
|
||||
|
||||
executor = FakeExecutor(clock)
|
||||
resumeListener =
|
||||
@@ -146,7 +149,8 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
tunerService,
|
||||
resumeBrowserFactory,
|
||||
dumpManager,
|
||||
clock
|
||||
clock,
|
||||
mediaFlags,
|
||||
)
|
||||
resumeListener.setManager(mediaDataManager)
|
||||
mediaDataManager.addListener(resumeListener)
|
||||
@@ -188,7 +192,8 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
tunerService,
|
||||
resumeBrowserFactory,
|
||||
dumpManager,
|
||||
clock
|
||||
clock,
|
||||
mediaFlags,
|
||||
)
|
||||
listener.setManager(mediaDataManager)
|
||||
verify(broadcastDispatcher, never())
|
||||
@@ -243,6 +248,32 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
verify(mediaDataManager, never()).setResumeAction(any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnLoad_localCast_remoteResumeAllowed_doesCheck() {
|
||||
// If local cast media is allowed to resume
|
||||
whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true)
|
||||
|
||||
// When media data is loaded that has not been checked yet, and is a local cast
|
||||
val dataCast = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
|
||||
resumeListener.onMediaDataLoaded(KEY, null, dataCast)
|
||||
|
||||
// Then we report back to the manager
|
||||
verify(mediaDataManager).setResumeAction(KEY, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnLoad_remoteCast_remoteResumeAllowed_doesCheck() {
|
||||
// If local cast media is allowed to resume
|
||||
whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true)
|
||||
|
||||
// When media data is loaded that has not been checked yet, and is a remote cast
|
||||
val dataRcn = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_REMOTE)
|
||||
resumeListener.onMediaDataLoaded(KEY, null, dataRcn)
|
||||
|
||||
// Then we do not take action
|
||||
verify(mediaDataManager, never()).setResumeAction(any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnLoad_checksForResume_hasService() {
|
||||
setUpMbsWithValidResolveInfo()
|
||||
@@ -389,7 +420,8 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
tunerService,
|
||||
resumeBrowserFactory,
|
||||
dumpManager,
|
||||
clock
|
||||
clock,
|
||||
mediaFlags,
|
||||
)
|
||||
resumeListener.setManager(mediaDataManager)
|
||||
mediaDataManager.addListener(resumeListener)
|
||||
@@ -421,7 +453,8 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
tunerService,
|
||||
resumeBrowserFactory,
|
||||
dumpManager,
|
||||
clock
|
||||
clock,
|
||||
mediaFlags,
|
||||
)
|
||||
resumeListener.setManager(mediaDataManager)
|
||||
mediaDataManager.addListener(resumeListener)
|
||||
@@ -463,7 +496,8 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
tunerService,
|
||||
resumeBrowserFactory,
|
||||
dumpManager,
|
||||
clock
|
||||
clock,
|
||||
mediaFlags,
|
||||
)
|
||||
resumeListener.setManager(mediaDataManager)
|
||||
mediaDataManager.addListener(resumeListener)
|
||||
|
||||
Reference in New Issue
Block a user