Merge "Sort remote cast notifications to the end" into sc-v2-dev am: ed5ced3fdd am: 0b3359da97
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/16274070 Change-Id: I1013e71937c74260069294b7ebf274cfe3573097
This commit is contained in:
@@ -833,12 +833,15 @@ internal object MediaPlayerData {
|
||||
)
|
||||
|
||||
private val comparator =
|
||||
compareByDescending<MediaSortKey> { it.data.isPlaying == true && it.data.isLocalSession }
|
||||
.thenByDescending { it.data.isPlaying }
|
||||
.thenByDescending { if (shouldPrioritizeSs) it.isSsMediaRec else !it.isSsMediaRec }
|
||||
.thenByDescending { !it.data.resumption }
|
||||
.thenByDescending { it.updateTime }
|
||||
.thenByDescending { !it.data.isLocalSession }
|
||||
compareByDescending<MediaSortKey> { it.data.isPlaying == true &&
|
||||
it.data.playbackLocation == MediaData.PLAYBACK_LOCAL }
|
||||
.thenByDescending { it.data.isPlaying == true &&
|
||||
it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL }
|
||||
.thenByDescending { if (shouldPrioritizeSs) it.isSsMediaRec else !it.isSsMediaRec }
|
||||
.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 mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
|
||||
|
||||
@@ -82,9 +82,9 @@ data class MediaData(
|
||||
*/
|
||||
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
|
||||
* will start the app and start playing).
|
||||
@@ -110,7 +110,20 @@ data class MediaData(
|
||||
* Timestamp when this player was last active.
|
||||
*/
|
||||
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. */
|
||||
data class MediaAction(
|
||||
|
||||
@@ -27,6 +27,8 @@ import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ImageDecoder
|
||||
@@ -145,6 +147,24 @@ class MediaDataManager(
|
||||
private var smartspaceSession: SmartspaceSession? = null
|
||||
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
|
||||
constructor(
|
||||
context: Context,
|
||||
@@ -434,7 +454,7 @@ class MediaDataManager(
|
||||
val existed = mediaEntries[key] != null
|
||||
backgroundExecutor.execute {
|
||||
mediaEntries[key]?.let { mediaData ->
|
||||
if (mediaData.isLocalSession) {
|
||||
if (mediaData.isLocalSession()) {
|
||||
mediaData.token?.let {
|
||||
val mediaController = mediaControllerFactory.create(it)
|
||||
mediaController.transportControls.stop()
|
||||
@@ -618,8 +638,11 @@ class MediaDataManager(
|
||||
}
|
||||
}
|
||||
|
||||
val isLocalSession = mediaController.playbackInfo?.playbackType ==
|
||||
MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL
|
||||
val playbackLocation =
|
||||
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 lastActive = systemClock.elapsedRealtime()
|
||||
foregroundExecutor.execute {
|
||||
@@ -629,7 +652,7 @@ class MediaDataManager(
|
||||
onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app,
|
||||
smallIcon, artist, song, artWorkIcon, actionIcons,
|
||||
actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null,
|
||||
active, resumeAction = resumeAction, isLocalSession = isLocalSession,
|
||||
active, resumeAction = resumeAction, playbackLocation = playbackLocation,
|
||||
notificationKey = key, hasCheckedForResume = hasCheckedForResume,
|
||||
isPlaying = isPlaying, isClearable = sbn.isClearable(),
|
||||
lastActive = lastActive))
|
||||
@@ -754,7 +777,7 @@ class MediaDataManager(
|
||||
fun onNotificationRemoved(key: String) {
|
||||
Assert.isMainThread()
|
||||
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")
|
||||
// Move to resume key (aka package name) if that key doesn't already exist.
|
||||
val resumeAction = getResumeMediaAction(removed.resumeAction!!)
|
||||
|
||||
@@ -193,7 +193,7 @@ class MediaResumeListener @Inject 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) {
|
||||
if (data.resumeAction == null && !data.hasCheckedForResume && data.isLocalSession()) {
|
||||
// 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
|
||||
|
||||
@@ -104,37 +104,54 @@ class MediaCarouselControllerTest : SysuiTestCase() {
|
||||
fun testPlayerOrdering() {
|
||||
// Test values: key, data, last active time
|
||||
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)
|
||||
|
||||
val playingRemote = Triple("playing remote",
|
||||
DATA.copy(active = true, isPlaying = true, isLocalSession = false, resumption = false),
|
||||
val playingCast = Triple("playing cast",
|
||||
DATA.copy(active = true, isPlaying = true,
|
||||
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false),
|
||||
5000L)
|
||||
|
||||
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)
|
||||
|
||||
val pausedRemote = Triple("paused remote",
|
||||
DATA.copy(active = true, isPlaying = false, isLocalSession = false, resumption = false),
|
||||
val pausedCast = Triple("paused cast",
|
||||
DATA.copy(active = true, isPlaying = false,
|
||||
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false),
|
||||
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",
|
||||
DATA.copy(active = false, isPlaying = false, isLocalSession = true, resumption = true),
|
||||
DATA.copy(active = false, isPlaying = false,
|
||||
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
|
||||
500L)
|
||||
|
||||
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)
|
||||
|
||||
// Expected ordering for media players:
|
||||
// Actively playing local sessions
|
||||
// Actively playing remote sessions
|
||||
// Paused sessions, by last active
|
||||
// Actively playing cast sessions
|
||||
// Paused local and cast sessions, by last active
|
||||
// RCNs
|
||||
// Resume controls, by last active
|
||||
|
||||
val expected = listOf(playingLocal, playingRemote, pausedRemote, pausedLocal, resume2,
|
||||
resume1)
|
||||
val expected = listOf(playingLocal, playingCast, pausedCast, pausedLocal, playingRcn,
|
||||
pausedRcn, resume2, resume1)
|
||||
|
||||
expected.forEach {
|
||||
clock.setCurrentTimeMillis(it.third)
|
||||
|
||||
@@ -74,8 +74,8 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
||||
mManager.addListener(mListener);
|
||||
|
||||
mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null,
|
||||
new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, true,
|
||||
false, KEY, false, false, false, 0L);
|
||||
new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null,
|
||||
MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L);
|
||||
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Notification.MediaStyle
|
||||
import android.app.PendingIntent
|
||||
import android.app.smartspace.SmartspaceAction
|
||||
@@ -228,6 +229,30 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
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
|
||||
fun testOnNotificationRemoved_callsListener() {
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
@@ -306,7 +331,8 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
|
||||
eq(false))
|
||||
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)
|
||||
|
||||
// WHEN the notification is removed
|
||||
|
||||
@@ -42,7 +42,8 @@ public class MediaPlayerDataTest : SysuiTestCase() {
|
||||
val mockito = MockitoJUnit.rule()
|
||||
|
||||
companion object {
|
||||
val LOCAL = true
|
||||
val LOCAL = MediaData.PLAYBACK_LOCAL
|
||||
val REMOTE = MediaData.PLAYBACK_CAST_LOCAL
|
||||
val RESUMPTION = true
|
||||
val PLAYING = true
|
||||
val UNDETERMINED = null
|
||||
@@ -58,7 +59,7 @@ public class MediaPlayerDataTest : SysuiTestCase() {
|
||||
val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
|
||||
|
||||
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("1", dataIsPlaying, playerIsPlaying, systemClock)
|
||||
@@ -100,13 +101,13 @@ public class MediaPlayerDataTest : SysuiTestCase() {
|
||||
val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
|
||||
|
||||
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 dataIsStoppedAndLocal = createMediaData("app3", !PLAYING, LOCAL, !RESUMPTION)
|
||||
|
||||
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 dataCanResume = createMediaData("app5", !PLAYING, LOCAL, RESUMPTION)
|
||||
@@ -127,8 +128,8 @@ public class MediaPlayerDataTest : SysuiTestCase() {
|
||||
val players = MediaPlayerData.players()
|
||||
assertThat(players).hasSize(6)
|
||||
assertThat(players).containsExactly(playerIsPlaying, playerIsPlayingAndRemote,
|
||||
playerIsStoppedAndRemote, playerIsStoppedAndLocal, playerCanResume,
|
||||
playerUndetermined).inOrder()
|
||||
playerIsStoppedAndRemote, playerIsStoppedAndLocal, playerUndetermined,
|
||||
playerCanResume).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -160,9 +161,10 @@ public class MediaPlayerDataTest : SysuiTestCase() {
|
||||
private fun createMediaData(
|
||||
app: String,
|
||||
isPlaying: Boolean?,
|
||||
isLocalSession: Boolean,
|
||||
location: Int,
|
||||
resumption: Boolean
|
||||
) =
|
||||
MediaData(0, false, 0, app, null, null, null, null, emptyList(), emptyList<Int>(), "",
|
||||
null, null, null, true, null, isLocalSession, resumption, null, false, isPlaying)
|
||||
MediaData(0, false, 0, app, null, null, null, null, emptyList(), emptyList<Int>(),
|
||||
"package:" + app, null, null, null, true, null, location, resumption, "key:" + app,
|
||||
false, isPlaying)
|
||||
}
|
||||
|
||||
@@ -211,10 +211,20 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnLoad_remotePlayback_doesNotCheck() {
|
||||
// When media data is loaded that has not been checked yet, and is not local
|
||||
val dataRemote = data.copy(isLocalSession = false)
|
||||
resumeListener.onMediaDataLoaded(KEY, null, dataRemote)
|
||||
fun testOnLoad_localCast_doesNotCheck() {
|
||||
// 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 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
|
||||
verify(mediaDataManager, never()).setResumeAction(any(), any())
|
||||
|
||||
Reference in New Issue
Block a user