Merge "Sort remote cast notifications to the end" into sc-v2-dev am: ed5ced3fdd

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/16274070

Change-Id: I3fa04797eb93d00922220fb9832e24408501ece3
This commit is contained in:
Beth Thibodeau
2021-11-19 16:23:33 +00:00
committed by Automerger Merge Worker
9 changed files with 137 additions and 43 deletions

View File

@@ -833,12 +833,15 @@ internal object MediaPlayerData {
) )
private val comparator = private val comparator =
compareByDescending<MediaSortKey> { it.data.isPlaying == true && it.data.isLocalSession } compareByDescending<MediaSortKey> { it.data.isPlaying == true &&
.thenByDescending { it.data.isPlaying } it.data.playbackLocation == MediaData.PLAYBACK_LOCAL }
.thenByDescending { if (shouldPrioritizeSs) it.isSsMediaRec else !it.isSsMediaRec } .thenByDescending { it.data.isPlaying == true &&
.thenByDescending { !it.data.resumption } it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL }
.thenByDescending { it.updateTime } .thenByDescending { if (shouldPrioritizeSs) it.isSsMediaRec else !it.isSsMediaRec }
.thenByDescending { !it.data.isLocalSession } .thenByDescending { !it.data.resumption }
.thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
.thenByDescending { it.updateTime }
.thenByDescending { it.data.notificationKey }
private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()

View File

@@ -82,9 +82,9 @@ data class MediaData(
*/ */
var resumeAction: Runnable?, var resumeAction: Runnable?,
/** /**
* Local or remote playback * Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE
*/ */
var isLocalSession: Boolean = true, var playbackLocation: Int = PLAYBACK_LOCAL,
/** /**
* Indicates that this player is a resumption player (ie. It only shows a play actions which * Indicates that this player is a resumption player (ie. It only shows a play actions which
* will start the app and start playing). * will start the app and start playing).
@@ -110,7 +110,20 @@ data class MediaData(
* Timestamp when this player was last active. * Timestamp when this player was last active.
*/ */
var lastActive: Long = 0L var lastActive: Long = 0L
) ) {
companion object {
/** Media is playing on the local device */
const val PLAYBACK_LOCAL = 0
/** Media is cast but originated on the local device */
const val PLAYBACK_CAST_LOCAL = 1
/** Media is from a remote cast notification */
const val PLAYBACK_CAST_REMOTE = 2
}
fun isLocalSession(): Boolean {
return playbackLocation == PLAYBACK_LOCAL
}
}
/** State of a media action. */ /** State of a media action. */
data class MediaAction( data class MediaAction(

View File

@@ -27,6 +27,8 @@ import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.ImageDecoder import android.graphics.ImageDecoder
@@ -145,6 +147,24 @@ class MediaDataManager(
private var smartspaceSession: SmartspaceSession? = null private var smartspaceSession: SmartspaceSession? = null
private var allowMediaRecommendations = Utils.allowMediaRecommendations(context) private var allowMediaRecommendations = Utils.allowMediaRecommendations(context)
/**
* Check whether this notification is an RCN
* TODO(b/204910409) implement new API for explicitly declaring this
*/
private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
val pm = context.packageManager
try {
val info = pm.getApplicationInfo(sbn.packageName, PackageManager.MATCH_SYSTEM_ONLY)
if (info.privateFlags and ApplicationInfo.PRIVATE_FLAG_PRIVILEGED != 0) {
val extras = sbn.notification.extras
if (extras.containsKey(Notification.EXTRA_SUBSTITUTE_APP_NAME)) {
return true
}
}
} catch (e: PackageManager.NameNotFoundException) { }
return false
}
@Inject @Inject
constructor( constructor(
context: Context, context: Context,
@@ -434,7 +454,7 @@ class MediaDataManager(
val existed = mediaEntries[key] != null val existed = mediaEntries[key] != null
backgroundExecutor.execute { backgroundExecutor.execute {
mediaEntries[key]?.let { mediaData -> mediaEntries[key]?.let { mediaData ->
if (mediaData.isLocalSession) { if (mediaData.isLocalSession()) {
mediaData.token?.let { mediaData.token?.let {
val mediaController = mediaControllerFactory.create(it) val mediaController = mediaControllerFactory.create(it)
mediaController.transportControls.stop() mediaController.transportControls.stop()
@@ -618,8 +638,11 @@ class MediaDataManager(
} }
} }
val isLocalSession = mediaController.playbackInfo?.playbackType == val playbackLocation =
MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
else if (mediaController.playbackInfo?.playbackType ==
MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) MediaData.PLAYBACK_LOCAL
else MediaData.PLAYBACK_CAST_LOCAL
val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
val lastActive = systemClock.elapsedRealtime() val lastActive = systemClock.elapsedRealtime()
foregroundExecutor.execute { foregroundExecutor.execute {
@@ -629,7 +652,7 @@ class MediaDataManager(
onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app, onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app,
smallIcon, artist, song, artWorkIcon, actionIcons, smallIcon, artist, song, artWorkIcon, actionIcons,
actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null, actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null,
active, resumeAction = resumeAction, isLocalSession = isLocalSession, active, resumeAction = resumeAction, playbackLocation = playbackLocation,
notificationKey = key, hasCheckedForResume = hasCheckedForResume, notificationKey = key, hasCheckedForResume = hasCheckedForResume,
isPlaying = isPlaying, isClearable = sbn.isClearable(), isPlaying = isPlaying, isClearable = sbn.isClearable(),
lastActive = lastActive)) lastActive = lastActive))
@@ -754,7 +777,7 @@ class MediaDataManager(
fun onNotificationRemoved(key: String) { fun onNotificationRemoved(key: String) {
Assert.isMainThread() Assert.isMainThread()
val removed = mediaEntries.remove(key) val removed = mediaEntries.remove(key)
if (useMediaResumption && removed?.resumeAction != null && removed?.isLocalSession) { if (useMediaResumption && removed?.resumeAction != null && removed?.isLocalSession()) {
Log.d(TAG, "Not removing $key because resumable") Log.d(TAG, "Not removing $key because resumable")
// Move to resume key (aka package name) if that key doesn't already exist. // Move to resume key (aka package name) if that key doesn't already exist.
val resumeAction = getResumeMediaAction(removed.resumeAction!!) val resumeAction = getResumeMediaAction(removed.resumeAction!!)

View File

@@ -193,7 +193,7 @@ class MediaResumeListener @Inject constructor(
mediaBrowser = null mediaBrowser = null
} }
// If we don't have a resume action, check if we haven't already // If we don't have a resume action, check if we haven't already
if (data.resumeAction == null && !data.hasCheckedForResume && data.isLocalSession) { if (data.resumeAction == null && !data.hasCheckedForResume && data.isLocalSession()) {
// TODO also check for a media button receiver intended for restarting (b/154127084) // TODO also check for a media button receiver intended for restarting (b/154127084)
Log.d(TAG, "Checking for service component for " + data.packageName) Log.d(TAG, "Checking for service component for " + data.packageName)
val pm = context.packageManager val pm = context.packageManager

View File

@@ -104,37 +104,54 @@ class MediaCarouselControllerTest : SysuiTestCase() {
fun testPlayerOrdering() { fun testPlayerOrdering() {
// Test values: key, data, last active time // Test values: key, data, last active time
val playingLocal = Triple("playing local", val playingLocal = Triple("playing local",
DATA.copy(active = true, isPlaying = true, isLocalSession = true, resumption = false), DATA.copy(active = true, isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false),
4500L) 4500L)
val playingRemote = Triple("playing remote", val playingCast = Triple("playing cast",
DATA.copy(active = true, isPlaying = true, isLocalSession = false, resumption = false), DATA.copy(active = true, isPlaying = true,
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false),
5000L) 5000L)
val pausedLocal = Triple("paused local", val pausedLocal = Triple("paused local",
DATA.copy(active = true, isPlaying = false, isLocalSession = true, resumption = false), DATA.copy(active = true, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false),
1000L) 1000L)
val pausedRemote = Triple("paused remote", val pausedCast = Triple("paused cast",
DATA.copy(active = true, isPlaying = false, isLocalSession = false, resumption = false), DATA.copy(active = true, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false),
2000L) 2000L)
val playingRcn = Triple("playing RCN",
DATA.copy(active = true, isPlaying = true,
playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false),
5000L)
val pausedRcn = Triple("paused RCN",
DATA.copy(active = true, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false),
5000L)
val resume1 = Triple("resume 1", val resume1 = Triple("resume 1",
DATA.copy(active = false, isPlaying = false, isLocalSession = true, resumption = true), DATA.copy(active = false, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
500L) 500L)
val resume2 = Triple("resume 2", val resume2 = Triple("resume 2",
DATA.copy(active = false, isPlaying = false, isLocalSession = true, resumption = true), DATA.copy(active = false, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
1000L) 1000L)
// Expected ordering for media players: // Expected ordering for media players:
// Actively playing local sessions // Actively playing local sessions
// Actively playing remote sessions // Actively playing cast sessions
// Paused sessions, by last active // Paused local and cast sessions, by last active
// RCNs
// Resume controls, by last active // Resume controls, by last active
val expected = listOf(playingLocal, playingRemote, pausedRemote, pausedLocal, resume2, val expected = listOf(playingLocal, playingCast, pausedCast, pausedLocal, playingRcn,
resume1) pausedRcn, resume2, resume1)
expected.forEach { expected.forEach {
clock.setCurrentTimeMillis(it.third) clock.setCurrentTimeMillis(it.third)

View File

@@ -74,8 +74,8 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
mManager.addListener(mListener); mManager.addListener(mListener);
mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null,
new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, true, new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null,
false, KEY, false, false, false, 0L); MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L);
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
} }

View File

@@ -1,5 +1,6 @@
package com.android.systemui.media package com.android.systemui.media
import android.app.Notification
import android.app.Notification.MediaStyle import android.app.Notification.MediaStyle
import android.app.PendingIntent import android.app.PendingIntent
import android.app.smartspace.SmartspaceAction import android.app.smartspace.SmartspaceAction
@@ -228,6 +229,30 @@ class MediaDataManagerTest : SysuiTestCase() {
assertThat(mediaDataCaptor.value!!.active).isTrue() assertThat(mediaDataCaptor.value!!.active).isTrue()
} }
@Test
fun testOnNotificationAdded_isRcn_markedRemote() {
val bundle = Bundle().apply {
putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, "Remote Cast Notification")
}
val rcn = SbnBuilder().run {
setPkg("com.android.systemui") // System package
modifyNotification(context).also {
it.setSmallIcon(android.R.drawable.ic_media_pause)
it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
it.addExtras(bundle)
}
build()
}
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(false))
assertThat(mediaDataCaptor.value!!.playbackLocation).isEqualTo(
MediaData.PLAYBACK_CAST_REMOTE)
}
@Test @Test
fun testOnNotificationRemoved_callsListener() { fun testOnNotificationRemoved_callsListener() {
mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onNotificationAdded(KEY, mediaNotification)
@@ -306,7 +331,8 @@ class MediaDataManagerTest : SysuiTestCase() {
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
eq(false)) eq(false))
val data = mediaDataCaptor.value val data = mediaDataCaptor.value
val dataRemoteWithResume = data.copy(resumeAction = Runnable {}, isLocalSession = false) val dataRemoteWithResume = data.copy(resumeAction = Runnable {},
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume) mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
// WHEN the notification is removed // WHEN the notification is removed

View File

@@ -42,7 +42,8 @@ public class MediaPlayerDataTest : SysuiTestCase() {
val mockito = MockitoJUnit.rule() val mockito = MockitoJUnit.rule()
companion object { companion object {
val LOCAL = true val LOCAL = MediaData.PLAYBACK_LOCAL
val REMOTE = MediaData.PLAYBACK_CAST_LOCAL
val RESUMPTION = true val RESUMPTION = true
val PLAYING = true val PLAYING = true
val UNDETERMINED = null val UNDETERMINED = null
@@ -58,7 +59,7 @@ public class MediaPlayerDataTest : SysuiTestCase() {
val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION) val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
val playerIsRemote = mock(MediaControlPanel::class.java) val playerIsRemote = mock(MediaControlPanel::class.java)
val dataIsRemote = createMediaData("app2", PLAYING, !LOCAL, !RESUMPTION) val dataIsRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
MediaPlayerData.addMediaPlayer("2", dataIsRemote, playerIsRemote, systemClock) MediaPlayerData.addMediaPlayer("2", dataIsRemote, playerIsRemote, systemClock)
MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock) MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock)
@@ -100,13 +101,13 @@ public class MediaPlayerDataTest : SysuiTestCase() {
val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION) val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
val playerIsPlayingAndRemote = mock(MediaControlPanel::class.java) val playerIsPlayingAndRemote = mock(MediaControlPanel::class.java)
val dataIsPlayingAndRemote = createMediaData("app2", PLAYING, !LOCAL, !RESUMPTION) val dataIsPlayingAndRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
val playerIsStoppedAndLocal = mock(MediaControlPanel::class.java) val playerIsStoppedAndLocal = mock(MediaControlPanel::class.java)
val dataIsStoppedAndLocal = createMediaData("app3", !PLAYING, LOCAL, !RESUMPTION) val dataIsStoppedAndLocal = createMediaData("app3", !PLAYING, LOCAL, !RESUMPTION)
val playerIsStoppedAndRemote = mock(MediaControlPanel::class.java) val playerIsStoppedAndRemote = mock(MediaControlPanel::class.java)
val dataIsStoppedAndRemote = createMediaData("app4", !PLAYING, !LOCAL, !RESUMPTION) val dataIsStoppedAndRemote = createMediaData("app4", !PLAYING, REMOTE, !RESUMPTION)
val playerCanResume = mock(MediaControlPanel::class.java) val playerCanResume = mock(MediaControlPanel::class.java)
val dataCanResume = createMediaData("app5", !PLAYING, LOCAL, RESUMPTION) val dataCanResume = createMediaData("app5", !PLAYING, LOCAL, RESUMPTION)
@@ -127,8 +128,8 @@ public class MediaPlayerDataTest : SysuiTestCase() {
val players = MediaPlayerData.players() val players = MediaPlayerData.players()
assertThat(players).hasSize(6) assertThat(players).hasSize(6)
assertThat(players).containsExactly(playerIsPlaying, playerIsPlayingAndRemote, assertThat(players).containsExactly(playerIsPlaying, playerIsPlayingAndRemote,
playerIsStoppedAndRemote, playerIsStoppedAndLocal, playerCanResume, playerIsStoppedAndRemote, playerIsStoppedAndLocal, playerUndetermined,
playerUndetermined).inOrder() playerCanResume).inOrder()
} }
@Test @Test
@@ -160,9 +161,10 @@ public class MediaPlayerDataTest : SysuiTestCase() {
private fun createMediaData( private fun createMediaData(
app: String, app: String,
isPlaying: Boolean?, isPlaying: Boolean?,
isLocalSession: Boolean, location: Int,
resumption: Boolean resumption: Boolean
) = ) =
MediaData(0, false, 0, app, null, null, null, null, emptyList(), emptyList<Int>(), "", MediaData(0, false, 0, app, null, null, null, null, emptyList(), emptyList<Int>(),
null, null, null, true, null, isLocalSession, resumption, null, false, isPlaying) "package:" + app, null, null, null, true, null, location, resumption, "key:" + app,
false, isPlaying)
} }

View File

@@ -211,10 +211,20 @@ class MediaResumeListenerTest : SysuiTestCase() {
} }
@Test @Test
fun testOnLoad_remotePlayback_doesNotCheck() { fun testOnLoad_localCast_doesNotCheck() {
// When media data is loaded that has not been checked yet, and is not local // When media data is loaded that has not been checked yet, and is a local cast
val dataRemote = data.copy(isLocalSession = false) val dataCast = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
resumeListener.onMediaDataLoaded(KEY, null, dataRemote) resumeListener.onMediaDataLoaded(KEY, null, dataCast)
// Then we do not take action
verify(mediaDataManager, never()).setResumeAction(any(), any())
}
@Test
fun testOnload_remoteCast_doesNotCheck() {
// 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 // Then we do not take action
verify(mediaDataManager, never()).setResumeAction(any(), any()) verify(mediaDataManager, never()).setResumeAction(any(), any())