diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index 7c30d6424b6ff..127c5dd54d722 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 a76b20683a457..7a8e4f7e9b859 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt @@ -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 eq(value: T): T = Mockito.eq(value) ?: value private fun 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) }