Merge "Filter media controls by user" into rvc-dev

This commit is contained in:
Beth Thibodeau
2020-07-01 04:05:38 +00:00
committed by Android (Google) Code Review
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

@@ -50,6 +50,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 {
@@ -95,7 +96,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)
}