Loading packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +9 −6 Original line number Diff line number Diff line Loading @@ -833,12 +833,15 @@ internal object MediaPlayerData { ) private val comparator = compareByDescending<MediaSortKey> { it.data.isPlaying == true && it.data.isLocalSession } .thenByDescending { it.data.isPlaying } 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.isLocalSession } .thenByDescending { it.data.notificationKey } private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() Loading packages/SystemUI/src/com/android/systemui/media/MediaData.kt +16 −3 Original line number Diff line number Diff line Loading @@ -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). Loading @@ -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( Loading packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +28 −5 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading Loading @@ -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() Loading Loading @@ -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 { Loading @@ -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)) Loading Loading @@ -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!!) Loading packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +1 −1 Original line number Diff line number Diff line Loading @@ -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 Loading packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt +29 −12 Original line number Diff line number Diff line Loading @@ -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) Loading Loading
packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +9 −6 Original line number Diff line number Diff line Loading @@ -833,12 +833,15 @@ internal object MediaPlayerData { ) private val comparator = compareByDescending<MediaSortKey> { it.data.isPlaying == true && it.data.isLocalSession } .thenByDescending { it.data.isPlaying } 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.isLocalSession } .thenByDescending { it.data.notificationKey } private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() Loading
packages/SystemUI/src/com/android/systemui/media/MediaData.kt +16 −3 Original line number Diff line number Diff line Loading @@ -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). Loading @@ -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( Loading
packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +28 −5 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading Loading @@ -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() Loading Loading @@ -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 { Loading @@ -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)) Loading Loading @@ -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!!) Loading
packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +1 −1 Original line number Diff line number Diff line Loading @@ -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 Loading
packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt +29 −12 Original line number Diff line number Diff line Loading @@ -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) Loading