From f285b1483d7169bc463fd0785608272e60398121 Mon Sep 17 00:00:00 2001 From: Beth Thibodeau Date: Mon, 29 Jun 2020 17:36:35 -0400 Subject: [PATCH] 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 --- .../systemui/media/MediaCarouselController.kt | 5 +- .../com/android/systemui/media/MediaData.kt | 1 + .../systemui/media/MediaDataCombineLatest.kt | 11 + .../android/systemui/media/MediaDataFilter.kt | 153 +++++++++++++ .../systemui/media/MediaDataManager.kt | 71 ++---- .../com/android/systemui/media/MediaHost.kt | 11 +- .../systemui/media/MediaResumeListener.kt | 10 +- .../systemui/media/MediaControlPanelTest.kt | 15 +- .../media/MediaDataCombineLatestTest.java | 3 +- .../systemui/media/MediaDataFilterTest.kt | 216 ++++++++++++++++++ .../systemui/media/MediaDataManagerTest.kt | 57 +---- .../systemui/media/MediaDeviceManagerTest.kt | 3 +- .../media/MediaTimeoutListenerTest.kt | 3 +- 13 files changed, 427 insertions(+), 132 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index 1a730c39dcd3e..043b368eaab0b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -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() diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt index 8c9cb1b240bfa..dafc52ad80255 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt @@ -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, /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt index 11cbc482459ad..0904ebccd4142 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt @@ -57,6 +57,17 @@ class MediaDataCombineLatest @Inject constructor( }) } + /** + * Get a map of all non-null data entries + */ + fun getData(): Map { + 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]. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt new file mode 100644 index 0000000000000..662831e4a445d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt @@ -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 = mutableSetOf() + + // The filtered mediaEntries, which will be a subset of all mediaEntries in MediaDataManager + private val mediaEntries: LinkedHashMap = 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) +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index d6b6660b778cf..8cb93bfc6d4de 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -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 { /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index 570de635ea1d2..3598719fcb3ad 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -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) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt index 0cc1e7bb1b564..4ec746fcb153c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt @@ -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() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt index b7f317b387433..c63781cb110a7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt @@ -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) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java index 9fdd9ad744ff4..5d4693d3ccf81 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java @@ -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); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt new file mode 100644 index 0000000000000..afb64a7649b4c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt @@ -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 eq(value: T): T = Mockito.eq(value) ?: value +private fun 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)) + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt index e56bbabfdc0bf..6761b282b26ab 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt @@ -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 anyObject(): T { return Mockito.anyObject() @@ -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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt index 6c7f2e8d79256..fc22eeb3ea685 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt @@ -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 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) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt index 916fd0fe11b78..bc143626a1de5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt @@ -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 eq(value: T): T = Mockito.eq(value) ?: value private fun 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) }