Filter media controls by user
Adds a new step MediaDataFilter in the pipeline, after MediaDataCombineLatest, which will only pass on media control loaded / removed events if they are for the current user If the user is switched, removes all existing controls for the old user and then adds back any old controls for the new user Fixes: 159958740 Test: atest com.android.systemui.media Test: manual, switch users and observe controls updated correctly Change-Id: Ia07180432df1ce031c42a6225d52160f83ef677e
This commit is contained in:
@@ -39,9 +39,8 @@ class MediaCarouselController @Inject constructor(
|
||||
private val mediaHostStatesManager: MediaHostStatesManager,
|
||||
private val activityStarter: ActivityStarter,
|
||||
@Main executor: DelayableExecutor,
|
||||
mediaManager: MediaDataCombineLatest,
|
||||
mediaManager: MediaDataFilter,
|
||||
configurationController: ConfigurationController,
|
||||
mediaDataManager: MediaDataManager,
|
||||
falsingManager: FalsingManager
|
||||
) {
|
||||
/**
|
||||
@@ -148,7 +147,7 @@ class MediaCarouselController @Inject constructor(
|
||||
mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
|
||||
pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
|
||||
mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
|
||||
executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation,
|
||||
executor, mediaManager::onSwipeToDismiss, this::updatePageIndicatorLocation,
|
||||
falsingManager)
|
||||
isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
inflateSettingsButton()
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.media.session.MediaSession
|
||||
|
||||
/** State of a media view. */
|
||||
data class MediaData(
|
||||
val userId: Int,
|
||||
val initialized: Boolean = false,
|
||||
val backgroundColor: Int,
|
||||
/**
|
||||
|
||||
@@ -57,6 +57,17 @@ class MediaDataCombineLatest @Inject constructor(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map of all non-null data entries
|
||||
*/
|
||||
fun getData(): Map<String, MediaData> {
|
||||
return entries.filter {
|
||||
(key, pair) -> pair.first != null
|
||||
}.mapValues {
|
||||
(key, pair) -> pair.first!!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener for [MediaData] changes that has been combined with latest [MediaDeviceData].
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.util.Log
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.settings.CurrentUserTracker
|
||||
import com.android.systemui.statusbar.NotificationLockscreenUserManager
|
||||
import java.util.concurrent.Executor
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val TAG = "MediaDataFilter"
|
||||
|
||||
/**
|
||||
* Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
|
||||
* switches (removing entries for the previous user, adding back entries for the current user)
|
||||
*
|
||||
* This is added downstream of [MediaDataManager] since we may still need to handle callbacks from
|
||||
* background users (e.g. timeouts) that UI classes should ignore.
|
||||
* Instead, UI classes should listen to this so they can stay in sync with the current user.
|
||||
*/
|
||||
@Singleton
|
||||
class MediaDataFilter @Inject constructor(
|
||||
private val dataSource: MediaDataCombineLatest,
|
||||
private val broadcastDispatcher: BroadcastDispatcher,
|
||||
private val mediaResumeListener: MediaResumeListener,
|
||||
private val mediaDataManager: MediaDataManager,
|
||||
private val lockscreenUserManager: NotificationLockscreenUserManager,
|
||||
@Main private val executor: Executor
|
||||
) : MediaDataManager.Listener {
|
||||
private val userTracker: CurrentUserTracker
|
||||
private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
|
||||
|
||||
// The filtered mediaEntries, which will be a subset of all mediaEntries in MediaDataManager
|
||||
private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
|
||||
|
||||
init {
|
||||
userTracker = object : CurrentUserTracker(broadcastDispatcher) {
|
||||
override fun onUserSwitched(newUserId: Int) {
|
||||
// Post this so we can be sure lockscreenUserManager already got the broadcast
|
||||
executor.execute { handleUserSwitched(newUserId) }
|
||||
}
|
||||
}
|
||||
userTracker.startTracking()
|
||||
dataSource.addListener(this)
|
||||
}
|
||||
|
||||
override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
|
||||
if (!lockscreenUserManager.isCurrentProfile(data.userId)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (oldKey != null) {
|
||||
mediaEntries.remove(oldKey)
|
||||
}
|
||||
mediaEntries.put(key, data)
|
||||
|
||||
// Notify listeners
|
||||
val listenersCopy = listeners.toSet()
|
||||
listenersCopy.forEach {
|
||||
it.onMediaDataLoaded(key, oldKey, data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMediaDataRemoved(key: String) {
|
||||
mediaEntries.remove(key)?.let {
|
||||
// Only notify listeners if something actually changed
|
||||
val listenersCopy = listeners.toSet()
|
||||
listenersCopy.forEach {
|
||||
it.onMediaDataRemoved(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun handleUserSwitched(id: Int) {
|
||||
// If the user changes, remove all current MediaData objects and inform listeners
|
||||
val listenersCopy = listeners.toSet()
|
||||
val keyCopy = mediaEntries.keys.toMutableList()
|
||||
// Clear the list first, to make sure callbacks from listeners if we have any entries
|
||||
// are up to date
|
||||
mediaEntries.clear()
|
||||
keyCopy.forEach {
|
||||
Log.d(TAG, "Removing $it after user change")
|
||||
listenersCopy.forEach { listener ->
|
||||
listener.onMediaDataRemoved(it)
|
||||
}
|
||||
}
|
||||
|
||||
dataSource.getData().forEach { (key, data) ->
|
||||
if (lockscreenUserManager.isCurrentProfile(data.userId)) {
|
||||
Log.d(TAG, "Re-adding $key after user change")
|
||||
mediaEntries.put(key, data)
|
||||
listenersCopy.forEach { listener ->
|
||||
listener.onMediaDataLoaded(key, null, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the user has dismissed the media carousel
|
||||
*/
|
||||
fun onSwipeToDismiss() {
|
||||
val mediaKeys = mediaEntries.keys.toSet()
|
||||
mediaKeys.forEach {
|
||||
mediaDataManager.setTimedOut(it, timedOut = true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Are there any media notifications active?
|
||||
*/
|
||||
fun hasActiveMedia() = mediaEntries.any { it.value.active }
|
||||
|
||||
/**
|
||||
* Are there any media entries we should display?
|
||||
* If resumption is enabled, this will include inactive players
|
||||
* If resumption is disabled, we only want to show active players
|
||||
*/
|
||||
fun hasAnyMedia() = if (mediaResumeListener.isResumptionEnabled()) {
|
||||
mediaEntries.isNotEmpty()
|
||||
} else {
|
||||
hasActiveMedia()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener for filtered [MediaData] changes
|
||||
*/
|
||||
fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
|
||||
|
||||
/**
|
||||
* Remove a listener that was registered with addListener
|
||||
*/
|
||||
fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
|
||||
}
|
||||
@@ -67,7 +67,7 @@ private const val DEFAULT_LUMINOSITY = 0.25f
|
||||
private const val LUMINOSITY_THRESHOLD = 0.05f
|
||||
private const val SATURATION_MULTIPLIER = 0.8f
|
||||
|
||||
private val LOADING = MediaData(false, 0, null, null, null, null, null,
|
||||
private val LOADING = MediaData(-1, false, 0, null, null, null, null, null,
|
||||
emptyList(), emptyList(), "INVALID", null, null, null, true, null)
|
||||
|
||||
fun isMediaNotification(sbn: StatusBarNotification): Boolean {
|
||||
@@ -116,15 +116,6 @@ class MediaDataManager(
|
||||
broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener,
|
||||
Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context))
|
||||
|
||||
private val userChangeReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (Intent.ACTION_USER_SWITCHED == intent.action) {
|
||||
// Remove all controls, regardless of state
|
||||
clearData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val appChangeReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
@@ -152,9 +143,6 @@ class MediaDataManager(
|
||||
mediaResumeListener.setManager(this)
|
||||
addListener(mediaResumeListener)
|
||||
|
||||
val userFilter = IntentFilter(Intent.ACTION_USER_SWITCHED)
|
||||
broadcastDispatcher.registerReceiver(userChangeReceiver, userFilter, null, UserHandle.ALL)
|
||||
|
||||
val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
|
||||
broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
|
||||
|
||||
@@ -169,7 +157,6 @@ class MediaDataManager(
|
||||
|
||||
fun destroy() {
|
||||
context.unregisterReceiver(appChangeReceiver)
|
||||
broadcastDispatcher.unregisterReceiver(userChangeReceiver)
|
||||
}
|
||||
|
||||
fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
|
||||
@@ -190,20 +177,6 @@ class MediaDataManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearData() {
|
||||
// Called on user change. Remove all current MediaData objects and inform listeners
|
||||
val listenersCopy = listeners.toSet()
|
||||
val keyCopy = mediaEntries.keys.toMutableList()
|
||||
// Clear the list first, to make sure callbacks from listeners if we have any entries
|
||||
// are up to date
|
||||
mediaEntries.clear()
|
||||
keyCopy.forEach {
|
||||
listenersCopy.forEach { listener ->
|
||||
listener.onMediaDataRemoved(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAllForPackage(packageName: String) {
|
||||
Assert.isMainThread()
|
||||
val listenersCopy = listeners.toSet()
|
||||
@@ -224,6 +197,7 @@ class MediaDataManager(
|
||||
}
|
||||
|
||||
fun addResumptionControls(
|
||||
userId: Int,
|
||||
desc: MediaDescription,
|
||||
action: Runnable,
|
||||
token: MediaSession.Token,
|
||||
@@ -238,7 +212,8 @@ class MediaDataManager(
|
||||
mediaEntries.put(packageName, resumeData)
|
||||
}
|
||||
backgroundExecutor.execute {
|
||||
loadMediaDataInBgForResumption(desc, action, token, appName, appIntent, packageName)
|
||||
loadMediaDataInBgForResumption(userId, desc, action, token, appName, appIntent,
|
||||
packageName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +257,7 @@ class MediaDataManager(
|
||||
* This will make the player not active anymore, hiding it from QQS and Keyguard.
|
||||
* @see MediaData.active
|
||||
*/
|
||||
private fun setTimedOut(token: String, timedOut: Boolean) {
|
||||
internal fun setTimedOut(token: String, timedOut: Boolean) {
|
||||
mediaEntries[token]?.let {
|
||||
if (it.active == !timedOut) {
|
||||
return
|
||||
@@ -293,6 +268,7 @@ class MediaDataManager(
|
||||
}
|
||||
|
||||
private fun loadMediaDataInBgForResumption(
|
||||
userId: Int,
|
||||
desc: MediaDescription,
|
||||
resumeAction: Runnable,
|
||||
token: MediaSession.Token,
|
||||
@@ -307,7 +283,7 @@ class MediaDataManager(
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "adding track from browser: $desc")
|
||||
Log.d(TAG, "adding track for $userId from browser: $desc")
|
||||
|
||||
// Album art
|
||||
var artworkBitmap = desc.iconBitmap
|
||||
@@ -323,7 +299,7 @@ class MediaDataManager(
|
||||
|
||||
val mediaAction = getResumeMediaAction(resumeAction)
|
||||
foregroundExecutor.execute {
|
||||
onMediaDataLoaded(packageName, null, MediaData(true, bgColor, appName,
|
||||
onMediaDataLoaded(packageName, null, MediaData(userId, true, bgColor, appName,
|
||||
null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
|
||||
packageName, token, appIntent, device = null, active = false,
|
||||
resumeAction = resumeAction, resumption = true, notificationKey = packageName,
|
||||
@@ -439,10 +415,11 @@ class MediaDataManager(
|
||||
val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
|
||||
val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
|
||||
val active = mediaEntries[key]?.active ?: true
|
||||
onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist,
|
||||
song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
|
||||
notif.contentIntent, null, active, resumeAction = resumeAction,
|
||||
notificationKey = key, hasCheckedForResume = hasCheckedForResume))
|
||||
onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app,
|
||||
smallIconDrawable, artist, song, artWorkIcon, actionIcons,
|
||||
actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null,
|
||||
active, resumeAction = resumeAction, notificationKey = key,
|
||||
hasCheckedForResume = hasCheckedForResume))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,18 +541,6 @@ class MediaDataManager(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Are there any media notifications active?
|
||||
*/
|
||||
fun hasActiveMedia() = mediaEntries.any { it.value.active }
|
||||
|
||||
/**
|
||||
* Are there any media entries we should display?
|
||||
* If resumption is enabled, this will include inactive players
|
||||
* If resumption is disabled, we only want to show active players
|
||||
*/
|
||||
fun hasAnyMedia() = if (useMediaResumption) mediaEntries.isNotEmpty() else hasActiveMedia()
|
||||
|
||||
fun setMediaResumptionEnabled(isEnabled: Boolean) {
|
||||
if (useMediaResumption == isEnabled) {
|
||||
return
|
||||
@@ -596,16 +561,6 @@ class MediaDataManager(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the user has dismissed the media carousel
|
||||
*/
|
||||
fun onSwipeToDismiss() {
|
||||
val mediaKeys = mediaEntries.keys.toSet()
|
||||
mediaKeys.forEach {
|
||||
setTimedOut(it, timedOut = true)
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,8 +14,7 @@ import javax.inject.Inject
|
||||
class MediaHost @Inject constructor(
|
||||
private val state: MediaHostStateHolder,
|
||||
private val mediaHierarchyManager: MediaHierarchyManager,
|
||||
private val mediaDataManager: MediaDataManager,
|
||||
private val mediaDataManagerCombineLatest: MediaDataCombineLatest,
|
||||
private val mediaDataFilter: MediaDataFilter,
|
||||
private val mediaHostStatesManager: MediaHostStatesManager
|
||||
) : MediaHostState by state {
|
||||
lateinit var hostView: UniqueObjectHostView
|
||||
@@ -80,12 +79,12 @@ class MediaHost @Inject constructor(
|
||||
// be a delay until the views and the controllers are initialized, leaving us
|
||||
// with either a blank view or the controllers not yet initialized and the
|
||||
// measuring wrong
|
||||
mediaDataManagerCombineLatest.addListener(listener)
|
||||
mediaDataFilter.addListener(listener)
|
||||
updateViewVisibility()
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View?) {
|
||||
mediaDataManagerCombineLatest.removeListener(listener)
|
||||
mediaDataFilter.removeListener(listener)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -114,9 +113,9 @@ class MediaHost @Inject constructor(
|
||||
|
||||
private fun updateViewVisibility() {
|
||||
visible = if (showsOnlyActiveMedia) {
|
||||
mediaDataManager.hasActiveMedia()
|
||||
mediaDataFilter.hasActiveMedia()
|
||||
} else {
|
||||
mediaDataManager.hasAnyMedia()
|
||||
mediaDataFilter.hasAnyMedia()
|
||||
}
|
||||
val newVisibility = if (visible) View.VISIBLE else View.GONE
|
||||
if (newVisibility != hostView.visibility) {
|
||||
|
||||
@@ -56,7 +56,7 @@ class MediaResumeListener @Inject constructor(
|
||||
private lateinit var mediaDataManager: MediaDataManager
|
||||
|
||||
private var mediaBrowser: ResumeMediaBrowser? = null
|
||||
private var currentUserId: Int
|
||||
private var currentUserId: Int = context.userId
|
||||
|
||||
private val userChangeReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
@@ -65,7 +65,6 @@ class MediaResumeListener @Inject constructor(
|
||||
} else if (Intent.ACTION_USER_SWITCHED == intent.action) {
|
||||
currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
|
||||
loadSavedComponents()
|
||||
loadMediaResumptionControls()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,13 +88,12 @@ class MediaResumeListener @Inject constructor(
|
||||
}
|
||||
|
||||
Log.d(TAG, "Adding resume controls $desc")
|
||||
mediaDataManager.addResumptionControls(desc, resumeAction, token, appName.toString(),
|
||||
appIntent, component.packageName)
|
||||
mediaDataManager.addResumptionControls(currentUserId, desc, resumeAction, token,
|
||||
appName.toString(), appIntent, component.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
currentUserId = context.userId
|
||||
if (useMediaResumption) {
|
||||
val unlockFilter = IntentFilter()
|
||||
unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED)
|
||||
@@ -118,6 +116,8 @@ class MediaResumeListener @Inject constructor(
|
||||
}, Settings.Secure.MEDIA_CONTROLS_RESUME)
|
||||
}
|
||||
|
||||
fun isResumptionEnabled() = useMediaResumption
|
||||
|
||||
private fun loadSavedComponents() {
|
||||
// Make sure list is empty (if we switched users)
|
||||
resumeComponents.clear()
|
||||
|
||||
@@ -64,6 +64,7 @@ private const val DEVICE_NAME = "DEVICE_NAME"
|
||||
private const val SESSION_KEY = "SESSION_KEY"
|
||||
private const val SESSION_ARTIST = "SESSION_ARTIST"
|
||||
private const val SESSION_TITLE = "SESSION_TITLE"
|
||||
private const val USER_ID = 0
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@@ -180,7 +181,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun bindWhenUnattached() {
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, null, null, device, true, null)
|
||||
player.bind(state)
|
||||
assertThat(player.isPlaying()).isFalse()
|
||||
@@ -189,7 +190,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun bindText() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
|
||||
player.bind(state)
|
||||
assertThat(appName.getText()).isEqualTo(APP)
|
||||
@@ -200,7 +201,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun bindBackgroundColor() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
|
||||
player.bind(state)
|
||||
val list = ArgumentCaptor.forClass(ColorStateList::class.java)
|
||||
@@ -211,7 +212,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun bindDevice() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
|
||||
player.bind(state)
|
||||
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
|
||||
@@ -223,7 +224,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
seamless.id = 1
|
||||
seamlessFallback.id = 2
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice, true, null)
|
||||
player.bind(state)
|
||||
verify(expandedSet).setVisibility(seamless.id, View.GONE)
|
||||
@@ -235,7 +236,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun bindNullDevice() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null)
|
||||
player.bind(state)
|
||||
assertThat(seamless.isEnabled()).isTrue()
|
||||
@@ -246,7 +247,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun bindDeviceResumptionPlayer() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null,
|
||||
resumption = true)
|
||||
player.bind(state)
|
||||
|
||||
@@ -52,6 +52,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
||||
private static final String ARTIST = "ARTIST";
|
||||
private static final String TITLE = "TITLE";
|
||||
private static final String DEVICE_NAME = "DEVICE_NAME";
|
||||
private static final int USER_ID = 0;
|
||||
|
||||
private MediaDataCombineLatest mManager;
|
||||
|
||||
@@ -78,7 +79,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
||||
|
||||
mManager.addListener(mListener);
|
||||
|
||||
mMediaData = new MediaData(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, false,
|
||||
KEY, false);
|
||||
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.graphics.Color
|
||||
import androidx.test.filters.SmallTest
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.statusbar.NotificationLockscreenUserManager
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
private const val KEY = "TEST_KEY"
|
||||
private const val KEY_ALT = "TEST_KEY_2"
|
||||
private const val USER_MAIN = 0
|
||||
private const val USER_GUEST = 10
|
||||
private const val APP = "APP"
|
||||
private const val BG_COLOR = Color.RED
|
||||
private const val PACKAGE = "PKG"
|
||||
private const val ARTIST = "ARTIST"
|
||||
private const val TITLE = "TITLE"
|
||||
private const val DEVICE_NAME = "DEVICE_NAME"
|
||||
|
||||
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
||||
private fun <T> any(): T = Mockito.any()
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@TestableLooper.RunWithLooper
|
||||
class MediaDataFilterTest : SysuiTestCase() {
|
||||
|
||||
@Mock
|
||||
private lateinit var combineLatest: MediaDataCombineLatest
|
||||
@Mock
|
||||
private lateinit var listener: MediaDataManager.Listener
|
||||
@Mock
|
||||
private lateinit var broadcastDispatcher: BroadcastDispatcher
|
||||
@Mock
|
||||
private lateinit var mediaResumeListener: MediaResumeListener
|
||||
@Mock
|
||||
private lateinit var mediaDataManager: MediaDataManager
|
||||
@Mock
|
||||
private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
|
||||
@Mock
|
||||
private lateinit var executor: Executor
|
||||
|
||||
private lateinit var mediaDataFilter: MediaDataFilter
|
||||
private lateinit var dataMain: MediaData
|
||||
private lateinit var dataGuest: MediaData
|
||||
private val device = MediaDeviceData(true, null, DEVICE_NAME)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
mediaDataFilter = MediaDataFilter(combineLatest, broadcastDispatcher, mediaResumeListener,
|
||||
mediaDataManager, lockscreenUserManager, executor)
|
||||
mediaDataFilter.addListener(listener)
|
||||
|
||||
// Start all tests as main user
|
||||
setUser(USER_MAIN)
|
||||
|
||||
// Set up test media data
|
||||
dataMain = MediaData(USER_MAIN, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, null, null, device, true, null)
|
||||
|
||||
dataGuest = MediaData(USER_GUEST, true, BG_COLOR, APP, null, ARTIST, TITLE, null,
|
||||
emptyList(), emptyList(), PACKAGE, null, null, device, true, null)
|
||||
}
|
||||
|
||||
private fun setUser(id: Int) {
|
||||
`when`(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
|
||||
`when`(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
|
||||
mediaDataFilter.handleUserSwitched(id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnDataLoadedForCurrentUser_callsListener() {
|
||||
// GIVEN a media for main user
|
||||
mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
|
||||
|
||||
// THEN we should tell the listener
|
||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnDataLoadedForGuest_doesNotCallListener() {
|
||||
// GIVEN a media for guest user
|
||||
mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
|
||||
|
||||
// THEN we should NOT tell the listener
|
||||
verify(listener, never()).onMediaDataLoaded(any(), any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnRemovedForCurrent_callsListener() {
|
||||
// GIVEN a media was removed for main user
|
||||
mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
|
||||
mediaDataFilter.onMediaDataRemoved(KEY)
|
||||
|
||||
// THEN we should tell the listener
|
||||
verify(listener).onMediaDataRemoved(eq(KEY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnRemovedForGuest_doesNotCallListener() {
|
||||
// GIVEN a media was removed for guest user
|
||||
mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
|
||||
mediaDataFilter.onMediaDataRemoved(KEY)
|
||||
|
||||
// THEN we should NOT tell the listener
|
||||
verify(listener, never()).onMediaDataRemoved(eq(KEY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnUserSwitched_removesOldUserControls() {
|
||||
// GIVEN that we have a media loaded for main user
|
||||
mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
|
||||
|
||||
// and we switch to guest user
|
||||
setUser(USER_GUEST)
|
||||
|
||||
// THEN we should remove the main user's media
|
||||
verify(listener).onMediaDataRemoved(eq(KEY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnUserSwitched_addsNewUserControls() {
|
||||
// GIVEN that we had some media for both users
|
||||
val dataMap = mapOf(KEY to dataMain, KEY_ALT to dataGuest)
|
||||
`when`(combineLatest.getData()).thenReturn(dataMap)
|
||||
|
||||
// and we switch to guest user
|
||||
setUser(USER_GUEST)
|
||||
|
||||
// THEN we should add back the guest user media
|
||||
verify(listener).onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest))
|
||||
|
||||
// but not the main user's
|
||||
verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), eq(dataMain))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHasAnyMedia() {
|
||||
assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
|
||||
|
||||
mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
|
||||
assertThat(mediaDataFilter.hasAnyMedia()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHasActiveMedia() {
|
||||
assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
|
||||
val data = dataMain.copy(active = true)
|
||||
|
||||
mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
|
||||
assertThat(mediaDataFilter.hasActiveMedia()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHasAnyMedia_onlyCurrentUser() {
|
||||
assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
|
||||
|
||||
mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataGuest)
|
||||
assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHasActiveMedia_onlyCurrentUser() {
|
||||
assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
|
||||
val data = dataGuest.copy(active = true)
|
||||
|
||||
mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
|
||||
assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnNotificationRemoved_doesntHaveMedia() {
|
||||
mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
|
||||
mediaDataFilter.onMediaDataRemoved(KEY)
|
||||
assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnSwipeToDismiss_setsTimedOut() {
|
||||
mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
|
||||
mediaDataFilter.onSwipeToDismiss()
|
||||
|
||||
verify(mediaDataManager).setTimedOut(eq(KEY), eq(true))
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ private const val PACKAGE_NAME = "com.android.systemui"
|
||||
private const val APP_NAME = "SystemUI"
|
||||
private const val SESSION_ARTIST = "artist"
|
||||
private const val SESSION_TITLE = "title"
|
||||
private const val USER_ID = 0
|
||||
|
||||
private fun <T> anyObject(): T {
|
||||
return Mockito.anyObject<T>()
|
||||
@@ -91,28 +92,15 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHasActiveMedia() {
|
||||
assertThat(mediaDataManager.hasActiveMedia()).isFalse()
|
||||
val data = mock(MediaData::class.java)
|
||||
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data)
|
||||
assertThat(mediaDataManager.hasActiveMedia()).isFalse()
|
||||
|
||||
whenever(data.active).thenReturn(true)
|
||||
assertThat(mediaDataManager.hasActiveMedia()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnSwipeToDismiss_deactivatesMedia() {
|
||||
val data = MediaData(initialized = true, backgroundColor = 0, app = null, appIcon = null,
|
||||
artist = null, song = null, artwork = null, actions = emptyList(),
|
||||
fun testSetTimedOut_deactivatesMedia() {
|
||||
val data = MediaData(userId = USER_ID, initialized = true, backgroundColor = 0, app = null,
|
||||
appIcon = null, artist = null, song = null, artwork = null, actions = emptyList(),
|
||||
actionsToShowInCompact = emptyList(), packageName = "INVALID", token = null,
|
||||
clickIntent = null, device = null, active = true, resumeAction = null)
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data)
|
||||
|
||||
mediaDataManager.onSwipeToDismiss()
|
||||
mediaDataManager.setTimedOut(KEY, timedOut = true)
|
||||
assertThat(data.active).isFalse()
|
||||
}
|
||||
|
||||
@@ -141,37 +129,6 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(listener.data!!.active).isTrue()
|
||||
|
||||
// Swiping away makes the notification not active
|
||||
mediaDataManager.onSwipeToDismiss()
|
||||
assertThat(mediaDataManager.hasActiveMedia()).isFalse()
|
||||
|
||||
// And when a notification is updated
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
|
||||
// MediaData should still be inactive
|
||||
assertThat(mediaDataManager.hasActiveMedia()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHasAnyMedia_whenAddingMedia() {
|
||||
assertThat(mediaDataManager.hasAnyMedia()).isFalse()
|
||||
val data = mock(MediaData::class.java)
|
||||
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data)
|
||||
assertThat(mediaDataManager.hasAnyMedia()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnNotificationRemoved_doesntHaveMedia() {
|
||||
val data = mock(MediaData::class.java)
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data)
|
||||
mediaDataManager.onNotificationRemoved(KEY)
|
||||
assertThat(mediaDataManager.hasAnyMedia()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -212,8 +169,8 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
setTitle(SESSION_TITLE)
|
||||
build()
|
||||
}
|
||||
mediaDataManager.addResumptionControls(desc, Runnable {}, session.sessionToken, APP_NAME,
|
||||
pendingIntent, PACKAGE_NAME)
|
||||
mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken,
|
||||
APP_NAME, pendingIntent, PACKAGE_NAME)
|
||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
||||
// THEN the media data indicates that it is for resumption
|
||||
|
||||
@@ -58,6 +58,7 @@ private const val SESSION_KEY = "SESSION_KEY"
|
||||
private const val SESSION_ARTIST = "SESSION_ARTIST"
|
||||
private const val SESSION_TITLE = "SESSION_TITLE"
|
||||
private const val DEVICE_NAME = "DEVICE_NAME"
|
||||
private const val USER_ID = 0
|
||||
|
||||
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
||||
|
||||
@@ -118,7 +119,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||
setSmallIcon(android.R.drawable.ic_media_pause)
|
||||
setStyle(Notification.MediaStyle().setMediaSession(session.getSessionToken()))
|
||||
}
|
||||
mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null,
|
||||
mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null,
|
||||
emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null,
|
||||
device = null, active = true, resumeAction = null)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ private const val PACKAGE = "PKG"
|
||||
private const val SESSION_KEY = "SESSION_KEY"
|
||||
private const val SESSION_ARTIST = "SESSION_ARTIST"
|
||||
private const val SESSION_TITLE = "SESSION_TITLE"
|
||||
private const val USER_ID = 0
|
||||
|
||||
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
||||
private fun <T> anyObject(): T {
|
||||
@@ -93,7 +94,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
setPlaybackState(playbackBuilder.build())
|
||||
}
|
||||
session.setActive(true)
|
||||
mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null,
|
||||
mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null,
|
||||
emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null,
|
||||
device = null, active = true, resumeAction = null)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user