diff options
11 files changed, 271 insertions, 5 deletions
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 657435326d15..a2359350243a 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -70,6 +70,7 @@ android_library { "res", ], static_libs: [ + "bcsmartspace", "WindowManager-Shell", "SystemUIPluginLib", "SystemUISharedLib", diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 42d7e587b668..0b56f63d4ddb 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -275,6 +275,9 @@ <!-- Permission to make accessibility service access Bubbles --> <uses-permission android:name="android.permission.ADD_TRUSTED_DISPLAY" /> + <!-- Permission for Smartspace. --> + <uses-permission android:name="android.permission.MANAGE_SMARTSPACE"/> + <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" /> diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index a3ff3753cbf9..33681c8d03e5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -1,6 +1,7 @@ package com.android.systemui.media import android.animation.ArgbEvaluator +import android.app.smartspace.SmartspaceTarget import android.content.Context import android.content.Intent import android.content.res.ColorStateList @@ -201,9 +202,18 @@ class MediaCarouselController @Inject constructor( } } + override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) { + Log.d(TAG, "My Smartspace media update is here") + addOrUpdateSmartspaceMediaRecommendations(key, data) + } + override fun onMediaDataRemoved(key: String) { removePlayer(key) } + + override fun onSmartspaceMediaDataRemoved(key: String) { + Log.d(TAG, "My Smartspace media removal request is received") + } }) mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> // The pageIndicator is not laid out yet when we get the current state update, @@ -291,6 +301,10 @@ class MediaCarouselController @Inject constructor( } } + private fun addOrUpdateSmartspaceMediaRecommendations(key: String, data: SmartspaceTarget) { + // TODO(b/182813345): Add Smartspace media recommendation view. + } + private fun removePlayer(key: String, dismissMediaData: Boolean = true) { val removed = MediaPlayerData.removeMediaPlayer(key) removed?.apply { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt index aa3699e9a22b..2c094b885032 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt @@ -16,6 +16,7 @@ package com.android.systemui.media +import android.app.smartspace.SmartspaceTarget import javax.inject.Inject /** @@ -37,10 +38,18 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, } } + override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) { + listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, data) } + } + override fun onMediaDataRemoved(key: String) { remove(key) } + override fun onSmartspaceMediaDataRemoved(key: String) { + listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key) } + } + override fun onMediaDeviceChanged( key: String, oldKey: String?, diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt index 1f580a953d09..aab27473d3e8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt @@ -16,6 +16,7 @@ package com.android.systemui.media +import android.app.smartspace.SmartspaceTarget import android.util.Log import com.android.internal.annotations.VisibleForTesting import com.android.systemui.broadcast.BroadcastDispatcher @@ -82,6 +83,10 @@ class MediaDataFilter @Inject constructor( } } + override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) { + listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data) } + } + override fun onMediaDataRemoved(key: String) { allEntries.remove(key) userEntries.remove(key)?.let { @@ -92,6 +97,10 @@ class MediaDataFilter @Inject constructor( } } + override fun onSmartspaceMediaDataRemoved(key: String) { + listeners.forEach { it.onSmartspaceMediaDataRemoved(key) } + } + @VisibleForTesting internal fun handleUserSwitched(id: Int) { // If the user changes, remove all current MediaData objects and inform listeners diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index 41c9daedf2d4..dfd588d0406e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -18,6 +18,10 @@ package com.android.systemui.media import android.app.Notification import android.app.PendingIntent +import android.app.smartspace.SmartspaceConfig +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceSession +import android.app.smartspace.SmartspaceTarget import android.content.BroadcastReceiver import android.content.ContentResolver import android.content.Context @@ -33,6 +37,7 @@ import android.media.MediaMetadata import android.media.session.MediaController import android.media.session.MediaSession import android.net.Uri +import android.os.Parcelable import android.os.UserHandle import android.service.notification.StatusBarNotification import android.text.TextUtils @@ -45,6 +50,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.notification.row.HybridGroupManager import com.android.systemui.util.Assert @@ -54,6 +60,7 @@ import java.io.FileDescriptor import java.io.IOException import java.io.PrintWriter import java.util.concurrent.Executor +import java.util.concurrent.Executors import javax.inject.Inject // URI fields to try loading album art from @@ -99,9 +106,16 @@ class MediaDataManager( mediaDataCombineLatest: MediaDataCombineLatest, private val mediaDataFilter: MediaDataFilter, private val activityStarter: ActivityStarter, + private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider, private var useMediaResumption: Boolean, private val useQsMediaPlayer: Boolean -) : Dumpable { +) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener { + + companion object { + // UI surface label for subscribing Smartspace updates. + @JvmField + val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" + } private val themeText = com.android.settingslib.Utils.getColorAttr(context, com.android.internal.R.attr.textColorPrimary).defaultColor @@ -117,6 +131,8 @@ class MediaDataManager( // TODO(b/159539991#comment5): Move internal listeners to separate package. private val internalListeners: MutableSet<Listener> = mutableSetOf() private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() + // There should ONLY be at most one Smartspace media recommendation. + private var smartspaceMediaTarget: SmartspaceTarget? = null internal var appsBlockedFromResume: MutableSet<String> = Utils.getBlockedMediaApps(context) set(value) { // Update list @@ -128,6 +144,7 @@ class MediaDataManager( removeAllForPackage(it) } } + private var smartspaceSession: SmartspaceSession? = null @Inject constructor( @@ -143,11 +160,13 @@ class MediaDataManager( mediaDeviceManager: MediaDeviceManager, mediaDataCombineLatest: MediaDataCombineLatest, mediaDataFilter: MediaDataFilter, - activityStarter: ActivityStarter + activityStarter: ActivityStarter, + smartspaceMediaDataProvider: SmartspaceMediaDataProvider ) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory, broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener, mediaSessionBasedFilter, mediaDeviceManager, mediaDataCombineLatest, mediaDataFilter, - activityStarter, Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context)) + activityStarter, smartspaceMediaDataProvider, Utils.useMediaResumption(context), + Utils.useQsMediaPlayer(context)) private val appChangeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -200,9 +219,31 @@ class MediaDataManager( } // BroadcastDispatcher does not allow filters with data schemes context.registerReceiver(appChangeReceiver, uninstallFilter) + + // Register for Smartspace data updates. + smartspaceMediaDataProvider.registerListener(this) + val smartspaceManager: SmartspaceManager = + context.getSystemService(SmartspaceManager::class.java) + smartspaceSession = smartspaceManager.createSmartspaceSession( + SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()) + smartspaceSession?.let { + it.registerSmartspaceUpdates( + // Use a new thread listening to Smartspace updates instead of using the existing + // backgroundExecutor. SmartspaceSession has scheduled routine updates which can be + // unpredictable on test simulators, using the backgroundExecutor makes it's hard to + // test the threads numbers. + // Switch to use backgroundExecutor when SmartspaceSession has a good way to be + // mocked. + Executors.newCachedThreadPool(), + SmartspaceSession.Callback { targets -> + smartspaceMediaDataProvider.onTargetsAvailable(targets) + }) + } + smartspaceSession?.let { it.requestSmartspaceUpdate() } } fun destroy() { + smartspaceMediaDataProvider.unregisterListener(this) context.unregisterReceiver(appChangeReceiver) } @@ -309,7 +350,7 @@ class MediaDataManager( private fun addInternalListener(listener: Listener) = internalListeners.add(listener) /** - * Notify internal listeners of loaded event. + * Notify internal listeners of media loaded event. * * External listeners registered with [addListener] will be notified after the event propagates * through the internal listener pipeline. @@ -319,7 +360,17 @@ class MediaDataManager( } /** - * Notify internal listeners of removed event. + * Notify internal listeners of Smartspace media loaded event. + * + * External listeners registered with [addListener] will be notified after the event propagates + * through the internal listener pipeline. + */ + private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceTarget) { + internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) } + } + + /** + * Notify internal listeners of media removed event. * * External listeners registered with [addListener] will be notified after the event propagates * through the internal listener pipeline. @@ -329,6 +380,16 @@ class MediaDataManager( } /** + * Notify internal listeners of Smartspace media removed event. + * + * External listeners registered with [addListener] will be notified after the event propagates + * through the internal listener pipeline. + */ + private fun notifySmartspaceMediaDataRemoved(key: String) { + internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key) } + } + + /** * Called whenever the player has been paused or stopped for a while, or swiped from QQS. * This will make the player not active anymore, hiding it from QQS and Keyguard. * @see MediaData.active @@ -601,6 +662,49 @@ class MediaDataManager( } } + override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) { + Log.d(TAG, "My Smartspace media updates are here") + val mediaTargets = targets.filterIsInstance<SmartspaceTarget>() + when (mediaTargets.size) { + 0 -> { + Log.d(TAG, "Empty Smartspace media target") + smartspaceMediaTarget?.let { + notifySmartspaceMediaDataRemoved(it.smartspaceTargetId) + } + smartspaceMediaTarget = null + } + 1 -> { + // TODO(b/182811956): Reactivate the resumable media sessions whose last active + // time is within 3 hours. + // TODO(b/182813365): Wire this up with MediaTimeoutListener so the session can be + // expired after 30 seconds. + val newMediaTarget = mediaTargets.get(0) + if (smartspaceMediaTarget != null && + smartspaceMediaTarget!!.smartspaceTargetId == + newMediaTarget.smartspaceTargetId) { + // The same Smartspace updates can be received. Only send the first one. + Log.d(TAG, "Same Smartspace media update exists. Skip loading data.") + } else { + smartspaceMediaTarget?.let { + notifySmartspaceMediaDataRemoved(it.smartspaceTargetId) + } + notifySmartspaceMediaDataLoaded( + newMediaTarget.smartspaceTargetId, newMediaTarget) + smartspaceMediaTarget = newMediaTarget + } + } + else -> { + // There should NOT be more than 1 Smartspace media update. When it happens, it + // indicates a bad state or an error. Reset the status accordingly. + Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") + smartspaceMediaTarget?.let { + notifySmartspaceMediaDataRemoved(it.smartspaceTargetId) + } + smartspaceMediaTarget = null + } + } + } + fun onNotificationRemoved(key: String) { Assert.isMainThread() val removed = mediaEntries.remove(key) @@ -685,10 +789,16 @@ class MediaDataManager( */ fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {} + /** Called whenever there's new Smartspace media data loaded. */ + fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) {} + /** * Called whenever a previously existing Media notification was removed */ fun onMediaDataRemoved(key: String) {} + + /** Called whenever a previously existing Smartspace media data was removed. */ + fun onSmartspaceMediaDataRemoved(key: String) {} } override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index b74ca283f16a..8c12a305992e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -1,5 +1,6 @@ package com.android.systemui.media +import android.app.smartspace.SmartspaceTarget import android.graphics.Rect import android.util.ArraySet import android.view.View @@ -53,9 +54,17 @@ class MediaHost constructor( updateViewVisibility() } + override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) { + updateViewVisibility() + } + override fun onMediaDataRemoved(key: String) { updateViewVisibility() } + + override fun onSmartspaceMediaDataRemoved(key: String) { + updateViewVisibility() + } } fun addVisibilityChangeListener(listener: (Boolean) -> Unit) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt index f695622b943a..d973478625de 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt @@ -16,6 +16,7 @@ package com.android.systemui.media +import android.app.smartspace.SmartspaceTarget import android.content.ComponentName import android.content.Context import android.media.session.MediaController @@ -134,6 +135,12 @@ class MediaSessionBasedFilter @Inject constructor( } } + override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) { + backgroundExecutor.execute { + dispatchSmartspaceMediaDataLoaded(key, data) + } + } + override fun onMediaDataRemoved(key: String) { // Queue on background thread to ensure ordering of loaded and removed events is maintained. backgroundExecutor.execute { @@ -142,6 +149,12 @@ class MediaSessionBasedFilter @Inject constructor( } } + override fun onSmartspaceMediaDataRemoved(key: String) { + backgroundExecutor.execute { + dispatchSmartspaceMediaDataRemoved(key) + } + } + private fun dispatchMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { foregroundExecutor.execute { listeners.toSet().forEach { it.onMediaDataLoaded(key, oldKey, info) } @@ -154,6 +167,18 @@ class MediaSessionBasedFilter @Inject constructor( } } + private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceTarget) { + foregroundExecutor.execute { + listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, info) } + } + } + + private fun dispatchSmartspaceMediaDataRemoved(key: String) { + foregroundExecutor.execute { + listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key) } + } + } + private fun handleControllersChanged(controllers: List<MediaController>) { packageControllers.clear() controllers.forEach { diff --git a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt b/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt new file mode 100644 index 000000000000..0eab572f315f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt @@ -0,0 +1,39 @@ +package com.android.systemui.media + +import android.app.smartspace.SmartspaceTarget +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener +import javax.inject.Inject + +/** Provides SmartspaceTargets of media types for SystemUI media control. */ +class SmartspaceMediaDataProvider @Inject constructor() : BcSmartspaceDataPlugin { + + private val smartspaceMediaTargetListeners: MutableList<SmartspaceTargetListener> = + mutableListOf() + private var smartspaceMediaTargets: List<SmartspaceTarget> = listOf() + + override fun registerListener(smartspaceTargetListener: SmartspaceTargetListener) { + smartspaceMediaTargetListeners.add(smartspaceTargetListener) + } + + override fun unregisterListener(smartspaceTargetListener: SmartspaceTargetListener?) { + smartspaceMediaTargetListeners.remove(smartspaceTargetListener) + } + + /** Updates Smartspace data and propagates it to any listeners. */ + fun onTargetsAvailable(targets: List<SmartspaceTarget>) { + // Filter out non-media targets. + val mediaTargets = mutableListOf<SmartspaceTarget>() + for (target in targets) { + val smartspaceTarget = target + if (smartspaceTarget.featureType == SmartspaceTarget.FEATURE_MEDIA) { + mediaTargets.add(smartspaceTarget) + } + } + + smartspaceMediaTargets = mediaTargets + smartspaceMediaTargetListeners.forEach { + it.onSmartspaceTargetsUpdated(smartspaceMediaTargets) + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index c565a271bdd7..5782d38a9281 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -24,6 +24,7 @@ import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Notification; +import android.app.smartspace.SmartspaceTarget; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; @@ -248,6 +249,11 @@ public class NotificationMediaManager implements Dumpable { } @Override + public void onSmartspaceMediaDataLoaded(@NonNull String key, + @NonNull SmartspaceTarget data) { + } + + @Override public void onMediaDataRemoved(@NonNull String key) { mNotifPipeline.getAllNotifs() .stream() @@ -260,6 +266,9 @@ public class NotificationMediaManager implements Dumpable { getDismissedByUserStats(entry)); }); } + + @Override + public void onSmartspaceMediaDataRemoved(@NonNull String key) {} }); } @@ -313,6 +322,11 @@ public class NotificationMediaManager implements Dumpable { } @Override + public void onSmartspaceMediaDataLoaded(@NonNull String key, + @NonNull SmartspaceTarget data) { + } + + @Override public void onMediaDataRemoved(@NonNull String key) { NotificationEntry entry = mEntryManager.getPendingOrActiveNotif(key); if (entry != null) { @@ -323,6 +337,9 @@ public class NotificationMediaManager implements Dumpable { NotificationListenerService.REASON_CANCEL); } } + + @Override + public void onSmartspaceMediaDataRemoved(@NonNull String key) {} }); } 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 e88c72860ed8..96eb4b096931 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt @@ -2,6 +2,7 @@ package com.android.systemui.media import android.app.Notification.MediaStyle import android.app.PendingIntent +import android.app.smartspace.SmartspaceTarget import android.graphics.Bitmap import android.media.MediaDescription import android.media.MediaMetadata @@ -39,6 +40,8 @@ import org.mockito.Mockito.`when` as whenever private const val KEY = "KEY" private const val KEY_2 = "KEY_2" +private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" +private const val KEY_NONE_MEDIA_SMARTSPACE = "NONE_MEDIA_SMARTSPACE_ID" private const val PACKAGE_NAME = "com.android.systemui" private const val APP_NAME = "SystemUI" private const val SESSION_ARTIST = "artist" @@ -72,6 +75,8 @@ class MediaDataManagerTest : SysuiTestCase() { @Mock lateinit var listener: MediaDataManager.Listener @Mock lateinit var pendingIntent: PendingIntent @Mock lateinit var activityStarter: ActivityStarter + lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider + @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget lateinit var mediaDataManager: MediaDataManager lateinit var mediaNotification: StatusBarNotification @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData> @@ -80,6 +85,7 @@ class MediaDataManagerTest : SysuiTestCase() { fun setup() { foregroundExecutor = FakeExecutor(FakeSystemClock()) backgroundExecutor = FakeExecutor(FakeSystemClock()) + smartspaceMediaDataProvider = SmartspaceMediaDataProvider() mediaDataManager = MediaDataManager( context = context, backgroundExecutor = backgroundExecutor, @@ -94,6 +100,7 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataCombineLatest = mediaDataCombineLatest, mediaDataFilter = mediaDataFilter, activityStarter = activityStarter, + smartspaceMediaDataProvider = smartspaceMediaDataProvider, useMediaResumption = true, useQsMediaPlayer = true ) @@ -117,6 +124,9 @@ class MediaDataManagerTest : SysuiTestCase() { // 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 + + whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE) + whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA) } @After @@ -320,4 +330,24 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor)) } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNewMediaTarget_callsListener() { + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + verify(listener).onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), eq(mediaSmartspaceTarget)) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() { + smartspaceMediaDataProvider.onTargetsAvailable(listOf()) + verify(listener, never()).onSmartspaceMediaDataLoaded(anyObject(), anyObject()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() { + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + smartspaceMediaDataProvider.onTargetsAvailable(listOf()) + verify(listener).onSmartspaceMediaDataRemoved(KEY_MEDIA_SMARTSPACE) + } } |