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:
Beth Thibodeau
2020-06-29 17:36:35 -04:00
parent bba5d59a4e
commit f285b1483d
13 changed files with 427 additions and 132 deletions

View File

@@ -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()

View File

@@ -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,
/**

View File

@@ -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].
*/

View File

@@ -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)
}

View File

@@ -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 {
/**

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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)

View File

@@ -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);

View File

@@ -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))
}
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)
}