summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt79
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt137
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaHost.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt163
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java104
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt15
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt110
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt383
13 files changed, 818 insertions, 265 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java
index 1e99a7b733e2..e2361d8f415a 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java
@@ -42,6 +42,7 @@ import android.hardware.SensorPrivacyManager;
import android.hardware.display.DisplayManager;
import android.media.AudioManager;
import android.media.MediaRouter2Manager;
+import android.media.session.MediaSessionManager;
import android.net.ConnectivityManager;
import android.net.NetworkScoreManager;
import android.net.wifi.WifiManager;
@@ -219,6 +220,11 @@ public class SystemServicesModule {
}
@Provides
+ static MediaSessionManager provideMediaSessionManager(Context context) {
+ return context.getSystemService(MediaSessionManager.class);
+ }
+
+ @Provides
@Singleton
static NetworkScoreManager provideNetworkScoreManager(Context context) {
return context.getSystemService(NetworkScoreManager.class);
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index 865b11f7b738..d4ae0b53d15d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -43,7 +43,7 @@ class MediaCarouselController @Inject constructor(
private val mediaHostStatesManager: MediaHostStatesManager,
private val activityStarter: ActivityStarter,
@Main executor: DelayableExecutor,
- mediaManager: MediaDataFilter,
+ mediaManager: MediaDataManager,
configurationController: ConfigurationController,
falsingManager: FalsingManager
) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
index d0642ccf9714..aa3699e9a22b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
@@ -17,65 +17,48 @@
package com.android.systemui.media
import javax.inject.Inject
-import javax.inject.Singleton
/**
- * Combines updates from [MediaDataManager] with [MediaDeviceManager].
+ * Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events.
*/
-@Singleton
-class MediaDataCombineLatest @Inject constructor(
- private val dataSource: MediaDataManager,
- private val deviceSource: MediaDeviceManager
-) {
+class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
+ MediaDeviceManager.Listener {
+
private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf()
- init {
- dataSource.addListener(object : MediaDataManager.Listener {
- override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
- if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
- entries[key] = data to entries.remove(oldKey)?.second
- update(key, oldKey)
- } else {
- entries[key] = data to entries[key]?.second
- update(key, key)
- }
- }
- override fun onMediaDataRemoved(key: String) {
- remove(key)
- }
- })
- deviceSource.addListener(object : MediaDeviceManager.Listener {
- override fun onMediaDeviceChanged(
- key: String,
- oldKey: String?,
- data: MediaDeviceData?
- ) {
- if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
- entries[key] = entries.remove(oldKey)?.first to data
- update(key, oldKey)
- } else {
- entries[key] = entries[key]?.first to data
- update(key, key)
- }
- }
- override fun onKeyRemoved(key: String) {
- remove(key)
- }
- })
+ override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+ if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
+ entries[key] = data to entries.remove(oldKey)?.second
+ update(key, oldKey)
+ } else {
+ entries[key] = data to entries[key]?.second
+ update(key, key)
+ }
}
- /**
- * Get a map of all non-null data entries
- */
- fun getData(): Map<String, MediaData> {
- return entries.filter {
- (key, pair) -> pair.first != null && pair.second != null
- }.mapValues {
- (key, pair) -> pair.first!!.copy(device = pair.second)
+ override fun onMediaDataRemoved(key: String) {
+ remove(key)
+ }
+
+ override fun onMediaDeviceChanged(
+ key: String,
+ oldKey: String?,
+ data: MediaDeviceData?
+ ) {
+ if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
+ entries[key] = entries.remove(oldKey)?.first to data
+ update(key, oldKey)
+ } else {
+ entries[key] = entries[key]?.first to data
+ update(key, key)
}
}
+ override fun onKeyRemoved(key: String) {
+ remove(key)
+ }
+
/**
* 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
index 24ca9708a4e3..0664a41f841d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
@@ -24,7 +24,6 @@ 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"
private const val DEBUG = true
@@ -33,24 +32,24 @@ private const val DEBUG = true
* 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.
+ * This is added at the end of the pipeline since we may still need to handle callbacks from
+ * background users (e.g. timeouts).
*/
-@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()
+ private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
+ internal val listeners: Set<MediaDataManager.Listener>
+ get() = _listeners.toSet()
+ internal lateinit var mediaDataManager: MediaDataManager
- // The filtered mediaEntries, which will be a subset of all mediaEntries in MediaDataManager
- private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
+ private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
+ // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
+ private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
init {
userTracker = object : CurrentUserTracker(broadcastDispatcher) {
@@ -60,31 +59,34 @@ class MediaDataFilter @Inject constructor(
}
}
userTracker.startTracking()
- dataSource.addListener(this)
}
override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+ if (oldKey != null && oldKey != key) {
+ allEntries.remove(oldKey)
+ }
+ allEntries.put(key, data)
+
if (!lockscreenUserManager.isCurrentProfile(data.userId)) {
return
}
- if (oldKey != null) {
- mediaEntries.remove(oldKey)
+ if (oldKey != null && oldKey != key) {
+ userEntries.remove(oldKey)
}
- mediaEntries.put(key, data)
+ userEntries.put(key, data)
// Notify listeners
- val listenersCopy = listeners.toSet()
- listenersCopy.forEach {
+ listeners.forEach {
it.onMediaDataLoaded(key, oldKey, data)
}
}
override fun onMediaDataRemoved(key: String) {
- mediaEntries.remove(key)?.let {
+ allEntries.remove(key)
+ userEntries.remove(key)?.let {
// Only notify listeners if something actually changed
- val listenersCopy = listeners.toSet()
- listenersCopy.forEach {
+ listeners.forEach {
it.onMediaDataRemoved(key)
}
}
@@ -93,11 +95,11 @@ class MediaDataFilter @Inject constructor(
@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()
+ val listenersCopy = listeners
+ val keyCopy = userEntries.keys.toMutableList()
// Clear the list first, to make sure callbacks from listeners if we have any entries
// are up to date
- mediaEntries.clear()
+ userEntries.clear()
keyCopy.forEach {
if (DEBUG) Log.d(TAG, "Removing $it after user change")
listenersCopy.forEach { listener ->
@@ -105,10 +107,10 @@ class MediaDataFilter @Inject constructor(
}
}
- dataSource.getData().forEach { (key, data) ->
+ allEntries.forEach { (key, data) ->
if (lockscreenUserManager.isCurrentProfile(data.userId)) {
if (DEBUG) Log.d(TAG, "Re-adding $key after user change")
- mediaEntries.put(key, data)
+ userEntries.put(key, data)
listenersCopy.forEach { listener ->
listener.onMediaDataLoaded(key, null, data)
}
@@ -121,7 +123,7 @@ class MediaDataFilter @Inject constructor(
*/
fun onSwipeToDismiss() {
if (DEBUG) Log.d(TAG, "Media carousel swiped away")
- val mediaKeys = mediaEntries.keys.toSet()
+ val mediaKeys = userEntries.keys.toSet()
mediaKeys.forEach {
mediaDataManager.setTimedOut(it, timedOut = true)
}
@@ -130,7 +132,7 @@ class MediaDataFilter @Inject constructor(
/**
* Are there any media notifications active?
*/
- fun hasActiveMedia() = mediaEntries.any { it.value.active }
+ fun hasActiveMedia() = userEntries.any { it.value.active }
/**
* Are there any media entries we should display?
@@ -138,7 +140,7 @@ class MediaDataFilter @Inject constructor(
* If resumption is disabled, we only want to show active players
*/
fun hasAnyMedia() = if (mediaResumeListener.isResumptionEnabled()) {
- mediaEntries.isNotEmpty()
+ userEntries.isNotEmpty()
} else {
hasActiveMedia()
}
@@ -146,10 +148,10 @@ class MediaDataFilter @Inject constructor(
/**
* Add a listener for filtered [MediaData] changes
*/
- fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
+ 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
+ fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index 45b2a4ed230b..a2a738788e3e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -98,11 +98,22 @@ class MediaDataManager(
dumpManager: DumpManager,
mediaTimeoutListener: MediaTimeoutListener,
mediaResumeListener: MediaResumeListener,
+ mediaSessionBasedFilter: MediaSessionBasedFilter,
+ mediaDeviceManager: MediaDeviceManager,
+ mediaDataCombineLatest: MediaDataCombineLatest,
+ private val mediaDataFilter: MediaDataFilter,
private var useMediaResumption: Boolean,
private val useQsMediaPlayer: Boolean
) : Dumpable {
- private val listeners: MutableSet<Listener> = mutableSetOf()
+ // Internal listeners are part of the internal pipeline. External listeners (those registered
+ // with [MediaDeviceManager.addListener]) receive events after they have propagated through
+ // the internal pipeline.
+ // Another way to think of the distinction between internal and external listeners is the
+ // following. Internal listeners are listeners that MediaDataManager depends on, and external
+ // listeners are listeners that depend on MediaDataManager.
+ // TODO(b/159539991#comment5): Move internal listeners to separate package.
+ private val internalListeners: MutableSet<Listener> = mutableSetOf()
private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
@Inject
@@ -114,9 +125,14 @@ class MediaDataManager(
dumpManager: DumpManager,
broadcastDispatcher: BroadcastDispatcher,
mediaTimeoutListener: MediaTimeoutListener,
- mediaResumeListener: MediaResumeListener
+ mediaResumeListener: MediaResumeListener,
+ mediaSessionBasedFilter: MediaSessionBasedFilter,
+ mediaDeviceManager: MediaDeviceManager,
+ mediaDataCombineLatest: MediaDataCombineLatest,
+ mediaDataFilter: MediaDataFilter
) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory,
broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener,
+ mediaSessionBasedFilter, mediaDeviceManager, mediaDataCombineLatest, mediaDataFilter,
Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context))
private val appChangeReceiver = object : BroadcastReceiver() {
@@ -139,12 +155,26 @@ class MediaDataManager(
init {
dumpManager.registerDumpable(TAG, this)
+
+ // Initialize the internal processing pipeline. The listeners at the front of the pipeline
+ // are set as internal listeners so that they receive events. From there, events are
+ // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter,
+ // so it is responsible for dispatching events to external listeners. To achieve this,
+ // external listeners that are registered with [MediaDataManager.addListener] are actually
+ // registered as listeners to mediaDataFilter.
+ addInternalListener(mediaTimeoutListener)
+ addInternalListener(mediaResumeListener)
+ addInternalListener(mediaSessionBasedFilter)
+ mediaSessionBasedFilter.addListener(mediaDeviceManager)
+ mediaSessionBasedFilter.addListener(mediaDataCombineLatest)
+ mediaDeviceManager.addListener(mediaDataCombineLatest)
+ mediaDataCombineLatest.addListener(mediaDataFilter)
+
+ // Set up links back into the pipeline for listeners that need to send events upstream.
mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
setTimedOut(token, timedOut) }
- addListener(mediaTimeoutListener)
-
mediaResumeListener.setManager(this)
- addListener(mediaResumeListener)
+ mediaDataFilter.mediaDataManager = this
val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
@@ -182,10 +212,9 @@ class MediaDataManager(
private fun removeAllForPackage(packageName: String) {
Assert.isMainThread()
- val listenersCopy = listeners.toSet()
val toRemove = mediaEntries.filter { it.value.packageName == packageName }
toRemove.forEach {
- removeEntry(it.key, listenersCopy)
+ removeEntry(it.key)
}
}
@@ -245,12 +274,45 @@ class MediaDataManager(
/**
* Add a listener for changes in this class
*/
- fun addListener(listener: Listener) = listeners.add(listener)
+ fun addListener(listener: Listener) {
+ // mediaDataFilter is the current end of the internal pipeline. Register external
+ // listeners as listeners to it.
+ mediaDataFilter.addListener(listener)
+ }
/**
* Remove a listener for changes in this class
*/
- fun removeListener(listener: Listener) = listeners.remove(listener)
+ fun removeListener(listener: Listener) {
+ // Since mediaDataFilter is the current end of the internal pipelie, external listeners
+ // have been registered to it. So, they need to be removed from it too.
+ mediaDataFilter.removeListener(listener)
+ }
+
+ /**
+ * Add a listener for internal events.
+ */
+ private fun addInternalListener(listener: Listener) = internalListeners.add(listener)
+
+ /**
+ * Notify internal listeners of loaded event.
+ *
+ * External listeners registered with [addListener] will be notified after the event propagates
+ * through the internal listener pipeline.
+ */
+ private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
+ internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) }
+ }
+
+ /**
+ * Notify internal listeners of removed event.
+ *
+ * External listeners registered with [addListener] will be notified after the event propagates
+ * through the internal listener pipeline.
+ */
+ private fun notifyMediaDataRemoved(key: String) {
+ internalListeners.forEach { it.onMediaDataRemoved(key) }
+ }
/**
* Called whenever the player has been paused or stopped for a while, or swiped from QQS.
@@ -268,16 +330,13 @@ class MediaDataManager(
}
}
- private fun removeEntry(key: String, listenersCopy: Set<Listener>) {
+ private fun removeEntry(key: String) {
mediaEntries.remove(key)
- listenersCopy.forEach {
- it.onMediaDataRemoved(key)
- }
+ notifyMediaDataRemoved(key)
}
fun dismissMediaData(key: String, delay: Long) {
- val listenersCopy = listeners.toSet()
- foregroundExecutor.executeDelayed({ removeEntry(key, listenersCopy) }, delay)
+ foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
}
private fun loadMediaDataInBgForResumption(
@@ -525,10 +584,7 @@ class MediaDataManager(
if (mediaEntries.containsKey(key)) {
// Otherwise this was removed already
mediaEntries.put(key, data)
- val listenersCopy = listeners.toSet()
- listenersCopy.forEach {
- it.onMediaDataLoaded(key, oldKey, data)
- }
+ notifyMediaDataLoaded(key, oldKey, data)
}
}
@@ -544,31 +600,21 @@ class MediaDataManager(
val pkg = removed?.packageName
val migrate = mediaEntries.put(pkg, updated) == null
// Notify listeners of "new" controls when migrating or removed and update when not
- val listenersCopy = listeners.toSet()
if (migrate) {
- listenersCopy.forEach {
- it.onMediaDataLoaded(pkg, key, updated)
- }
+ notifyMediaDataLoaded(pkg, key, updated)
} else {
// Since packageName is used for the key of the resumption controls, it is
// possible that another notification has already been reused for the resumption
// controls of this package. In this case, rather than renaming this player as
// packageName, just remove it and then send a update to the existing resumption
// controls.
- listenersCopy.forEach {
- it.onMediaDataRemoved(key)
- }
- listenersCopy.forEach {
- it.onMediaDataLoaded(pkg, pkg, updated)
- }
+ notifyMediaDataRemoved(key)
+ notifyMediaDataLoaded(pkg, pkg, updated)
}
return
}
if (removed != null) {
- val listenersCopy = listeners.toSet()
- listenersCopy.forEach {
- it.onMediaDataRemoved(key)
- }
+ notifyMediaDataRemoved(key)
}
}
@@ -581,17 +627,31 @@ class MediaDataManager(
if (!useMediaResumption) {
// Remove any existing resume controls
- val listenersCopy = listeners.toSet()
val filtered = mediaEntries.filter { !it.value.active }
filtered.forEach {
mediaEntries.remove(it.key)
- listenersCopy.forEach { listener ->
- listener.onMediaDataRemoved(it.key)
- }
+ notifyMediaDataRemoved(it.key)
}
}
}
+ /**
+ * Invoked when the user has dismissed the media carousel
+ */
+ fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss()
+
+ /**
+ * Are there any media notifications active?
+ */
+ fun hasActiveMedia() = mediaDataFilter.hasActiveMedia()
+
+ /**
+ * 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() = mediaDataFilter.hasAnyMedia()
+
interface Listener {
/**
@@ -611,7 +671,8 @@ class MediaDataManager(
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
pw.apply {
- println("listeners: $listeners")
+ println("internalListeners: $internalListeners")
+ println("externalListeners: ${mediaDataFilter.listeners}")
println("mediaEntries: $mediaEntries")
println("useMediaResumption: $useMediaResumption")
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
index ae7f66b5ac48..102a4842e3c3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
@@ -32,26 +32,23 @@ import java.io.FileDescriptor
import java.io.PrintWriter
import java.util.concurrent.Executor
import javax.inject.Inject
-import javax.inject.Singleton
/**
* Provides information about the route (ie. device) where playback is occurring.
*/
-@Singleton
class MediaDeviceManager @Inject constructor(
private val context: Context,
private val localMediaManagerFactory: LocalMediaManagerFactory,
private val mr2manager: MediaRouter2Manager,
@Main private val fgExecutor: Executor,
@Background private val bgExecutor: Executor,
- private val mediaDataManager: MediaDataManager,
- private val dumpManager: DumpManager
+ dumpManager: DumpManager
) : MediaDataManager.Listener, Dumpable {
+
private val listeners: MutableSet<Listener> = mutableSetOf()
private val entries: MutableMap<String, Entry> = mutableMapOf()
init {
- mediaDataManager.addListener(this)
dumpManager.registerDumpable(javaClass.name, this)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index 3598719fcb3a..ce184aa23a57 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -14,7 +14,7 @@ import javax.inject.Inject
class MediaHost @Inject constructor(
private val state: MediaHostStateHolder,
private val mediaHierarchyManager: MediaHierarchyManager,
- private val mediaDataFilter: MediaDataFilter,
+ private val mediaDataManager: MediaDataManager,
private val mediaHostStatesManager: MediaHostStatesManager
) : MediaHostState by state {
lateinit var hostView: UniqueObjectHostView
@@ -79,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
- mediaDataFilter.addListener(listener)
+ mediaDataManager.addListener(listener)
updateViewVisibility()
}
override fun onViewDetachedFromWindow(v: View?) {
- mediaDataFilter.removeListener(listener)
+ mediaDataManager.removeListener(listener)
}
})
@@ -113,9 +113,9 @@ class MediaHost @Inject constructor(
private fun updateViewVisibility() {
visible = if (showsOnlyActiveMedia) {
- mediaDataFilter.hasActiveMedia()
+ mediaDataManager.hasActiveMedia()
} else {
- mediaDataFilter.hasAnyMedia()
+ mediaDataManager.hasAnyMedia()
}
val newVisibility = if (visible) View.VISIBLE else View.GONE
if (newVisibility != hostView.visibility) {
@@ -289,4 +289,4 @@ interface MediaHostState {
* Get a copy of this view state, deepcopying all appropriate members
*/
fun copy(): MediaHostState
-} \ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt
new file mode 100644
index 000000000000..f01713fb5f6c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.content.ComponentName
+import android.content.Context
+import android.media.session.MediaController
+import android.media.session.MediaController.PlaybackInfo
+import android.media.session.MediaSession
+import android.media.session.MediaSessionManager
+import android.util.Log
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private const val TAG = "MediaSessionBasedFilter"
+
+/**
+ * Filters media loaded events for local media sessions while an app is casting.
+ *
+ * When an app is casting there can be one remote media sessions and potentially more local media
+ * sessions. In this situation, there should only be a media object for the remote session. To
+ * achieve this, update events for the local session need to be filtered.
+ */
+class MediaSessionBasedFilter @Inject constructor(
+ context: Context,
+ private val sessionManager: MediaSessionManager,
+ @Main private val foregroundExecutor: Executor,
+ @Background private val backgroundExecutor: Executor
+) : MediaDataManager.Listener {
+
+ private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
+
+ // Keep track of MediaControllers for a given package to check if an app is casting and it
+ // filter loaded events for local sessions.
+ private val packageControllers: LinkedHashMap<String, MutableList<MediaController>> =
+ LinkedHashMap()
+
+ // Keep track of the key used for the session tokens. This information is used to know when
+ // dispatch a removed event so that a media object for a local session will be removed.
+ private val keyedTokens: MutableMap<String, MutableList<MediaSession.Token>> = mutableMapOf()
+
+ private val sessionListener = object : MediaSessionManager.OnActiveSessionsChangedListener {
+ override fun onActiveSessionsChanged(controllers: List<MediaController>) {
+ handleControllersChanged(controllers)
+ }
+ }
+
+ init {
+ backgroundExecutor.execute {
+ val name = ComponentName(context, NotificationListenerWithPlugins::class.java)
+ sessionManager.addOnActiveSessionsChangedListener(sessionListener, name)
+ handleControllersChanged(sessionManager.getActiveSessions(name))
+ }
+ }
+
+ /**
+ * 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)
+
+ /**
+ * May filter loaded events by not passing them along to listeners.
+ *
+ * If an app has only one session with playback type PLAYBACK_TYPE_REMOTE, then assuming that
+ * the app is casting. Sometimes apps will send redundant updates to a local session with
+ * playback type PLAYBACK_TYPE_LOCAL. These updates should be filtered to improve the usability
+ * of the media controls.
+ */
+ override fun onMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
+ backgroundExecutor.execute {
+ val isMigration = oldKey != null && key != oldKey
+ if (isMigration) {
+ keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) }
+ }
+ if (info.token != null) {
+ keyedTokens.get(key)?.let {
+ tokens ->
+ tokens.add(info.token)
+ } ?: run {
+ val tokens = mutableListOf(info.token)
+ keyedTokens.put(key, tokens)
+ }
+ }
+ // Determine if an app is casting by checking if it has a session with playback type
+ // PLAYBACK_TYPE_REMOTE.
+ val remoteControllers = packageControllers.get(info.packageName)?.filter {
+ it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE
+ }
+ // Limiting search to only apps with a single remote session.
+ val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null
+ if (isMigration || remote == null || remote.sessionToken == info.token) {
+ // Not filtering in this case. Passing the event along to listeners.
+ dispatchMediaDataLoaded(key, oldKey, info)
+ } else {
+ // Filtering this event because the app is casting and the loaded events is for a
+ // local session.
+ Log.d(TAG, "filtering key=$key local=${info.token} remote=${remote?.sessionToken}")
+ // If the local session uses a different notification key, then lets go a step
+ // farther and dismiss the media data so that media controls for the local session
+ // don't hang around while casting.
+ if (!keyedTokens.get(key)!!.contains(remote.sessionToken)) {
+ dispatchMediaDataRemoved(key)
+ }
+ }
+ }
+ }
+
+ override fun onMediaDataRemoved(key: String) {
+ // Queue on background thread to ensure ordering of loaded and removed events is maintained.
+ backgroundExecutor.execute {
+ keyedTokens.remove(key)
+ dispatchMediaDataRemoved(key)
+ }
+ }
+
+ private fun dispatchMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
+ foregroundExecutor.execute {
+ listeners.toSet().forEach { it.onMediaDataLoaded(key, oldKey, info) }
+ }
+ }
+
+ private fun dispatchMediaDataRemoved(key: String) {
+ foregroundExecutor.execute {
+ listeners.toSet().forEach { it.onMediaDataRemoved(key) }
+ }
+ }
+
+ private fun handleControllersChanged(controllers: List<MediaController>) {
+ packageControllers.clear()
+ controllers.forEach {
+ controller ->
+ packageControllers.get(controller.packageName)?.let {
+ tokens ->
+ tokens.add(controller)
+ } ?: run {
+ val tokens = mutableListOf(controller)
+ packageControllers.put(controller.packageName, tokens)
+ }
+ }
+ }
+}
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 2e794a40d238..89538ac8bc9f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
@@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
@@ -34,19 +33,23 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
-import java.util.Map;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class MediaDataCombineLatestTest extends SysuiTestCase {
+ @Rule public MockitoRule mockito = MockitoJUnit.rule();
+
private static final String KEY = "TEST_KEY";
private static final String OLD_KEY = "TEST_KEY_OLD";
private static final String APP = "APP";
@@ -59,27 +62,14 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
private MediaDataCombineLatest mManager;
- @Mock private MediaDataManager mDataSource;
- @Mock private MediaDeviceManager mDeviceSource;
@Mock private MediaDataManager.Listener mListener;
- private MediaDataManager.Listener mDataListener;
- private MediaDeviceManager.Listener mDeviceListener;
-
private MediaData mMediaData;
private MediaDeviceData mDeviceData;
@Before
public void setUp() {
- mDataSource = mock(MediaDataManager.class);
- mDeviceSource = mock(MediaDeviceManager.class);
- mListener = mock(MediaDataManager.Listener.class);
-
- mManager = new MediaDataCombineLatest(mDataSource, mDeviceSource);
-
- mDataListener = captureDataListener();
- mDeviceListener = captureDeviceListener();
-
+ mManager = new MediaDataCombineLatest();
mManager.addListener(mListener);
mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null,
@@ -91,7 +81,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
@Test
public void eventNotEmittedWithoutDevice() {
// WHEN data source emits an event without device data
- mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
+ mManager.onMediaDataLoaded(KEY, null, mMediaData);
// THEN an event isn't emitted
verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any());
}
@@ -99,7 +89,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
@Test
public void eventNotEmittedWithoutMedia() {
// WHEN device source emits an event without media data
- mDeviceListener.onMediaDeviceChanged(KEY, null, mDeviceData);
+ mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
// THEN an event isn't emitted
verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any());
}
@@ -107,9 +97,9 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
@Test
public void emitEventAfterDeviceFirst() {
// GIVEN that a device event has already been received
- mDeviceListener.onMediaDeviceChanged(KEY, null, mDeviceData);
+ mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
// WHEN media event is received
- mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
+ mManager.onMediaDataLoaded(KEY, null, mMediaData);
// THEN the listener receives a combined event
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture());
@@ -119,9 +109,9 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
@Test
public void emitEventAfterMediaFirst() {
// GIVEN that media event has already been received
- mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
+ mManager.onMediaDataLoaded(KEY, null, mMediaData);
// WHEN device event is received
- mDeviceListener.onMediaDeviceChanged(KEY, null, mDeviceData);
+ mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
// THEN the listener receives a combined event
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture());
@@ -131,11 +121,11 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
@Test
public void migrateKeyMediaFirst() {
// GIVEN that media and device info has already been received
- mDataListener.onMediaDataLoaded(OLD_KEY, null, mMediaData);
- mDeviceListener.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
+ mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData);
+ mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
reset(mListener);
// WHEN a key migration event is received
- mDataListener.onMediaDataLoaded(KEY, OLD_KEY, mMediaData);
+ mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData);
// THEN the listener receives a combined event
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
verify(mListener).onMediaDataLoaded(eq(KEY), eq(OLD_KEY), captor.capture());
@@ -145,11 +135,11 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
@Test
public void migrateKeyDeviceFirst() {
// GIVEN that media and device info has already been received
- mDataListener.onMediaDataLoaded(OLD_KEY, null, mMediaData);
- mDeviceListener.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
+ mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData);
+ mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
reset(mListener);
// WHEN a key migration event is received
- mDeviceListener.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
+ mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
// THEN the listener receives a combined event
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
verify(mListener).onMediaDataLoaded(eq(KEY), eq(OLD_KEY), captor.capture());
@@ -159,12 +149,12 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
@Test
public void migrateKeyMediaAfter() {
// GIVEN that media and device info has already been received
- mDataListener.onMediaDataLoaded(OLD_KEY, null, mMediaData);
- mDeviceListener.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
- mDeviceListener.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
+ mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData);
+ mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
+ mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
reset(mListener);
// WHEN a second key migration event is received for media
- mDataListener.onMediaDataLoaded(KEY, OLD_KEY, mMediaData);
+ mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData);
// THEN the key has already been migrated
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
verify(mListener).onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture());
@@ -174,12 +164,12 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
@Test
public void migrateKeyDeviceAfter() {
// GIVEN that media and device info has already been received
- mDataListener.onMediaDataLoaded(OLD_KEY, null, mMediaData);
- mDeviceListener.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
- mDataListener.onMediaDataLoaded(KEY, OLD_KEY, mMediaData);
+ mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData);
+ mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
+ mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData);
reset(mListener);
// WHEN a second key migration event is received for the device
- mDeviceListener.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
+ mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
// THEN the key has already be migrated
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
verify(mListener).onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture());
@@ -189,60 +179,34 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
@Test
public void mediaDataRemoved() {
// WHEN media data is removed without first receiving device or data
- mDataListener.onMediaDataRemoved(KEY);
+ mManager.onMediaDataRemoved(KEY);
// THEN a removed event isn't emitted
verify(mListener, never()).onMediaDataRemoved(eq(KEY));
}
@Test
public void mediaDataRemovedAfterMediaEvent() {
- mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
- mDataListener.onMediaDataRemoved(KEY);
+ mManager.onMediaDataLoaded(KEY, null, mMediaData);
+ mManager.onMediaDataRemoved(KEY);
verify(mListener).onMediaDataRemoved(eq(KEY));
}
@Test
public void mediaDataRemovedAfterDeviceEvent() {
- mDeviceListener.onMediaDeviceChanged(KEY, null, mDeviceData);
- mDataListener.onMediaDataRemoved(KEY);
+ mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
+ mManager.onMediaDataRemoved(KEY);
verify(mListener).onMediaDataRemoved(eq(KEY));
}
@Test
public void mediaDataKeyUpdated() {
// GIVEN that device and media events have already been received
- mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
- mDeviceListener.onMediaDeviceChanged(KEY, null, mDeviceData);
+ mManager.onMediaDataLoaded(KEY, null, mMediaData);
+ mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
// WHEN the key is changed
- mDataListener.onMediaDataLoaded("NEW_KEY", KEY, mMediaData);
+ mManager.onMediaDataLoaded("NEW_KEY", KEY, mMediaData);
// THEN the listener gets a load event with the correct keys
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
verify(mListener).onMediaDataLoaded(eq("NEW_KEY"), any(), captor.capture());
}
-
- @Test
- public void getDataIncludesDevice() {
- // GIVEN that device and media events have been received
- mDeviceListener.onMediaDeviceChanged(KEY, null, mDeviceData);
- mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
-
- // THEN the result of getData includes device info
- Map<String, MediaData> results = mManager.getData();
- assertThat(results.get(KEY)).isNotNull();
- assertThat(results.get(KEY).getDevice()).isEqualTo(mDeviceData);
- }
-
- private MediaDataManager.Listener captureDataListener() {
- ArgumentCaptor<MediaDataManager.Listener> captor = ArgumentCaptor.forClass(
- MediaDataManager.Listener.class);
- verify(mDataSource).addListener(captor.capture());
- return captor.getValue();
- }
-
- private MediaDeviceManager.Listener captureDeviceListener() {
- ArgumentCaptor<MediaDeviceManager.Listener> captor = ArgumentCaptor.forClass(
- MediaDeviceManager.Listener.class);
- verify(mDeviceSource).addListener(captor.capture());
- return captor.getValue();
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
index afb64a7649b4..36b6527167f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
@@ -32,6 +32,7 @@ import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.util.concurrent.Executor
@@ -56,8 +57,6 @@ private fun <T> any(): T = Mockito.any()
class MediaDataFilterTest : SysuiTestCase() {
@Mock
- private lateinit var combineLatest: MediaDataCombineLatest
- @Mock
private lateinit var listener: MediaDataManager.Listener
@Mock
private lateinit var broadcastDispatcher: BroadcastDispatcher
@@ -78,8 +77,9 @@ class MediaDataFilterTest : SysuiTestCase() {
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- mediaDataFilter = MediaDataFilter(combineLatest, broadcastDispatcher, mediaResumeListener,
- mediaDataManager, lockscreenUserManager, executor)
+ mediaDataFilter = MediaDataFilter(broadcastDispatcher, mediaResumeListener,
+ lockscreenUserManager, executor)
+ mediaDataFilter.mediaDataManager = mediaDataManager
mediaDataFilter.addListener(listener)
// Start all tests as main user
@@ -152,8 +152,9 @@ class MediaDataFilterTest : SysuiTestCase() {
@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)
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+ mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataGuest)
+ reset(listener)
// and we switch to guest user
setUser(USER_GUEST)
@@ -213,4 +214,4 @@ class MediaDataFilterTest : SysuiTestCase() {
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 3789e6ef1f65..b47ee2921380 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -15,6 +15,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.SbnBuilder
import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -23,9 +24,13 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever
@@ -47,6 +52,7 @@ private fun <T> anyObject(): T {
@RunWith(AndroidTestingRunner::class)
class MediaDataManagerTest : SysuiTestCase() {
+ @JvmField @Rule val mockito = MockitoJUnit.rule()
@Mock lateinit var mediaControllerFactory: MediaControllerFactory
@Mock lateinit var controller: MediaController
lateinit var session: MediaSession
@@ -57,19 +63,36 @@ class MediaDataManagerTest : SysuiTestCase() {
@Mock lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock lateinit var mediaTimeoutListener: MediaTimeoutListener
@Mock lateinit var mediaResumeListener: MediaResumeListener
+ @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter
+ @Mock lateinit var mediaDeviceManager: MediaDeviceManager
+ @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
+ @Mock lateinit var mediaDataFilter: MediaDataFilter
+ @Mock lateinit var listener: MediaDataManager.Listener
@Mock lateinit var pendingIntent: PendingIntent
- @JvmField @Rule val mockito = MockitoJUnit.rule()
lateinit var mediaDataManager: MediaDataManager
lateinit var mediaNotification: StatusBarNotification
+ @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
@Before
fun setup() {
foregroundExecutor = FakeExecutor(FakeSystemClock())
backgroundExecutor = FakeExecutor(FakeSystemClock())
- mediaDataManager = MediaDataManager(context, backgroundExecutor, foregroundExecutor,
- mediaControllerFactory, broadcastDispatcher, dumpManager,
- mediaTimeoutListener, mediaResumeListener, useMediaResumption = true,
- useQsMediaPlayer = true)
+ mediaDataManager = MediaDataManager(
+ context = context,
+ backgroundExecutor = backgroundExecutor,
+ foregroundExecutor = foregroundExecutor,
+ mediaControllerFactory = mediaControllerFactory,
+ broadcastDispatcher = broadcastDispatcher,
+ dumpManager = dumpManager,
+ mediaTimeoutListener = mediaTimeoutListener,
+ mediaResumeListener = mediaResumeListener,
+ mediaSessionBasedFilter = mediaSessionBasedFilter,
+ mediaDeviceManager = mediaDeviceManager,
+ mediaDataCombineLatest = mediaDataCombineLatest,
+ mediaDataFilter = mediaDataFilter,
+ useMediaResumption = true,
+ useQsMediaPlayer = true
+ )
session = MediaSession(context, "MediaDataManagerTestSession")
mediaNotification = SbnBuilder().run {
setPkg(PACKAGE_NAME)
@@ -84,6 +107,12 @@ class MediaDataManagerTest : SysuiTestCase() {
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
}
whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller)
+
+ // This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal
+ // listeners in the internal processing pipeline. It receives events, but ince it is a
+ // mock, it doesn't pass those events along the chain to the external listeners. So, just
+ // treat mediaSessionBasedFilter as a listener for testing.
+ listener = mediaSessionBasedFilter
}
@After
@@ -113,8 +142,6 @@ class MediaDataManagerTest : SysuiTestCase() {
@Test
fun testOnMetaDataLoaded_callsListener() {
- val listener = mock(MediaDataManager.Listener::class.java)
- mediaDataManager.addListener(listener)
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), anyObject())
@@ -122,84 +149,74 @@ class MediaDataManagerTest : SysuiTestCase() {
@Test
fun testOnMetaDataLoaded_conservesActiveFlag() {
- val listener = TestListener()
whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
whenever(controller.metadata).thenReturn(metadataBuilder.build())
mediaDataManager.addListener(listener)
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
- assertThat(listener.data!!.active).isTrue()
+ verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
+ assertThat(mediaDataCaptor.value!!.active).isTrue()
}
@Test
fun testOnNotificationRemoved_callsListener() {
- val listener = mock(MediaDataManager.Listener::class.java)
- mediaDataManager.addListener(listener)
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
mediaDataManager.onNotificationRemoved(KEY)
-
verify(listener).onMediaDataRemoved(eq(KEY))
}
@Test
fun testOnNotificationRemoved_withResumption() {
// GIVEN that the manager has a notification with a resume action
- val listener = TestListener()
- mediaDataManager.addListener(listener)
whenever(controller.metadata).thenReturn(metadataBuilder.build())
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
- val data = listener.data!!
+ verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
+ val data = mediaDataCaptor.value
assertThat(data.resumption).isFalse()
mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
// WHEN the notification is removed
mediaDataManager.onNotificationRemoved(KEY)
// THEN the media data indicates that it is for resumption
- assertThat(listener.data!!.resumption).isTrue()
- // AND the new key is the package name
- assertThat(listener.key!!).isEqualTo(PACKAGE_NAME)
- assertThat(listener.oldKey!!).isEqualTo(KEY)
- assertThat(listener.removedKey).isNull()
+ verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor))
+ assertThat(mediaDataCaptor.value.resumption).isTrue()
}
@Test
fun testOnNotificationRemoved_twoWithResumption() {
// GIVEN that the manager has two notifications with resume actions
- val listener = TestListener()
- mediaDataManager.addListener(listener)
whenever(controller.metadata).thenReturn(metadataBuilder.build())
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
- val data = listener.data!!
+ verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
+ val data = mediaDataCaptor.value
assertThat(data.resumption).isFalse()
val resumableData = data.copy(resumeAction = Runnable {})
mediaDataManager.onMediaDataLoaded(KEY, null, resumableData)
mediaDataManager.onMediaDataLoaded(KEY_2, null, resumableData)
+ reset(listener)
// WHEN the first is removed
mediaDataManager.onNotificationRemoved(KEY)
// THEN the data is for resumption and the key is migrated to the package name
- assertThat(listener.data!!.resumption).isTrue()
- assertThat(listener.key!!).isEqualTo(PACKAGE_NAME)
- assertThat(listener.oldKey!!).isEqualTo(KEY)
- assertThat(listener.removedKey).isNull()
+ verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor))
+ assertThat(mediaDataCaptor.value.resumption).isTrue()
+ verify(listener, never()).onMediaDataRemoved(eq(KEY))
// WHEN the second is removed
mediaDataManager.onNotificationRemoved(KEY_2)
// THEN the data is for resumption and the second key is removed
- assertThat(listener.data!!.resumption).isTrue()
- assertThat(listener.key!!).isEqualTo(PACKAGE_NAME)
- assertThat(listener.oldKey!!).isEqualTo(PACKAGE_NAME)
- assertThat(listener.removedKey!!).isEqualTo(KEY_2)
+ verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(PACKAGE_NAME),
+ capture(mediaDataCaptor))
+ assertThat(mediaDataCaptor.value.resumption).isTrue()
+ verify(listener).onMediaDataRemoved(eq(KEY_2))
}
@Test
fun testAddResumptionControls() {
- val listener = TestListener()
- mediaDataManager.addListener(listener)
// WHEN resumption controls are added`
val desc = MediaDescription.Builder().run {
setTitle(SESSION_TITLE)
@@ -210,7 +227,8 @@ class MediaDataManagerTest : SysuiTestCase() {
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
// THEN the media data indicates that it is for resumption
- val data = listener.data!!
+ verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor))
+ val data = mediaDataCaptor.value
assertThat(data.resumption).isTrue()
assertThat(data.song).isEqualTo(SESSION_TITLE)
assertThat(data.app).isEqualTo(APP_NAME)
@@ -219,8 +237,6 @@ class MediaDataManagerTest : SysuiTestCase() {
@Test
fun testDismissMedia_listenerCalled() {
- val listener = mock(MediaDataManager.Listener::class.java)
- mediaDataManager.addListener(listener)
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
mediaDataManager.dismissMediaData(KEY, 0L)
@@ -230,26 +246,4 @@ class MediaDataManagerTest : SysuiTestCase() {
verify(listener).onMediaDataRemoved(eq(KEY))
}
-
- /**
- * Simple implementation of [MediaDataManager.Listener] for the test.
- *
- * Giving up on trying to get a mock Listener and ArgumentCaptor to work.
- */
- private class TestListener : MediaDataManager.Listener {
- var data: MediaData? = null
- var key: String? = null
- var oldKey: String? = null
- var removedKey: String? = null
-
- override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
- this.key = key
- this.oldKey = oldKey
- this.data = data
- }
-
- override fun onMediaDataRemoved(key: String) {
- removedKey = key
- }
- }
}
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 7bc15dd46cd6..fdb432cc097c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
@@ -68,7 +68,6 @@ private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
public class MediaDeviceManagerTest : SysuiTestCase() {
private lateinit var manager: MediaDeviceManager
- @Mock private lateinit var mediaDataManager: MediaDataManager
@Mock private lateinit var lmmFactory: LocalMediaManagerFactory
@Mock private lateinit var lmm: LocalMediaManager
@Mock private lateinit var mr2: MediaRouter2Manager
@@ -91,7 +90,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
fakeFgExecutor = FakeExecutor(FakeSystemClock())
fakeBgExecutor = FakeExecutor(FakeSystemClock())
manager = MediaDeviceManager(context, lmmFactory, mr2, fakeFgExecutor, fakeBgExecutor,
- mediaDataManager, dumpster)
+ dumpster)
manager.addListener(listener)
// Configure mocks.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt
new file mode 100644
index 000000000000..887cc777d4fe
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt
@@ -0,0 +1,383 @@
+/*
+ * 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 android.media.session.MediaController
+import android.media.session.MediaController.PlaybackInfo
+import android.media.session.MediaSession
+import android.media.session.MediaSessionManager
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.Mockito.`when` as whenever
+
+private const val PACKAGE = "PKG"
+private const val KEY = "TEST_KEY"
+private const val NOTIF_KEY = "TEST_KEY"
+private const val SESSION_ARTIST = "SESSION_ARTIST"
+private const val SESSION_TITLE = "SESSION_TITLE"
+private const val APP_NAME = "APP_NAME"
+private const val USER_ID = 0
+
+private val info = MediaData(
+ userId = USER_ID,
+ initialized = true,
+ backgroundColor = Color.DKGRAY,
+ app = APP_NAME,
+ appIcon = null,
+ artist = SESSION_ARTIST,
+ song = SESSION_TITLE,
+ artwork = null,
+ actions = emptyList(),
+ actionsToShowInCompact = emptyList(),
+ packageName = PACKAGE,
+ token = null,
+ clickIntent = null,
+ device = null,
+ active = true,
+ resumeAction = null,
+ resumption = false,
+ notificationKey = NOTIF_KEY,
+ hasCheckedForResume = false
+)
+
+private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+public class MediaSessionBasedFilterTest : SysuiTestCase() {
+
+ @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+ // Unit to be tested
+ private lateinit var filter: MediaSessionBasedFilter
+
+ private lateinit var sessionListener: MediaSessionManager.OnActiveSessionsChangedListener
+ @Mock private lateinit var mediaListener: MediaDataManager.Listener
+
+ // MediaSessionBasedFilter dependencies
+ @Mock private lateinit var mediaSessionManager: MediaSessionManager
+ private lateinit var fgExecutor: FakeExecutor
+ private lateinit var bgExecutor: FakeExecutor
+
+ @Mock private lateinit var controller1: MediaController
+ @Mock private lateinit var controller2: MediaController
+ @Mock private lateinit var controller3: MediaController
+ @Mock private lateinit var controller4: MediaController
+
+ private lateinit var token1: MediaSession.Token
+ private lateinit var token2: MediaSession.Token
+ private lateinit var token3: MediaSession.Token
+ private lateinit var token4: MediaSession.Token
+
+ @Mock private lateinit var remotePlaybackInfo: PlaybackInfo
+ @Mock private lateinit var localPlaybackInfo: PlaybackInfo
+
+ private lateinit var session1: MediaSession
+ private lateinit var session2: MediaSession
+ private lateinit var session3: MediaSession
+ private lateinit var session4: MediaSession
+
+ private lateinit var mediaData1: MediaData
+ private lateinit var mediaData2: MediaData
+ private lateinit var mediaData3: MediaData
+ private lateinit var mediaData4: MediaData
+
+ @Before
+ fun setUp() {
+ fgExecutor = FakeExecutor(FakeSystemClock())
+ bgExecutor = FakeExecutor(FakeSystemClock())
+ filter = MediaSessionBasedFilter(context, mediaSessionManager, fgExecutor, bgExecutor)
+
+ // Configure mocks.
+ whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(emptyList())
+
+ session1 = MediaSession(context, "MediaSessionBasedFilter1")
+ session2 = MediaSession(context, "MediaSessionBasedFilter2")
+ session3 = MediaSession(context, "MediaSessionBasedFilter3")
+ session4 = MediaSession(context, "MediaSessionBasedFilter4")
+
+ token1 = session1.sessionToken
+ token2 = session2.sessionToken
+ token3 = session3.sessionToken
+ token4 = session4.sessionToken
+
+ whenever(controller1.getSessionToken()).thenReturn(token1)
+ whenever(controller2.getSessionToken()).thenReturn(token2)
+ whenever(controller3.getSessionToken()).thenReturn(token3)
+ whenever(controller4.getSessionToken()).thenReturn(token4)
+
+ whenever(controller1.getPackageName()).thenReturn(PACKAGE)
+ whenever(controller2.getPackageName()).thenReturn(PACKAGE)
+ whenever(controller3.getPackageName()).thenReturn(PACKAGE)
+ whenever(controller4.getPackageName()).thenReturn(PACKAGE)
+
+ mediaData1 = info.copy(token = token1)
+ mediaData2 = info.copy(token = token2)
+ mediaData3 = info.copy(token = token3)
+ mediaData4 = info.copy(token = token4)
+
+ whenever(remotePlaybackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE)
+ whenever(localPlaybackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_LOCAL)
+
+ whenever(controller1.getPlaybackInfo()).thenReturn(localPlaybackInfo)
+ whenever(controller2.getPlaybackInfo()).thenReturn(localPlaybackInfo)
+ whenever(controller3.getPlaybackInfo()).thenReturn(localPlaybackInfo)
+ whenever(controller4.getPlaybackInfo()).thenReturn(localPlaybackInfo)
+
+ // Capture listener
+ bgExecutor.runAllReady()
+ val listenerCaptor = ArgumentCaptor.forClass(
+ MediaSessionManager.OnActiveSessionsChangedListener::class.java)
+ verify(mediaSessionManager).addOnActiveSessionsChangedListener(
+ listenerCaptor.capture(), any())
+ sessionListener = listenerCaptor.value
+
+ filter.addListener(mediaListener)
+ }
+
+ @After
+ fun tearDown() {
+ session1.release()
+ session2.release()
+ session3.release()
+ session4.release()
+ }
+
+ @Test
+ fun noMediaSession_loadedEventNotFiltered() {
+ filter.onMediaDataLoaded(KEY, null, mediaData1)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1))
+ }
+
+ @Test
+ fun noMediaSession_removedEventNotFiltered() {
+ filter.onMediaDataRemoved(KEY)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ verify(mediaListener).onMediaDataRemoved(eq(KEY))
+ }
+
+ @Test
+ fun matchingMediaSession_loadedEventNotFiltered() {
+ // GIVEN an active session
+ val controllers = listOf(controller1)
+ whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+ sessionListener.onActiveSessionsChanged(controllers)
+ // WHEN a loaded event is received that matches the session
+ filter.onMediaDataLoaded(KEY, null, mediaData1)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the event is not filtered
+ verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1))
+ }
+
+ @Test
+ fun matchingMediaSession_removedEventNotFiltered() {
+ // GIVEN an active session
+ val controllers = listOf(controller1)
+ whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+ sessionListener.onActiveSessionsChanged(controllers)
+ // WHEN a removed event is received
+ filter.onMediaDataRemoved(KEY)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the event is not filtered
+ verify(mediaListener).onMediaDataRemoved(eq(KEY))
+ }
+
+ @Test
+ fun remoteSession_loadedEventNotFiltered() {
+ // GIVEN a remove session
+ whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+ val controllers = listOf(controller1)
+ whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+ sessionListener.onActiveSessionsChanged(controllers)
+ // WHEN a loaded event is received that matche the session
+ filter.onMediaDataLoaded(KEY, null, mediaData1)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the event is not filtered
+ verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1))
+ }
+
+ @Test
+ fun remoteAndLocalSessions_localLoadedEventFiltered() {
+ // GIVEN remote and local sessions
+ whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+ val controllers = listOf(controller1, controller2)
+ whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+ sessionListener.onActiveSessionsChanged(controllers)
+ // WHEN a loaded event is received that matches the remote session
+ filter.onMediaDataLoaded(KEY, null, mediaData1)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the event is not filtered
+ verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1))
+ // WHEN a loaded event is received that matches the local session
+ filter.onMediaDataLoaded(KEY, null, mediaData2)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the event is filtered
+ verify(mediaListener, never()).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2))
+ }
+
+ @Test
+ fun remoteAndLocalHaveDifferentKeys_localLoadedEventFiltered() {
+ // GIVEN remote and local sessions
+ val key1 = "KEY_1"
+ val key2 = "KEY_2"
+ whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+ val controllers = listOf(controller1, controller2)
+ whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+ sessionListener.onActiveSessionsChanged(controllers)
+ // WHEN a loaded event is received that matches the remote session
+ filter.onMediaDataLoaded(key1, null, mediaData1)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the event is not filtered
+ verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1))
+ // WHEN a loaded event is received that matches the local session
+ filter.onMediaDataLoaded(key2, null, mediaData2)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the event is filtered
+ verify(mediaListener, never()).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2))
+ // AND there should be a removed event for key2
+ verify(mediaListener).onMediaDataRemoved(eq(key2))
+ }
+
+ @Test
+ fun multipleRemoteSessions_loadedEventNotFiltered() {
+ // GIVEN two remote sessions
+ whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+ whenever(controller2.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+ val controllers = listOf(controller1, controller2)
+ whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+ sessionListener.onActiveSessionsChanged(controllers)
+ // WHEN a loaded event is received that matches the remote session
+ filter.onMediaDataLoaded(KEY, null, mediaData1)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the event is not filtered
+ verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1))
+ // WHEN a loaded event is received that matches the local session
+ filter.onMediaDataLoaded(KEY, null, mediaData2)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the event is not filtered
+ verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2))
+ }
+
+ @Test
+ fun multipleOtherSessions_loadedEventNotFiltered() {
+ // GIVEN multiple active sessions from other packages
+ val controllers = listOf(controller1, controller2, controller3, controller4)
+ whenever(controller1.getPackageName()).thenReturn("PKG_1")
+ whenever(controller2.getPackageName()).thenReturn("PKG_2")
+ whenever(controller3.getPackageName()).thenReturn("PKG_3")
+ whenever(controller4.getPackageName()).thenReturn("PKG_4")
+ whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+ sessionListener.onActiveSessionsChanged(controllers)
+ // WHEN a loaded event is received
+ filter.onMediaDataLoaded(KEY, null, mediaData1)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the event is not filtered
+ verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1))
+ }
+
+ @Test
+ fun doNotFilterDuringKeyMigration() {
+ val key1 = "KEY_1"
+ val key2 = "KEY_2"
+ // GIVEN a loaded event
+ filter.onMediaDataLoaded(key1, null, mediaData2)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ reset(mediaListener)
+ // GIVEN remote and local sessions
+ whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+ val controllers = listOf(controller1, controller2)
+ whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+ sessionListener.onActiveSessionsChanged(controllers)
+ // WHEN a loaded event is received that matches the local session but it is a key migration
+ filter.onMediaDataLoaded(key2, key1, mediaData2)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the key migration event is fired
+ verify(mediaListener).onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2))
+ }
+
+ @Test
+ fun filterAfterKeyMigration() {
+ val key1 = "KEY_1"
+ val key2 = "KEY_2"
+ // GIVEN a loaded event
+ filter.onMediaDataLoaded(key1, null, mediaData1)
+ filter.onMediaDataLoaded(key1, null, mediaData2)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ reset(mediaListener)
+ // GIVEN remote and local sessions
+ whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+ val controllers = listOf(controller1, controller2)
+ whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+ sessionListener.onActiveSessionsChanged(controllers)
+ // GIVEN that the keys have been migrated
+ filter.onMediaDataLoaded(key2, key1, mediaData1)
+ filter.onMediaDataLoaded(key2, key1, mediaData2)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ reset(mediaListener)
+ // WHEN a loaded event is received that matches the local session
+ filter.onMediaDataLoaded(key2, null, mediaData2)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the key migration event is filtered
+ verify(mediaListener, never()).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2))
+ // WHEN a loaded event is received that matches the remote session
+ filter.onMediaDataLoaded(key2, null, mediaData1)
+ bgExecutor.runAllReady()
+ fgExecutor.runAllReady()
+ // THEN the key migration event is fired
+ verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1))
+ }
+}