1 package com.android.systemui.media
2
3 import android.app.Notification.MediaStyle
4 import android.app.PendingIntent
5 import android.app.smartspace.SmartspaceAction
6 import android.app.smartspace.SmartspaceTarget
7 import android.graphics.Bitmap
8 import android.media.MediaDescription
9 import android.media.MediaMetadata
10 import android.media.session.MediaController
11 import android.media.session.MediaSession
12 import android.os.Bundle
13 import android.provider.Settings
14 import android.service.notification.StatusBarNotification
15 import android.testing.AndroidTestingRunner
16 import android.testing.TestableLooper.RunWithLooper
17 import androidx.test.filters.SmallTest
18 import com.android.systemui.SysuiTestCase
19 import com.android.systemui.broadcast.BroadcastDispatcher
20 import com.android.systemui.dump.DumpManager
21 import com.android.systemui.plugins.ActivityStarter
22 import com.android.systemui.statusbar.SbnBuilder
23 import com.android.systemui.tuner.TunerService
24 import com.android.systemui.util.concurrency.FakeExecutor
25 import com.android.systemui.util.mockito.capture
26 import com.android.systemui.util.mockito.eq
27 import com.android.systemui.util.time.FakeSystemClock
28 import com.google.common.truth.Truth.assertThat
29 import org.junit.After
30 import org.junit.Before
31 import org.junit.Rule
32 import org.junit.Test
33 import org.junit.runner.RunWith
34 import org.mockito.ArgumentCaptor
35 import org.mockito.ArgumentMatchers.anyBoolean
36 import org.mockito.Captor
37 import org.mockito.Mock
38 import org.mockito.Mockito
39 import org.mockito.Mockito.mock
40 import org.mockito.Mockito.never
41 import org.mockito.Mockito.reset
42 import org.mockito.Mockito.verify
43 import org.mockito.junit.MockitoJUnit
44 import org.mockito.Mockito.`when` as whenever
45
46 private const val KEY = "KEY"
47 private const val KEY_2 = "KEY_2"
48 private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
49 private const val PACKAGE_NAME = "com.android.systemui"
50 private const val APP_NAME = "SystemUI"
51 private const val SESSION_ARTIST = "artist"
52 private const val SESSION_TITLE = "title"
53 private const val USER_ID = 0
54
anyObjectnull55 private fun <T> anyObject(): T {
56 return Mockito.anyObject<T>()
57 }
58
59 @SmallTest
60 @RunWithLooper(setAsMainLooper = true)
61 @RunWith(AndroidTestingRunner::class)
62 class MediaDataManagerTest : SysuiTestCase() {
63
64 @JvmField @Rule val mockito = MockitoJUnit.rule()
65 @Mock lateinit var mediaControllerFactory: MediaControllerFactory
66 @Mock lateinit var controller: MediaController
67 @Mock lateinit var playbackInfo: MediaController.PlaybackInfo
68 lateinit var session: MediaSession
69 lateinit var metadataBuilder: MediaMetadata.Builder
70 lateinit var backgroundExecutor: FakeExecutor
71 lateinit var foregroundExecutor: FakeExecutor
72 @Mock lateinit var dumpManager: DumpManager
73 @Mock lateinit var broadcastDispatcher: BroadcastDispatcher
74 @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener
75 @Mock lateinit var mediaResumeListener: MediaResumeListener
76 @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter
77 @Mock lateinit var mediaDeviceManager: MediaDeviceManager
78 @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
79 @Mock lateinit var mediaDataFilter: MediaDataFilter
80 @Mock lateinit var listener: MediaDataManager.Listener
81 @Mock lateinit var pendingIntent: PendingIntent
82 @Mock lateinit var activityStarter: ActivityStarter
83 lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
84 @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
85 @Mock private lateinit var mediaRecommendationItem: SmartspaceAction
86 lateinit var mediaDataManager: MediaDataManager
87 lateinit var mediaNotification: StatusBarNotification
88 @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
89 private val clock = FakeSystemClock()
90 @Mock private lateinit var tunerService: TunerService
91 @Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
92
93 private val originalSmartspaceSetting = Settings.Secure.getInt(context.contentResolver,
94 Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
95
96 @Before
setupnull97 fun setup() {
98 foregroundExecutor = FakeExecutor(clock)
99 backgroundExecutor = FakeExecutor(clock)
100 smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
101 Settings.Secure.putInt(context.contentResolver,
102 Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
103 mediaDataManager = MediaDataManager(
104 context = context,
105 backgroundExecutor = backgroundExecutor,
106 foregroundExecutor = foregroundExecutor,
107 mediaControllerFactory = mediaControllerFactory,
108 broadcastDispatcher = broadcastDispatcher,
109 dumpManager = dumpManager,
110 mediaTimeoutListener = mediaTimeoutListener,
111 mediaResumeListener = mediaResumeListener,
112 mediaSessionBasedFilter = mediaSessionBasedFilter,
113 mediaDeviceManager = mediaDeviceManager,
114 mediaDataCombineLatest = mediaDataCombineLatest,
115 mediaDataFilter = mediaDataFilter,
116 activityStarter = activityStarter,
117 smartspaceMediaDataProvider = smartspaceMediaDataProvider,
118 useMediaResumption = true,
119 useQsMediaPlayer = true,
120 systemClock = clock,
121 tunerService = tunerService
122 )
123 verify(tunerService).addTunable(capture(tunableCaptor),
124 eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
125 session = MediaSession(context, "MediaDataManagerTestSession")
126 mediaNotification = SbnBuilder().run {
127 setPkg(PACKAGE_NAME)
128 modifyNotification(context).also {
129 it.setSmallIcon(android.R.drawable.ic_media_pause)
130 it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
131 }
132 build()
133 }
134 metadataBuilder = MediaMetadata.Builder().apply {
135 putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
136 putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
137 }
138 whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller)
139 whenever(controller.playbackInfo).thenReturn(playbackInfo)
140 whenever(playbackInfo.playbackType).thenReturn(
141 MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL)
142
143 // This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal
144 // listeners in the internal processing pipeline. It receives events, but ince it is a
145 // mock, it doesn't pass those events along the chain to the external listeners. So, just
146 // treat mediaSessionBasedFilter as a listener for testing.
147 listener = mediaSessionBasedFilter
148
149 val recommendationExtras = Bundle()
150 recommendationExtras.putString("package_name", PACKAGE_NAME)
151 whenever(mediaRecommendationItem.extras).thenReturn(recommendationExtras)
152 whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE)
153 whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
154 whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf(mediaRecommendationItem))
155 }
156
157 @After
tearDownnull158 fun tearDown() {
159 session.release()
160 mediaDataManager.destroy()
161 Settings.Secure.putInt(context.contentResolver,
162 Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, originalSmartspaceSetting)
163 }
164
165 @Test
testSetTimedOut_deactivatesMedianull166 fun testSetTimedOut_deactivatesMedia() {
167 val data = MediaData(userId = USER_ID, initialized = true, backgroundColor = 0, app = null,
168 appIcon = null, artist = null, song = null, artwork = null, actions = emptyList(),
169 actionsToShowInCompact = emptyList(), packageName = "INVALID", token = null,
170 clickIntent = null, device = null, active = true, resumeAction = null)
171 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
172 mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data)
173
174 mediaDataManager.setTimedOut(KEY, timedOut = true)
175 assertThat(data.active).isFalse()
176 }
177
178 @Test
testLoadsMetadataOnBackgroundnull179 fun testLoadsMetadataOnBackground() {
180 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
181 assertThat(backgroundExecutor.numPending()).isEqualTo(1)
182 }
183
184 @Test
testOnMetaDataLoaded_callsListenernull185 fun testOnMetaDataLoaded_callsListener() {
186 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
187 mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
188 verify(listener).onMediaDataLoaded(eq(KEY), eq(null), anyObject(), eq(true),
189 eq(false))
190 }
191
192 @Test
testOnMetaDataLoaded_conservesActiveFlagnull193 fun testOnMetaDataLoaded_conservesActiveFlag() {
194 whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
195 whenever(controller.metadata).thenReturn(metadataBuilder.build())
196 mediaDataManager.addListener(listener)
197 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
198 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
199 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
200 verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
201 eq(false))
202 assertThat(mediaDataCaptor.value!!.active).isTrue()
203 }
204
205 @Test
testOnNotificationRemoved_callsListenernull206 fun testOnNotificationRemoved_callsListener() {
207 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
208 mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
209 mediaDataManager.onNotificationRemoved(KEY)
210 verify(listener).onMediaDataRemoved(eq(KEY))
211 }
212
213 @Test
testOnNotificationRemoved_withResumptionnull214 fun testOnNotificationRemoved_withResumption() {
215 // GIVEN that the manager has a notification with a resume action
216 whenever(controller.metadata).thenReturn(metadataBuilder.build())
217 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
218 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
219 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
220 verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
221 eq(false))
222 val data = mediaDataCaptor.value
223 assertThat(data.resumption).isFalse()
224 mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
225 // WHEN the notification is removed
226 mediaDataManager.onNotificationRemoved(KEY)
227 // THEN the media data indicates that it is for resumption
228 verify(listener)
229 .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true),
230 eq(false))
231 assertThat(mediaDataCaptor.value.resumption).isTrue()
232 }
233
234 @Test
testOnNotificationRemoved_twoWithResumptionnull235 fun testOnNotificationRemoved_twoWithResumption() {
236 // GIVEN that the manager has two notifications with resume actions
237 whenever(controller.metadata).thenReturn(metadataBuilder.build())
238 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
239 mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
240 assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
241 assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
242 verify(listener)
243 .onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
244 eq(false))
245 val data = mediaDataCaptor.value
246 assertThat(data.resumption).isFalse()
247 val resumableData = data.copy(resumeAction = Runnable {})
248 mediaDataManager.onMediaDataLoaded(KEY, null, resumableData)
249 mediaDataManager.onMediaDataLoaded(KEY_2, null, resumableData)
250 reset(listener)
251 // WHEN the first is removed
252 mediaDataManager.onNotificationRemoved(KEY)
253 // THEN the data is for resumption and the key is migrated to the package name
254 verify(listener)
255 .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true),
256 eq(false))
257 assertThat(mediaDataCaptor.value.resumption).isTrue()
258 verify(listener, never()).onMediaDataRemoved(eq(KEY))
259 // WHEN the second is removed
260 mediaDataManager.onNotificationRemoved(KEY_2)
261 // THEN the data is for resumption and the second key is removed
262 verify(listener)
263 .onMediaDataLoaded(
264 eq(PACKAGE_NAME), eq(PACKAGE_NAME), capture(mediaDataCaptor), eq(true),
265 eq(false))
266 assertThat(mediaDataCaptor.value.resumption).isTrue()
267 verify(listener).onMediaDataRemoved(eq(KEY_2))
268 }
269
270 @Test
testOnNotificationRemoved_withResumption_butNotLocalnull271 fun testOnNotificationRemoved_withResumption_butNotLocal() {
272 // GIVEN that the manager has a notification with a resume action, but is not local
273 whenever(controller.metadata).thenReturn(metadataBuilder.build())
274 whenever(playbackInfo.playbackType).thenReturn(
275 MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
276 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
277 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
278 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
279 verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
280 eq(false))
281 val data = mediaDataCaptor.value
282 val dataRemoteWithResume = data.copy(resumeAction = Runnable {}, isLocalSession = false)
283 mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
284
285 // WHEN the notification is removed
286 mediaDataManager.onNotificationRemoved(KEY)
287
288 // THEN the media data is removed
289 verify(listener).onMediaDataRemoved(eq(KEY))
290 }
291
292 @Test
testAddResumptionControlsnull293 fun testAddResumptionControls() {
294 // WHEN resumption controls are added
295 val desc = MediaDescription.Builder().run {
296 setTitle(SESSION_TITLE)
297 build()
298 }
299 val currentTime = clock.elapsedRealtime()
300 mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken,
301 APP_NAME, pendingIntent, PACKAGE_NAME)
302 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
303 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
304 // THEN the media data indicates that it is for resumption
305 verify(listener)
306 .onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor), eq(true),
307 eq(false))
308 val data = mediaDataCaptor.value
309 assertThat(data.resumption).isTrue()
310 assertThat(data.song).isEqualTo(SESSION_TITLE)
311 assertThat(data.app).isEqualTo(APP_NAME)
312 assertThat(data.actions).hasSize(1)
313 assertThat(data.lastActive).isAtLeast(currentTime)
314 }
315
316 @Test
testDismissMedia_listenerCallednull317 fun testDismissMedia_listenerCalled() {
318 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
319 mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
320 val removed = mediaDataManager.dismissMediaData(KEY, 0L)
321 assertThat(removed).isTrue()
322
323 foregroundExecutor.advanceClockToLast()
324 foregroundExecutor.runAllReady()
325
326 verify(listener).onMediaDataRemoved(eq(KEY))
327 }
328
329 @Test
testDismissMedia_keyDoesNotExist_returnsFalsenull330 fun testDismissMedia_keyDoesNotExist_returnsFalse() {
331 val removed = mediaDataManager.dismissMediaData(KEY, 0L)
332 assertThat(removed).isFalse()
333 }
334
335 @Test
testBadArtwork_doesNotUsenull336 fun testBadArtwork_doesNotUse() {
337 // WHEN notification has a too-small artwork
338 val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
339 val notif = SbnBuilder().run {
340 setPkg(PACKAGE_NAME)
341 modifyNotification(context).also {
342 it.setSmallIcon(android.R.drawable.ic_media_pause)
343 it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
344 it.setLargeIcon(artwork)
345 }
346 build()
347 }
348 mediaDataManager.onNotificationAdded(KEY, notif)
349
350 // THEN it still loads
351 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
352 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
353 verify(listener)
354 .onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
355 eq(false))
356 }
357
358 @Test
testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListenernull359 fun testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListener() {
360 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
361 verify(listener).onSmartspaceMediaDataLoaded(
362 eq(KEY_MEDIA_SMARTSPACE),
363 eq(SmartspaceMediaData(KEY_MEDIA_SMARTSPACE, true /* isActive */, true /*isValid */,
364 PACKAGE_NAME, null, listOf(mediaRecommendationItem), 0)),
365 eq(false))
366 }
367
368 @Test
testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListenernull369 fun testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListener() {
370 whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
371 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
372 verify(listener).onSmartspaceMediaDataLoaded(
373 eq(KEY_MEDIA_SMARTSPACE),
374 eq(EMPTY_SMARTSPACE_MEDIA_DATA
375 .copy(targetId = KEY_MEDIA_SMARTSPACE, isActive = true, isValid = false)),
376 eq(false))
377 }
378
379 @Test
testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListenernull380 fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() {
381 smartspaceMediaDataProvider.onTargetsAvailable(listOf())
382 verify(listener, never())
383 .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
384 }
385
386 @Test
testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListenernull387 fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() {
388 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
389 smartspaceMediaDataProvider.onTargetsAvailable(listOf())
390 foregroundExecutor.advanceClockToLast()
391 foregroundExecutor.runAllReady()
392
393 verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
394 }
395
396 @Test
testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothingnull397 fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
398 // WHEN media recommendation setting is off
399 Settings.Secure.putInt(context.contentResolver,
400 Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
401 tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
402
403 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
404
405 // THEN smartspace signal is ignored
406 verify(listener, never())
407 .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
408 }
409
410 @Test
testMediaRecommendationDisabled_removesSmartspaceDatanull411 fun testMediaRecommendationDisabled_removesSmartspaceData() {
412 // GIVEN a media recommendation card is present
413 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
414 verify(listener).onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(),
415 anyBoolean())
416
417 // WHEN the media recommendation setting is turned off
418 Settings.Secure.putInt(context.contentResolver,
419 Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
420 tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
421
422 // THEN listeners are notified
423 foregroundExecutor.advanceClockToLast()
424 foregroundExecutor.runAllReady()
425 verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(true))
426 }
427
428 @Test
testOnMediaDataChanged_updatesLastActiveTimenull429 fun testOnMediaDataChanged_updatesLastActiveTime() {
430 val currentTime = clock.elapsedRealtime()
431 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
432 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
433 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
434 verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
435 eq(false))
436 assertThat(mediaDataCaptor.value!!.lastActive).isAtLeast(currentTime)
437 }
438
439 @Test
testOnMediaDataTimedOut_doesNotUpdateLastActiveTimenull440 fun testOnMediaDataTimedOut_doesNotUpdateLastActiveTime() {
441 // GIVEN that the manager has a notification
442 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
443 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
444 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
445
446 // WHEN the notification times out
447 clock.advanceTime(100)
448 val currentTime = clock.elapsedRealtime()
449 mediaDataManager.setTimedOut(KEY, true, true)
450
451 // THEN the last active time is not changed
452 verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), capture(mediaDataCaptor), eq(true),
453 eq(false))
454 assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime)
455 }
456
457 @Test
testOnActiveMediaConverted_doesNotUpdateLastActiveTimenull458 fun testOnActiveMediaConverted_doesNotUpdateLastActiveTime() {
459 // GIVEN that the manager has a notification with a resume action
460 whenever(controller.metadata).thenReturn(metadataBuilder.build())
461 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
462 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
463 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
464 verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
465 eq(false))
466 val data = mediaDataCaptor.value
467 assertThat(data.resumption).isFalse()
468 mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
469
470 // WHEN the notification is removed
471 clock.advanceTime(100)
472 val currentTime = clock.elapsedRealtime()
473 mediaDataManager.onNotificationRemoved(KEY)
474
475 // THEN the last active time is not changed
476 verify(listener)
477 .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true),
478 eq(false))
479 assertThat(mediaDataCaptor.value.resumption).isTrue()
480 assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime)
481 }
482
483 @Test
testTooManyCompactActions_isTruncatednull484 fun testTooManyCompactActions_isTruncated() {
485 // GIVEN a notification where too many compact actions were specified
486 val notif = SbnBuilder().run {
487 setPkg(PACKAGE_NAME)
488 modifyNotification(context).also {
489 it.setSmallIcon(android.R.drawable.ic_media_pause)
490 it.setStyle(MediaStyle().apply {
491 setMediaSession(session.sessionToken)
492 setShowActionsInCompactView(0, 1, 2, 3, 4)
493 })
494 }
495 build()
496 }
497
498 // WHEN the notification is loaded
499 mediaDataManager.onNotificationAdded(KEY, notif)
500 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
501 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
502
503 // THEN only the first MAX_COMPACT_ACTIONS are actually set
504 verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
505 eq(false))
506 assertThat(mediaDataCaptor.value.actionsToShowInCompact.size).isEqualTo(
507 MediaDataManager.MAX_COMPACT_ACTIONS)
508 }
509 }
510