1 /*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.media.controls.pipeline
18
19 import android.app.Notification
20 import android.app.Notification.FLAG_NO_CLEAR
21 import android.app.Notification.MediaStyle
22 import android.app.PendingIntent
23 import android.app.smartspace.SmartspaceAction
24 import android.app.smartspace.SmartspaceConfig
25 import android.app.smartspace.SmartspaceManager
26 import android.app.smartspace.SmartspaceTarget
27 import android.content.Intent
28 import android.graphics.Bitmap
29 import android.graphics.drawable.Icon
30 import android.media.MediaDescription
31 import android.media.MediaMetadata
32 import android.media.session.MediaController
33 import android.media.session.MediaSession
34 import android.media.session.PlaybackState
35 import android.os.Bundle
36 import android.provider.Settings
37 import android.service.notification.StatusBarNotification
38 import android.testing.AndroidTestingRunner
39 import android.testing.TestableLooper.RunWithLooper
40 import androidx.media.utils.MediaConstants
41 import androidx.test.filters.SmallTest
42 import com.android.internal.logging.InstanceId
43 import com.android.keyguard.KeyguardUpdateMonitor
44 import com.android.systemui.InstanceIdSequenceFake
45 import com.android.systemui.R
46 import com.android.systemui.SysuiTestCase
47 import com.android.systemui.broadcast.BroadcastDispatcher
48 import com.android.systemui.dump.DumpManager
49 import com.android.systemui.media.controls.models.player.MediaData
50 import com.android.systemui.media.controls.models.recommendation.EXTRA_KEY_TRIGGER_SOURCE
51 import com.android.systemui.media.controls.models.recommendation.EXTRA_VALUE_TRIGGER_PERIODIC
52 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
53 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
54 import com.android.systemui.media.controls.resume.MediaResumeListener
55 import com.android.systemui.media.controls.resume.ResumeMediaBrowser
56 import com.android.systemui.media.controls.util.MediaControllerFactory
57 import com.android.systemui.media.controls.util.MediaFlags
58 import com.android.systemui.media.controls.util.MediaUiEventLogger
59 import com.android.systemui.plugins.ActivityStarter
60 import com.android.systemui.statusbar.SbnBuilder
61 import com.android.systemui.tuner.TunerService
62 import com.android.systemui.util.concurrency.FakeExecutor
63 import com.android.systemui.util.mockito.any
64 import com.android.systemui.util.mockito.capture
65 import com.android.systemui.util.mockito.eq
66 import com.android.systemui.util.time.FakeSystemClock
67 import com.google.common.truth.Truth.assertThat
68 import org.junit.After
69 import org.junit.Before
70 import org.junit.Rule
71 import org.junit.Test
72 import org.junit.runner.RunWith
73 import org.mockito.ArgumentCaptor
74 import org.mockito.ArgumentMatchers.anyBoolean
75 import org.mockito.ArgumentMatchers.anyInt
76 import org.mockito.Captor
77 import org.mockito.Mock
78 import org.mockito.Mockito
79 import org.mockito.Mockito.never
80 import org.mockito.Mockito.reset
81 import org.mockito.Mockito.verify
82 import org.mockito.Mockito.verifyNoMoreInteractions
83 import org.mockito.Mockito.`when` as whenever
84 import org.mockito.junit.MockitoJUnit
85
86 private const val KEY = "KEY"
87 private const val KEY_2 = "KEY_2"
88 private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
89 private const val SMARTSPACE_CREATION_TIME = 1234L
90 private const val SMARTSPACE_EXPIRY_TIME = 5678L
91 private const val PACKAGE_NAME = "com.example.app"
92 private const val SYSTEM_PACKAGE_NAME = "com.android.systemui"
93 private const val APP_NAME = "SystemUI"
94 private const val SESSION_ARTIST = "artist"
95 private const val SESSION_TITLE = "title"
96 private const val SESSION_BLANK_TITLE = " "
97 private const val SESSION_EMPTY_TITLE = ""
98 private const val USER_ID = 0
<lambda>null99 private val DISMISS_INTENT = Intent().apply { action = "dismiss" }
100
anyObjectnull101 private fun <T> anyObject(): T {
102 return Mockito.anyObject<T>()
103 }
104
105 @SmallTest
106 @RunWithLooper(setAsMainLooper = true)
107 @RunWith(AndroidTestingRunner::class)
108 class MediaDataManagerTest : SysuiTestCase() {
109
110 @JvmField @Rule val mockito = MockitoJUnit.rule()
111 @Mock lateinit var mediaControllerFactory: MediaControllerFactory
112 @Mock lateinit var controller: MediaController
113 @Mock lateinit var transportControls: MediaController.TransportControls
114 @Mock lateinit var playbackInfo: MediaController.PlaybackInfo
115 lateinit var session: MediaSession
116 lateinit var metadataBuilder: MediaMetadata.Builder
117 lateinit var backgroundExecutor: FakeExecutor
118 lateinit var foregroundExecutor: FakeExecutor
119 lateinit var uiExecutor: FakeExecutor
120 @Mock lateinit var dumpManager: DumpManager
121 @Mock lateinit var broadcastDispatcher: BroadcastDispatcher
122 @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener
123 @Mock lateinit var mediaResumeListener: MediaResumeListener
124 @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter
125 @Mock lateinit var mediaDeviceManager: MediaDeviceManager
126 @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
127 @Mock lateinit var mediaDataFilter: MediaDataFilter
128 @Mock lateinit var listener: MediaDataManager.Listener
129 @Mock lateinit var pendingIntent: PendingIntent
130 @Mock lateinit var activityStarter: ActivityStarter
131 @Mock lateinit var smartspaceManager: SmartspaceManager
132 @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
133 lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
134 @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
135 @Mock private lateinit var mediaRecommendationItem: SmartspaceAction
136 lateinit var validRecommendationList: List<SmartspaceAction>
137 @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction
138 @Mock private lateinit var mediaFlags: MediaFlags
139 @Mock private lateinit var logger: MediaUiEventLogger
140 lateinit var mediaDataManager: MediaDataManager
141 lateinit var mediaNotification: StatusBarNotification
142 lateinit var remoteCastNotification: StatusBarNotification
143 @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
144 private val clock = FakeSystemClock()
145 @Mock private lateinit var tunerService: TunerService
146 @Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
147 @Captor lateinit var stateCallbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
148 @Captor lateinit var sessionCallbackCaptor: ArgumentCaptor<(String) -> Unit>
149 @Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig>
150
151 private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
152
153 private val originalSmartspaceSetting =
154 Settings.Secure.getInt(
155 context.contentResolver,
156 Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
157 1
158 )
159
160 @Before
setupnull161 fun setup() {
162 foregroundExecutor = FakeExecutor(clock)
163 backgroundExecutor = FakeExecutor(clock)
164 uiExecutor = FakeExecutor(clock)
165 smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
166 Settings.Secure.putInt(
167 context.contentResolver,
168 Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
169 1
170 )
171 mediaDataManager =
172 MediaDataManager(
173 context = context,
174 backgroundExecutor = backgroundExecutor,
175 uiExecutor = uiExecutor,
176 foregroundExecutor = foregroundExecutor,
177 mediaControllerFactory = mediaControllerFactory,
178 broadcastDispatcher = broadcastDispatcher,
179 dumpManager = dumpManager,
180 mediaTimeoutListener = mediaTimeoutListener,
181 mediaResumeListener = mediaResumeListener,
182 mediaSessionBasedFilter = mediaSessionBasedFilter,
183 mediaDeviceManager = mediaDeviceManager,
184 mediaDataCombineLatest = mediaDataCombineLatest,
185 mediaDataFilter = mediaDataFilter,
186 activityStarter = activityStarter,
187 smartspaceMediaDataProvider = smartspaceMediaDataProvider,
188 useMediaResumption = true,
189 useQsMediaPlayer = true,
190 systemClock = clock,
191 tunerService = tunerService,
192 mediaFlags = mediaFlags,
193 logger = logger,
194 smartspaceManager = smartspaceManager,
195 keyguardUpdateMonitor = keyguardUpdateMonitor
196 )
197 verify(tunerService)
198 .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
199 verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor)
200 verify(mediaTimeoutListener).sessionCallback = capture(sessionCallbackCaptor)
201 session = MediaSession(context, "MediaDataManagerTestSession")
202 mediaNotification =
203 SbnBuilder().run {
204 setPkg(PACKAGE_NAME)
205 modifyNotification(context).also {
206 it.setSmallIcon(android.R.drawable.ic_media_pause)
207 it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
208 }
209 build()
210 }
211 remoteCastNotification =
212 SbnBuilder().run {
213 setPkg(SYSTEM_PACKAGE_NAME)
214 modifyNotification(context).also {
215 it.setSmallIcon(android.R.drawable.ic_media_pause)
216 it.setStyle(
217 MediaStyle().apply {
218 setMediaSession(session.sessionToken)
219 setRemotePlaybackInfo("Remote device", 0, null)
220 }
221 )
222 }
223 build()
224 }
225 metadataBuilder =
226 MediaMetadata.Builder().apply {
227 putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
228 putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
229 }
230 verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor))
231 whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller)
232 whenever(controller.transportControls).thenReturn(transportControls)
233 whenever(controller.playbackInfo).thenReturn(playbackInfo)
234 whenever(controller.metadata).thenReturn(metadataBuilder.build())
235 whenever(playbackInfo.playbackType)
236 .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL)
237
238 // This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal
239 // listeners in the internal processing pipeline. It receives events, but ince it is a
240 // mock, it doesn't pass those events along the chain to the external listeners. So, just
241 // treat mediaSessionBasedFilter as a listener for testing.
242 listener = mediaSessionBasedFilter
243
244 val recommendationExtras =
245 Bundle().apply {
246 putString("package_name", PACKAGE_NAME)
247 putParcelable("dismiss_intent", DISMISS_INTENT)
248 }
249 val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play)
250 whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
251 whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
252 whenever(mediaRecommendationItem.extras).thenReturn(recommendationExtras)
253 whenever(mediaRecommendationItem.icon).thenReturn(icon)
254 validRecommendationList =
255 listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem)
256 whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE)
257 whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
258 whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
259 whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME)
260 whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME)
261 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
262 whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true)
263 whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false)
264 whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
265 whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(false)
266 whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
267 whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false)
268 }
269
270 @After
tearDownnull271 fun tearDown() {
272 session.release()
273 mediaDataManager.destroy()
274 Settings.Secure.putInt(
275 context.contentResolver,
276 Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
277 originalSmartspaceSetting
278 )
279 }
280
281 @Test
testSetTimedOut_active_deactivatesMedianull282 fun testSetTimedOut_active_deactivatesMedia() {
283 addNotificationAndLoad()
284 val data = mediaDataCaptor.value
285 assertThat(data.active).isTrue()
286
287 mediaDataManager.setTimedOut(KEY, timedOut = true)
288 assertThat(data.active).isFalse()
289 verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
290 }
291
292 @Test
testSetTimedOut_resume_dismissesMedianull293 fun testSetTimedOut_resume_dismissesMedia() {
294 // WHEN resume controls are present, and time out
295 val desc =
296 MediaDescription.Builder().run {
297 setTitle(SESSION_TITLE)
298 build()
299 }
300 mediaDataManager.addResumptionControls(
301 USER_ID,
302 desc,
303 Runnable {},
304 session.sessionToken,
305 APP_NAME,
306 pendingIntent,
307 PACKAGE_NAME
308 )
309
310 backgroundExecutor.runAllReady()
311 foregroundExecutor.runAllReady()
312 verify(listener)
313 .onMediaDataLoaded(
314 eq(PACKAGE_NAME),
315 eq(null),
316 capture(mediaDataCaptor),
317 eq(true),
318 eq(0),
319 eq(false)
320 )
321
322 mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true)
323 verify(logger)
324 .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId))
325
326 // THEN it is removed and listeners are informed
327 foregroundExecutor.advanceClockToLast()
328 foregroundExecutor.runAllReady()
329 verify(listener).onMediaDataRemoved(PACKAGE_NAME)
330 }
331
332 @Test
testLoadsMetadataOnBackgroundnull333 fun testLoadsMetadataOnBackground() {
334 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
335 assertThat(backgroundExecutor.numPending()).isEqualTo(1)
336 }
337
338 @Test
testLoadMetadata_withExplicitIndicatornull339 fun testLoadMetadata_withExplicitIndicator() {
340 whenever(controller.metadata)
341 .thenReturn(
342 metadataBuilder
343 .putLong(
344 MediaConstants.METADATA_KEY_IS_EXPLICIT,
345 MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
346 )
347 .build()
348 )
349
350 mediaDataManager.addListener(listener)
351 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
352
353 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
354 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
355 verify(listener)
356 .onMediaDataLoaded(
357 eq(KEY),
358 eq(null),
359 capture(mediaDataCaptor),
360 eq(true),
361 eq(0),
362 eq(false)
363 )
364 assertThat(mediaDataCaptor.value!!.isExplicit).isTrue()
365 }
366
367 @Test
testOnMetaDataLoaded_withoutExplicitIndicatornull368 fun testOnMetaDataLoaded_withoutExplicitIndicator() {
369 mediaDataManager.addListener(listener)
370 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
371
372 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
373 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
374 verify(listener)
375 .onMediaDataLoaded(
376 eq(KEY),
377 eq(null),
378 capture(mediaDataCaptor),
379 eq(true),
380 eq(0),
381 eq(false)
382 )
383 assertThat(mediaDataCaptor.value!!.isExplicit).isFalse()
384 }
385
386 @Test
testOnMetaDataLoaded_callsListenernull387 fun testOnMetaDataLoaded_callsListener() {
388 addNotificationAndLoad()
389 verify(logger)
390 .logActiveMediaAdded(
391 anyInt(),
392 eq(PACKAGE_NAME),
393 eq(mediaDataCaptor.value.instanceId),
394 eq(MediaData.PLAYBACK_LOCAL)
395 )
396 }
397
398 @Test
testOnMetaDataLoaded_conservesActiveFlagnull399 fun testOnMetaDataLoaded_conservesActiveFlag() {
400 whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
401 mediaDataManager.addListener(listener)
402 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
403 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
404 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
405 verify(listener)
406 .onMediaDataLoaded(
407 eq(KEY),
408 eq(null),
409 capture(mediaDataCaptor),
410 eq(true),
411 eq(0),
412 eq(false)
413 )
414 assertThat(mediaDataCaptor.value!!.active).isTrue()
415 }
416
417 @Test
testOnNotificationAdded_isRcn_markedRemotenull418 fun testOnNotificationAdded_isRcn_markedRemote() {
419 addNotificationAndLoad(remoteCastNotification)
420
421 assertThat(mediaDataCaptor.value!!.playbackLocation)
422 .isEqualTo(MediaData.PLAYBACK_CAST_REMOTE)
423 verify(logger)
424 .logActiveMediaAdded(
425 anyInt(),
426 eq(SYSTEM_PACKAGE_NAME),
427 eq(mediaDataCaptor.value.instanceId),
428 eq(MediaData.PLAYBACK_CAST_REMOTE)
429 )
430 }
431
432 @Test
testOnNotificationAdded_hasSubstituteName_isUsednull433 fun testOnNotificationAdded_hasSubstituteName_isUsed() {
434 val subName = "Substitute Name"
435 val notif =
436 SbnBuilder().run {
437 modifyNotification(context).also {
438 it.extras =
439 Bundle().apply {
440 putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName)
441 }
442 it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
443 }
444 build()
445 }
446
447 mediaDataManager.onNotificationAdded(KEY, notif)
448 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
449 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
450 verify(listener)
451 .onMediaDataLoaded(
452 eq(KEY),
453 eq(null),
454 capture(mediaDataCaptor),
455 eq(true),
456 eq(0),
457 eq(false)
458 )
459
460 assertThat(mediaDataCaptor.value!!.app).isEqualTo(subName)
461 }
462
463 @Test
testLoadMediaDataInBg_invalidTokenNoCrashnull464 fun testLoadMediaDataInBg_invalidTokenNoCrash() {
465 val bundle = Bundle()
466 // wrong data type
467 bundle.putParcelable(Notification.EXTRA_MEDIA_SESSION, Bundle())
468 val rcn =
469 SbnBuilder().run {
470 setPkg(SYSTEM_PACKAGE_NAME)
471 modifyNotification(context).also {
472 it.setSmallIcon(android.R.drawable.ic_media_pause)
473 it.addExtras(bundle)
474 it.setStyle(
475 MediaStyle().apply { setRemotePlaybackInfo("Remote device", 0, null) }
476 )
477 }
478 build()
479 }
480
481 mediaDataManager.loadMediaDataInBg(KEY, rcn, null)
482 // no crash even though the data structure is incorrect
483 }
484
485 @Test
testLoadMediaDataInBg_invalidMediaRemoteIntentNoCrashnull486 fun testLoadMediaDataInBg_invalidMediaRemoteIntentNoCrash() {
487 val bundle = Bundle()
488 // wrong data type
489 bundle.putParcelable(Notification.EXTRA_MEDIA_REMOTE_INTENT, Bundle())
490 val rcn =
491 SbnBuilder().run {
492 setPkg(SYSTEM_PACKAGE_NAME)
493 modifyNotification(context).also {
494 it.setSmallIcon(android.R.drawable.ic_media_pause)
495 it.addExtras(bundle)
496 it.setStyle(
497 MediaStyle().apply {
498 setMediaSession(session.sessionToken)
499 setRemotePlaybackInfo("Remote device", 0, null)
500 }
501 )
502 }
503 build()
504 }
505
506 mediaDataManager.loadMediaDataInBg(KEY, rcn, null)
507 // no crash even though the data structure is incorrect
508 }
509
510 @Test
testOnNotificationRemoved_callsListenernull511 fun testOnNotificationRemoved_callsListener() {
512 addNotificationAndLoad()
513 val data = mediaDataCaptor.value
514 mediaDataManager.onNotificationRemoved(KEY)
515 verify(listener).onMediaDataRemoved(eq(KEY))
516 verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
517 }
518
519 @Test
testOnNotificationRemoved_emptyTitle_notConvertednull520 fun testOnNotificationRemoved_emptyTitle_notConverted() {
521 // GIVEN that the manager has a notification with a resume action and empty title.
522 whenever(controller.metadata)
523 .thenReturn(
524 metadataBuilder
525 .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE)
526 .build()
527 )
528 addNotificationAndLoad()
529 val data = mediaDataCaptor.value
530 val instanceId = data.instanceId
531 assertThat(data.resumption).isFalse()
532 mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
533
534 // WHEN the notification is removed
535 reset(listener)
536 mediaDataManager.onNotificationRemoved(KEY)
537
538 // THEN active media is not converted to resume.
539 verify(listener, never())
540 .onMediaDataLoaded(
541 eq(PACKAGE_NAME),
542 eq(KEY),
543 capture(mediaDataCaptor),
544 eq(true),
545 eq(0),
546 eq(false)
547 )
548 verify(logger, never())
549 .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId))
550 verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
551 verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(instanceId))
552 }
553
554 @Test
testOnNotificationRemoved_blankTitle_notConvertednull555 fun testOnNotificationRemoved_blankTitle_notConverted() {
556 // GIVEN that the manager has a notification with a resume action and blank title.
557 whenever(controller.metadata)
558 .thenReturn(
559 metadataBuilder
560 .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE)
561 .build()
562 )
563 addNotificationAndLoad()
564 val data = mediaDataCaptor.value
565 val instanceId = data.instanceId
566 assertThat(data.resumption).isFalse()
567 mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
568
569 // WHEN the notification is removed
570 reset(listener)
571 mediaDataManager.onNotificationRemoved(KEY)
572
573 // THEN active media is not converted to resume.
574 verify(listener, never())
575 .onMediaDataLoaded(
576 eq(PACKAGE_NAME),
577 eq(KEY),
578 capture(mediaDataCaptor),
579 eq(true),
580 eq(0),
581 eq(false)
582 )
583 verify(logger, never())
584 .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId))
585 verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
586 verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(instanceId))
587 }
588
589 @Test
testOnNotificationRemoved_withResumptionnull590 fun testOnNotificationRemoved_withResumption() {
591 // GIVEN that the manager has a notification with a resume action
592 addNotificationAndLoad()
593 val data = mediaDataCaptor.value
594 assertThat(data.resumption).isFalse()
595 mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
596 // WHEN the notification is removed
597 mediaDataManager.onNotificationRemoved(KEY)
598 // THEN the media data indicates that it is for resumption
599 verify(listener)
600 .onMediaDataLoaded(
601 eq(PACKAGE_NAME),
602 eq(KEY),
603 capture(mediaDataCaptor),
604 eq(true),
605 eq(0),
606 eq(false)
607 )
608 assertThat(mediaDataCaptor.value.resumption).isTrue()
609 assertThat(mediaDataCaptor.value.isPlaying).isFalse()
610 verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
611 }
612
613 @Test
testOnNotificationRemoved_twoWithResumptionnull614 fun testOnNotificationRemoved_twoWithResumption() {
615 // GIVEN that the manager has two notifications with resume actions
616 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
617 mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
618 assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
619 assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
620
621 verify(listener)
622 .onMediaDataLoaded(
623 eq(KEY),
624 eq(null),
625 capture(mediaDataCaptor),
626 eq(true),
627 eq(0),
628 eq(false)
629 )
630 val data = mediaDataCaptor.value
631 assertThat(data.resumption).isFalse()
632
633 verify(listener)
634 .onMediaDataLoaded(
635 eq(KEY_2),
636 eq(null),
637 capture(mediaDataCaptor),
638 eq(true),
639 eq(0),
640 eq(false)
641 )
642 val data2 = mediaDataCaptor.value
643 assertThat(data2.resumption).isFalse()
644
645 mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
646 mediaDataManager.onMediaDataLoaded(KEY_2, null, data2.copy(resumeAction = Runnable {}))
647 reset(listener)
648 // WHEN the first is removed
649 mediaDataManager.onNotificationRemoved(KEY)
650 // THEN the data is for resumption and the key is migrated to the package name
651 verify(listener)
652 .onMediaDataLoaded(
653 eq(PACKAGE_NAME),
654 eq(KEY),
655 capture(mediaDataCaptor),
656 eq(true),
657 eq(0),
658 eq(false)
659 )
660 assertThat(mediaDataCaptor.value.resumption).isTrue()
661 verify(listener, never()).onMediaDataRemoved(eq(KEY))
662 // WHEN the second is removed
663 mediaDataManager.onNotificationRemoved(KEY_2)
664 // THEN the data is for resumption and the second key is removed
665 verify(listener)
666 .onMediaDataLoaded(
667 eq(PACKAGE_NAME),
668 eq(PACKAGE_NAME),
669 capture(mediaDataCaptor),
670 eq(true),
671 eq(0),
672 eq(false)
673 )
674 assertThat(mediaDataCaptor.value.resumption).isTrue()
675 verify(listener).onMediaDataRemoved(eq(KEY_2))
676 }
677
678 @Test
testOnNotificationRemoved_withResumption_butNotLocalnull679 fun testOnNotificationRemoved_withResumption_butNotLocal() {
680 // GIVEN that the manager has a notification with a resume action, but is not local
681 whenever(playbackInfo.playbackType)
682 .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
683 addNotificationAndLoad()
684 val data = mediaDataCaptor.value
685 val dataRemoteWithResume =
686 data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
687 mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
688 verify(logger)
689 .logActiveMediaAdded(
690 anyInt(),
691 eq(PACKAGE_NAME),
692 eq(mediaDataCaptor.value.instanceId),
693 eq(MediaData.PLAYBACK_CAST_LOCAL)
694 )
695
696 // WHEN the notification is removed
697 mediaDataManager.onNotificationRemoved(KEY)
698
699 // THEN the media data is removed
700 verify(listener).onMediaDataRemoved(eq(KEY))
701 }
702
703 @Test
testOnNotificationRemoved_withResumption_isRemoteAndRemoteAllowednull704 fun testOnNotificationRemoved_withResumption_isRemoteAndRemoteAllowed() {
705 // With the flag enabled to allow remote media to resume
706 whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true)
707
708 // GIVEN that the manager has a notification with a resume action, but is not local
709 whenever(controller.metadata).thenReturn(metadataBuilder.build())
710 whenever(playbackInfo.playbackType)
711 .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
712 addNotificationAndLoad()
713 val data = mediaDataCaptor.value
714 val dataRemoteWithResume =
715 data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
716 mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
717
718 // WHEN the notification is removed
719 mediaDataManager.onNotificationRemoved(KEY)
720
721 // THEN the media data is converted to a resume state
722 verify(listener)
723 .onMediaDataLoaded(
724 eq(PACKAGE_NAME),
725 eq(KEY),
726 capture(mediaDataCaptor),
727 eq(true),
728 eq(0),
729 eq(false)
730 )
731 assertThat(mediaDataCaptor.value.resumption).isTrue()
732 }
733
734 @Test
testOnNotificationRemoved_withResumption_isRcnAndRemoteAllowednull735 fun testOnNotificationRemoved_withResumption_isRcnAndRemoteAllowed() {
736 // With the flag enabled to allow remote media to resume
737 whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true)
738
739 // GIVEN that the manager has a remote cast notification
740 addNotificationAndLoad(remoteCastNotification)
741 val data = mediaDataCaptor.value
742 assertThat(data.playbackLocation).isEqualTo(MediaData.PLAYBACK_CAST_REMOTE)
743 val dataRemoteWithResume = data.copy(resumeAction = Runnable {})
744 mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
745
746 // WHEN the RCN is removed
747 mediaDataManager.onNotificationRemoved(KEY)
748
749 // THEN the media data is removed
750 verify(listener).onMediaDataRemoved(eq(KEY))
751 }
752
753 @Test
testOnNotificationRemoved_withResumption_tooManyPlayersnull754 fun testOnNotificationRemoved_withResumption_tooManyPlayers() {
755 // Given the maximum number of resume controls already
756 val desc =
757 MediaDescription.Builder().run {
758 setTitle(SESSION_TITLE)
759 build()
760 }
761 for (i in 0..ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
762 addResumeControlAndLoad(desc, "$i:$PACKAGE_NAME")
763 clock.advanceTime(1000)
764 }
765
766 // And an active, resumable notification
767 addNotificationAndLoad()
768 val data = mediaDataCaptor.value
769 assertThat(data.resumption).isFalse()
770 mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
771
772 // When the notification is removed
773 mediaDataManager.onNotificationRemoved(KEY)
774
775 // Then it is converted to resumption
776 verify(listener)
777 .onMediaDataLoaded(
778 eq(PACKAGE_NAME),
779 eq(KEY),
780 capture(mediaDataCaptor),
781 eq(true),
782 eq(0),
783 eq(false)
784 )
785 assertThat(mediaDataCaptor.value.resumption).isTrue()
786 assertThat(mediaDataCaptor.value.isPlaying).isFalse()
787
788 // And the oldest resume control was removed
789 verify(listener).onMediaDataRemoved(eq("0:$PACKAGE_NAME"))
790 }
791
testOnNotificationRemoved_lockDownModenull792 fun testOnNotificationRemoved_lockDownMode() {
793 whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(true)
794
795 addNotificationAndLoad()
796 val data = mediaDataCaptor.value
797 mediaDataManager.onNotificationRemoved(KEY)
798
799 verify(listener, never()).onMediaDataRemoved(eq(KEY))
800 verify(logger, never())
801 .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
802 verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
803 }
804
805 @Test
testAddResumptionControlsnull806 fun testAddResumptionControls() {
807 // WHEN resumption controls are added
808 val desc =
809 MediaDescription.Builder().run {
810 setTitle(SESSION_TITLE)
811 build()
812 }
813 val currentTime = clock.elapsedRealtime()
814 addResumeControlAndLoad(desc)
815
816 val data = mediaDataCaptor.value
817 assertThat(data.resumption).isTrue()
818 assertThat(data.song).isEqualTo(SESSION_TITLE)
819 assertThat(data.app).isEqualTo(APP_NAME)
820 assertThat(data.actions).hasSize(1)
821 assertThat(data.semanticActions!!.playOrPause).isNotNull()
822 assertThat(data.lastActive).isAtLeast(currentTime)
823 verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
824 }
825
826 @Test
testAddResumptionControls_withExplicitIndicatornull827 fun testAddResumptionControls_withExplicitIndicator() {
828 val bundle = Bundle()
829 // WHEN resumption controls are added with explicit indicator
830 bundle.putLong(
831 MediaConstants.METADATA_KEY_IS_EXPLICIT,
832 MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
833 )
834 val desc =
835 MediaDescription.Builder().run {
836 setTitle(SESSION_TITLE)
837 setExtras(bundle)
838 build()
839 }
840 val currentTime = clock.elapsedRealtime()
841 addResumeControlAndLoad(desc)
842
843 val data = mediaDataCaptor.value
844 assertThat(data.resumption).isTrue()
845 assertThat(data.song).isEqualTo(SESSION_TITLE)
846 assertThat(data.app).isEqualTo(APP_NAME)
847 assertThat(data.actions).hasSize(1)
848 assertThat(data.semanticActions!!.playOrPause).isNotNull()
849 assertThat(data.lastActive).isAtLeast(currentTime)
850 assertThat(data.isExplicit).isTrue()
851 verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
852 }
853
854 @Test
testAddResumptionControls_hasPartialProgressnull855 fun testAddResumptionControls_hasPartialProgress() {
856 whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
857
858 // WHEN resumption controls are added with partial progress
859 val progress = 0.5
860 val extras =
861 Bundle().apply {
862 putInt(
863 MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
864 MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
865 )
866 putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress)
867 }
868 val desc =
869 MediaDescription.Builder().run {
870 setTitle(SESSION_TITLE)
871 setExtras(extras)
872 build()
873 }
874 addResumeControlAndLoad(desc)
875
876 val data = mediaDataCaptor.value
877 assertThat(data.resumption).isTrue()
878 assertThat(data.resumeProgress).isEqualTo(progress)
879 }
880
881 @Test
testAddResumptionControls_hasNotPlayedProgressnull882 fun testAddResumptionControls_hasNotPlayedProgress() {
883 whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
884
885 // WHEN resumption controls are added that have not been played
886 val extras =
887 Bundle().apply {
888 putInt(
889 MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
890 MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
891 )
892 }
893 val desc =
894 MediaDescription.Builder().run {
895 setTitle(SESSION_TITLE)
896 setExtras(extras)
897 build()
898 }
899 addResumeControlAndLoad(desc)
900
901 val data = mediaDataCaptor.value
902 assertThat(data.resumption).isTrue()
903 assertThat(data.resumeProgress).isEqualTo(0)
904 }
905
906 @Test
testAddResumptionControls_hasFullProgressnull907 fun testAddResumptionControls_hasFullProgress() {
908 whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
909
910 // WHEN resumption controls are added with progress info
911 val extras =
912 Bundle().apply {
913 putInt(
914 MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
915 MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
916 )
917 }
918 val desc =
919 MediaDescription.Builder().run {
920 setTitle(SESSION_TITLE)
921 setExtras(extras)
922 build()
923 }
924 addResumeControlAndLoad(desc)
925
926 // THEN the media data includes the progress
927 val data = mediaDataCaptor.value
928 assertThat(data.resumption).isTrue()
929 assertThat(data.resumeProgress).isEqualTo(1)
930 }
931
932 @Test
testAddResumptionControls_hasNoExtrasnull933 fun testAddResumptionControls_hasNoExtras() {
934 whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
935
936 // WHEN resumption controls are added that do not have any extras
937 val desc =
938 MediaDescription.Builder().run {
939 setTitle(SESSION_TITLE)
940 build()
941 }
942 addResumeControlAndLoad(desc)
943
944 // Resume progress is null
945 val data = mediaDataCaptor.value
946 assertThat(data.resumption).isTrue()
947 assertThat(data.resumeProgress).isEqualTo(null)
948 }
949
950 @Test
testAddResumptionControls_hasEmptyTitlenull951 fun testAddResumptionControls_hasEmptyTitle() {
952 whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
953
954 // WHEN resumption controls are added that have empty title
955 val desc =
956 MediaDescription.Builder().run {
957 setTitle(SESSION_EMPTY_TITLE)
958 build()
959 }
960 mediaDataManager.addResumptionControls(
961 USER_ID,
962 desc,
963 Runnable {},
964 session.sessionToken,
965 APP_NAME,
966 pendingIntent,
967 PACKAGE_NAME
968 )
969
970 // Resumption controls are not added.
971 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
972 assertThat(foregroundExecutor.runAllReady()).isEqualTo(0)
973 verify(listener, never())
974 .onMediaDataLoaded(
975 eq(PACKAGE_NAME),
976 eq(null),
977 capture(mediaDataCaptor),
978 eq(true),
979 eq(0),
980 eq(false)
981 )
982 }
983
984 @Test
testAddResumptionControls_hasBlankTitlenull985 fun testAddResumptionControls_hasBlankTitle() {
986 whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
987
988 // WHEN resumption controls are added that have a blank title
989 val desc =
990 MediaDescription.Builder().run {
991 setTitle(SESSION_BLANK_TITLE)
992 build()
993 }
994 mediaDataManager.addResumptionControls(
995 USER_ID,
996 desc,
997 Runnable {},
998 session.sessionToken,
999 APP_NAME,
1000 pendingIntent,
1001 PACKAGE_NAME
1002 )
1003
1004 // Resumption controls are not added.
1005 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
1006 assertThat(foregroundExecutor.runAllReady()).isEqualTo(0)
1007 verify(listener, never())
1008 .onMediaDataLoaded(
1009 eq(PACKAGE_NAME),
1010 eq(null),
1011 capture(mediaDataCaptor),
1012 eq(true),
1013 eq(0),
1014 eq(false)
1015 )
1016 }
1017
1018 @Test
testResumptionDisabled_dismissesResumeControlsnull1019 fun testResumptionDisabled_dismissesResumeControls() {
1020 // WHEN there are resume controls and resumption is switched off
1021 val desc =
1022 MediaDescription.Builder().run {
1023 setTitle(SESSION_TITLE)
1024 build()
1025 }
1026 addResumeControlAndLoad(desc)
1027
1028 val data = mediaDataCaptor.value
1029 mediaDataManager.setMediaResumptionEnabled(false)
1030
1031 // THEN the resume controls are dismissed
1032 verify(listener).onMediaDataRemoved(eq(PACKAGE_NAME))
1033 verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
1034 }
1035
1036 @Test
testDismissMedia_listenerCallednull1037 fun testDismissMedia_listenerCalled() {
1038 addNotificationAndLoad()
1039 val data = mediaDataCaptor.value
1040 val removed = mediaDataManager.dismissMediaData(KEY, 0L)
1041 assertThat(removed).isTrue()
1042
1043 foregroundExecutor.advanceClockToLast()
1044 foregroundExecutor.runAllReady()
1045
1046 verify(listener).onMediaDataRemoved(eq(KEY))
1047 verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
1048 }
1049
1050 @Test
testDismissMedia_keyDoesNotExist_returnsFalsenull1051 fun testDismissMedia_keyDoesNotExist_returnsFalse() {
1052 val removed = mediaDataManager.dismissMediaData(KEY, 0L)
1053 assertThat(removed).isFalse()
1054 }
1055
1056 @Test
testBadArtwork_doesNotUsenull1057 fun testBadArtwork_doesNotUse() {
1058 // WHEN notification has a too-small artwork
1059 val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
1060 val notif =
1061 SbnBuilder().run {
1062 setPkg(PACKAGE_NAME)
1063 modifyNotification(context).also {
1064 it.setSmallIcon(android.R.drawable.ic_media_pause)
1065 it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
1066 it.setLargeIcon(artwork)
1067 }
1068 build()
1069 }
1070 mediaDataManager.onNotificationAdded(KEY, notif)
1071
1072 // THEN it still loads
1073 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
1074 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
1075 verify(listener)
1076 .onMediaDataLoaded(
1077 eq(KEY),
1078 eq(null),
1079 capture(mediaDataCaptor),
1080 eq(true),
1081 eq(0),
1082 eq(false)
1083 )
1084 }
1085
1086 @Test
testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListenernull1087 fun testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListener() {
1088 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
1089 verify(logger).getNewInstanceId()
1090 val instanceId = instanceIdSequence.lastInstanceId
1091
1092 verify(listener)
1093 .onSmartspaceMediaDataLoaded(
1094 eq(KEY_MEDIA_SMARTSPACE),
1095 eq(
1096 SmartspaceMediaData(
1097 targetId = KEY_MEDIA_SMARTSPACE,
1098 isActive = true,
1099 packageName = PACKAGE_NAME,
1100 cardAction = mediaSmartspaceBaseAction,
1101 recommendations = validRecommendationList,
1102 dismissIntent = DISMISS_INTENT,
1103 headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
1104 instanceId = InstanceId.fakeInstanceId(instanceId),
1105 expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
1106 )
1107 ),
1108 eq(false)
1109 )
1110 }
1111
1112 @Test
testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListenernull1113 fun testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListener() {
1114 whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
1115 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
1116 verify(logger).getNewInstanceId()
1117 val instanceId = instanceIdSequence.lastInstanceId
1118
1119 verify(listener)
1120 .onSmartspaceMediaDataLoaded(
1121 eq(KEY_MEDIA_SMARTSPACE),
1122 eq(
1123 EMPTY_SMARTSPACE_MEDIA_DATA.copy(
1124 targetId = KEY_MEDIA_SMARTSPACE,
1125 isActive = true,
1126 dismissIntent = DISMISS_INTENT,
1127 headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
1128 instanceId = InstanceId.fakeInstanceId(instanceId),
1129 expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
1130 )
1131 ),
1132 eq(false)
1133 )
1134 }
1135
1136 @Test
testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListenernull1137 fun testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListener() {
1138 val recommendationExtras =
1139 Bundle().apply {
1140 putString("package_name", PACKAGE_NAME)
1141 putParcelable("dismiss_intent", null)
1142 }
1143 whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
1144 whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
1145 whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
1146
1147 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
1148 verify(logger).getNewInstanceId()
1149 val instanceId = instanceIdSequence.lastInstanceId
1150
1151 verify(listener)
1152 .onSmartspaceMediaDataLoaded(
1153 eq(KEY_MEDIA_SMARTSPACE),
1154 eq(
1155 EMPTY_SMARTSPACE_MEDIA_DATA.copy(
1156 targetId = KEY_MEDIA_SMARTSPACE,
1157 isActive = true,
1158 dismissIntent = null,
1159 headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
1160 instanceId = InstanceId.fakeInstanceId(instanceId),
1161 expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
1162 )
1163 ),
1164 eq(false)
1165 )
1166 }
1167
1168 @Test
testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListenernull1169 fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() {
1170 smartspaceMediaDataProvider.onTargetsAvailable(listOf())
1171 verify(logger, never()).getNewInstanceId()
1172 verify(listener, never())
1173 .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
1174 }
1175
1176 @Test
testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListenernull1177 fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() {
1178 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
1179 verify(logger).getNewInstanceId()
1180
1181 smartspaceMediaDataProvider.onTargetsAvailable(listOf())
1182 uiExecutor.advanceClockToLast()
1183 uiExecutor.runAllReady()
1184
1185 verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
1186 verifyNoMoreInteractions(logger)
1187 }
1188
1189 @Test
testOnSmartspaceMediaDataLoaded_persistentEnabled_headphoneTrigger_isActivenull1190 fun testOnSmartspaceMediaDataLoaded_persistentEnabled_headphoneTrigger_isActive() {
1191 whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
1192 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
1193 val instanceId = instanceIdSequence.lastInstanceId
1194
1195 verify(listener)
1196 .onSmartspaceMediaDataLoaded(
1197 eq(KEY_MEDIA_SMARTSPACE),
1198 eq(
1199 SmartspaceMediaData(
1200 targetId = KEY_MEDIA_SMARTSPACE,
1201 isActive = true,
1202 packageName = PACKAGE_NAME,
1203 cardAction = mediaSmartspaceBaseAction,
1204 recommendations = validRecommendationList,
1205 dismissIntent = DISMISS_INTENT,
1206 headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
1207 instanceId = InstanceId.fakeInstanceId(instanceId),
1208 expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
1209 )
1210 ),
1211 eq(false)
1212 )
1213 }
1214
1215 @Test
testOnSmartspaceMediaDataLoaded_persistentEnabled_periodicTrigger_notActivenull1216 fun testOnSmartspaceMediaDataLoaded_persistentEnabled_periodicTrigger_notActive() {
1217 whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
1218 val extras =
1219 Bundle().apply {
1220 putString("package_name", PACKAGE_NAME)
1221 putParcelable("dismiss_intent", DISMISS_INTENT)
1222 putString(EXTRA_KEY_TRIGGER_SOURCE, EXTRA_VALUE_TRIGGER_PERIODIC)
1223 }
1224 whenever(mediaSmartspaceBaseAction.extras).thenReturn(extras)
1225
1226 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
1227 val instanceId = instanceIdSequence.lastInstanceId
1228
1229 verify(listener)
1230 .onSmartspaceMediaDataLoaded(
1231 eq(KEY_MEDIA_SMARTSPACE),
1232 eq(
1233 SmartspaceMediaData(
1234 targetId = KEY_MEDIA_SMARTSPACE,
1235 isActive = false,
1236 packageName = PACKAGE_NAME,
1237 cardAction = mediaSmartspaceBaseAction,
1238 recommendations = validRecommendationList,
1239 dismissIntent = DISMISS_INTENT,
1240 headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
1241 instanceId = InstanceId.fakeInstanceId(instanceId),
1242 expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
1243 )
1244 ),
1245 eq(false)
1246 )
1247 }
1248
1249 @Test
testOnSmartspaceMediaDataLoaded_persistentEnabled_noTargets_inactivenull1250 fun testOnSmartspaceMediaDataLoaded_persistentEnabled_noTargets_inactive() {
1251 whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
1252
1253 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
1254 val instanceId = instanceIdSequence.lastInstanceId
1255
1256 smartspaceMediaDataProvider.onTargetsAvailable(listOf())
1257 uiExecutor.advanceClockToLast()
1258 uiExecutor.runAllReady()
1259
1260 verify(listener)
1261 .onSmartspaceMediaDataLoaded(
1262 eq(KEY_MEDIA_SMARTSPACE),
1263 eq(
1264 SmartspaceMediaData(
1265 targetId = KEY_MEDIA_SMARTSPACE,
1266 isActive = false,
1267 packageName = PACKAGE_NAME,
1268 cardAction = mediaSmartspaceBaseAction,
1269 recommendations = validRecommendationList,
1270 dismissIntent = DISMISS_INTENT,
1271 headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
1272 instanceId = InstanceId.fakeInstanceId(instanceId),
1273 expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
1274 )
1275 ),
1276 eq(false)
1277 )
1278 verify(listener, never()).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
1279 }
1280
1281 @Test
testSetRecommendationInactive_notifiesListenersnull1282 fun testSetRecommendationInactive_notifiesListeners() {
1283 whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
1284
1285 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
1286 val instanceId = instanceIdSequence.lastInstanceId
1287
1288 mediaDataManager.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
1289 uiExecutor.advanceClockToLast()
1290 uiExecutor.runAllReady()
1291
1292 verify(listener)
1293 .onSmartspaceMediaDataLoaded(
1294 eq(KEY_MEDIA_SMARTSPACE),
1295 eq(
1296 SmartspaceMediaData(
1297 targetId = KEY_MEDIA_SMARTSPACE,
1298 isActive = false,
1299 packageName = PACKAGE_NAME,
1300 cardAction = mediaSmartspaceBaseAction,
1301 recommendations = validRecommendationList,
1302 dismissIntent = DISMISS_INTENT,
1303 headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
1304 instanceId = InstanceId.fakeInstanceId(instanceId),
1305 expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
1306 )
1307 ),
1308 eq(false)
1309 )
1310 }
1311
1312 @Test
testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothingnull1313 fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
1314 // WHEN media recommendation setting is off
1315 Settings.Secure.putInt(
1316 context.contentResolver,
1317 Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
1318 0
1319 )
1320 tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
1321
1322 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
1323
1324 // THEN smartspace signal is ignored
1325 verify(listener, never())
1326 .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
1327 }
1328
1329 @Test
testMediaRecommendationDisabled_removesSmartspaceDatanull1330 fun testMediaRecommendationDisabled_removesSmartspaceData() {
1331 // GIVEN a media recommendation card is present
1332 smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
1333 verify(listener)
1334 .onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), anyBoolean())
1335
1336 // WHEN the media recommendation setting is turned off
1337 Settings.Secure.putInt(
1338 context.contentResolver,
1339 Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
1340 0
1341 )
1342 tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
1343
1344 // THEN listeners are notified
1345 uiExecutor.advanceClockToLast()
1346 foregroundExecutor.advanceClockToLast()
1347 uiExecutor.runAllReady()
1348 foregroundExecutor.runAllReady()
1349 verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(true))
1350 }
1351
1352 @Test
testOnMediaDataChanged_updatesLastActiveTimenull1353 fun testOnMediaDataChanged_updatesLastActiveTime() {
1354 val currentTime = clock.elapsedRealtime()
1355 addNotificationAndLoad()
1356 assertThat(mediaDataCaptor.value!!.lastActive).isAtLeast(currentTime)
1357 }
1358
1359 @Test
testOnMediaDataTimedOut_doesNotUpdateLastActiveTimenull1360 fun testOnMediaDataTimedOut_doesNotUpdateLastActiveTime() {
1361 // GIVEN that the manager has a notification
1362 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
1363 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
1364 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
1365
1366 // WHEN the notification times out
1367 clock.advanceTime(100)
1368 val currentTime = clock.elapsedRealtime()
1369 mediaDataManager.setTimedOut(KEY, true, true)
1370
1371 // THEN the last active time is not changed
1372 verify(listener)
1373 .onMediaDataLoaded(
1374 eq(KEY),
1375 eq(KEY),
1376 capture(mediaDataCaptor),
1377 eq(true),
1378 eq(0),
1379 eq(false)
1380 )
1381 assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime)
1382 }
1383
1384 @Test
testOnActiveMediaConverted_doesNotUpdateLastActiveTimenull1385 fun testOnActiveMediaConverted_doesNotUpdateLastActiveTime() {
1386 // GIVEN that the manager has a notification with a resume action
1387 addNotificationAndLoad()
1388 val data = mediaDataCaptor.value
1389 val instanceId = data.instanceId
1390 assertThat(data.resumption).isFalse()
1391 mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
1392
1393 // WHEN the notification is removed
1394 clock.advanceTime(100)
1395 val currentTime = clock.elapsedRealtime()
1396 mediaDataManager.onNotificationRemoved(KEY)
1397
1398 // THEN the last active time is not changed
1399 verify(listener)
1400 .onMediaDataLoaded(
1401 eq(PACKAGE_NAME),
1402 eq(KEY),
1403 capture(mediaDataCaptor),
1404 eq(true),
1405 eq(0),
1406 eq(false)
1407 )
1408 assertThat(mediaDataCaptor.value.resumption).isTrue()
1409 assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime)
1410
1411 // Log as a conversion event, not as a new resume control
1412 verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId))
1413 verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
1414 }
1415
1416 @Test
testTooManyCompactActions_isTruncatednull1417 fun testTooManyCompactActions_isTruncated() {
1418 // GIVEN a notification where too many compact actions were specified
1419 val notif =
1420 SbnBuilder().run {
1421 setPkg(PACKAGE_NAME)
1422 modifyNotification(context).also {
1423 it.setSmallIcon(android.R.drawable.ic_media_pause)
1424 it.setStyle(
1425 MediaStyle().apply {
1426 setMediaSession(session.sessionToken)
1427 setShowActionsInCompactView(0, 1, 2, 3, 4)
1428 }
1429 )
1430 }
1431 build()
1432 }
1433
1434 // WHEN the notification is loaded
1435 mediaDataManager.onNotificationAdded(KEY, notif)
1436 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
1437 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
1438
1439 // THEN only the first MAX_COMPACT_ACTIONS are actually set
1440 verify(listener)
1441 .onMediaDataLoaded(
1442 eq(KEY),
1443 eq(null),
1444 capture(mediaDataCaptor),
1445 eq(true),
1446 eq(0),
1447 eq(false)
1448 )
1449 assertThat(mediaDataCaptor.value.actionsToShowInCompact.size)
1450 .isEqualTo(MediaDataManager.MAX_COMPACT_ACTIONS)
1451 }
1452
1453 @Test
testTooManyNotificationActions_isTruncatednull1454 fun testTooManyNotificationActions_isTruncated() {
1455 // GIVEN a notification where too many notification actions are added
1456 val action = Notification.Action(R.drawable.ic_android, "action", null)
1457 val notif =
1458 SbnBuilder().run {
1459 setPkg(PACKAGE_NAME)
1460 modifyNotification(context).also {
1461 it.setSmallIcon(android.R.drawable.ic_media_pause)
1462 it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
1463 for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) {
1464 it.addAction(action)
1465 }
1466 }
1467 build()
1468 }
1469
1470 // WHEN the notification is loaded
1471 mediaDataManager.onNotificationAdded(KEY, notif)
1472 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
1473 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
1474
1475 // THEN only the first MAX_NOTIFICATION_ACTIONS are actually included
1476 verify(listener)
1477 .onMediaDataLoaded(
1478 eq(KEY),
1479 eq(null),
1480 capture(mediaDataCaptor),
1481 eq(true),
1482 eq(0),
1483 eq(false)
1484 )
1485 assertThat(mediaDataCaptor.value.actions.size)
1486 .isEqualTo(MediaDataManager.MAX_NOTIFICATION_ACTIONS)
1487 }
1488
1489 @Test
testPlaybackActions_noState_usesNotificationnull1490 fun testPlaybackActions_noState_usesNotification() {
1491 val desc = "Notification Action"
1492 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
1493 whenever(controller.playbackState).thenReturn(null)
1494
1495 val notifWithAction =
1496 SbnBuilder().run {
1497 setPkg(PACKAGE_NAME)
1498 modifyNotification(context).also {
1499 it.setSmallIcon(android.R.drawable.ic_media_pause)
1500 it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
1501 it.addAction(android.R.drawable.ic_media_play, desc, null)
1502 }
1503 build()
1504 }
1505 mediaDataManager.onNotificationAdded(KEY, notifWithAction)
1506
1507 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
1508 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
1509 verify(listener)
1510 .onMediaDataLoaded(
1511 eq(KEY),
1512 eq(null),
1513 capture(mediaDataCaptor),
1514 eq(true),
1515 eq(0),
1516 eq(false)
1517 )
1518
1519 assertThat(mediaDataCaptor.value!!.semanticActions).isNull()
1520 assertThat(mediaDataCaptor.value!!.actions).hasSize(1)
1521 assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc)
1522 }
1523
1524 @Test
testPlaybackActions_hasPrevNextnull1525 fun testPlaybackActions_hasPrevNext() {
1526 val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
1527 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
1528 val stateActions =
1529 PlaybackState.ACTION_PLAY or
1530 PlaybackState.ACTION_SKIP_TO_PREVIOUS or
1531 PlaybackState.ACTION_SKIP_TO_NEXT
1532 val stateBuilder = PlaybackState.Builder().setActions(stateActions)
1533 customDesc.forEach {
1534 stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
1535 }
1536 whenever(controller.playbackState).thenReturn(stateBuilder.build())
1537
1538 addNotificationAndLoad()
1539
1540 assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
1541 val actions = mediaDataCaptor.value!!.semanticActions!!
1542
1543 assertThat(actions.playOrPause).isNotNull()
1544 assertThat(actions.playOrPause!!.contentDescription)
1545 .isEqualTo(context.getString(R.string.controls_media_button_play))
1546 actions.playOrPause!!.action!!.run()
1547 verify(transportControls).play()
1548
1549 assertThat(actions.prevOrCustom).isNotNull()
1550 assertThat(actions.prevOrCustom!!.contentDescription)
1551 .isEqualTo(context.getString(R.string.controls_media_button_prev))
1552 actions.prevOrCustom!!.action!!.run()
1553 verify(transportControls).skipToPrevious()
1554
1555 assertThat(actions.nextOrCustom).isNotNull()
1556 assertThat(actions.nextOrCustom!!.contentDescription)
1557 .isEqualTo(context.getString(R.string.controls_media_button_next))
1558 actions.nextOrCustom!!.action!!.run()
1559 verify(transportControls).skipToNext()
1560
1561 assertThat(actions.custom0).isNotNull()
1562 assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[0])
1563
1564 assertThat(actions.custom1).isNotNull()
1565 assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1])
1566 }
1567
1568 @Test
testPlaybackActions_noPrevNext_usesCustomnull1569 fun testPlaybackActions_noPrevNext_usesCustom() {
1570 val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5")
1571 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
1572 val stateActions = PlaybackState.ACTION_PLAY
1573 val stateBuilder = PlaybackState.Builder().setActions(stateActions)
1574 customDesc.forEach {
1575 stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
1576 }
1577 whenever(controller.playbackState).thenReturn(stateBuilder.build())
1578
1579 addNotificationAndLoad()
1580
1581 assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
1582 val actions = mediaDataCaptor.value!!.semanticActions!!
1583
1584 assertThat(actions.playOrPause).isNotNull()
1585 assertThat(actions.playOrPause!!.contentDescription)
1586 .isEqualTo(context.getString(R.string.controls_media_button_play))
1587
1588 assertThat(actions.prevOrCustom).isNotNull()
1589 assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo(customDesc[0])
1590
1591 assertThat(actions.nextOrCustom).isNotNull()
1592 assertThat(actions.nextOrCustom!!.contentDescription).isEqualTo(customDesc[1])
1593
1594 assertThat(actions.custom0).isNotNull()
1595 assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[2])
1596
1597 assertThat(actions.custom1).isNotNull()
1598 assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3])
1599 }
1600
1601 @Test
testPlaybackActions_connectingnull1602 fun testPlaybackActions_connecting() {
1603 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
1604 val stateActions = PlaybackState.ACTION_PLAY
1605 val stateBuilder =
1606 PlaybackState.Builder()
1607 .setState(PlaybackState.STATE_BUFFERING, 0, 10f)
1608 .setActions(stateActions)
1609 whenever(controller.playbackState).thenReturn(stateBuilder.build())
1610
1611 addNotificationAndLoad()
1612
1613 assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
1614 val actions = mediaDataCaptor.value!!.semanticActions!!
1615
1616 assertThat(actions.playOrPause).isNotNull()
1617 assertThat(actions.playOrPause!!.contentDescription)
1618 .isEqualTo(context.getString(R.string.controls_media_button_connecting))
1619 }
1620
1621 @Test
testPlaybackActions_reservedSpacenull1622 fun testPlaybackActions_reservedSpace() {
1623 val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
1624 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
1625 val stateActions = PlaybackState.ACTION_PLAY
1626 val stateBuilder = PlaybackState.Builder().setActions(stateActions)
1627 customDesc.forEach {
1628 stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
1629 }
1630 val extras =
1631 Bundle().apply {
1632 putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
1633 putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
1634 }
1635 whenever(controller.playbackState).thenReturn(stateBuilder.build())
1636 whenever(controller.extras).thenReturn(extras)
1637
1638 addNotificationAndLoad()
1639
1640 assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
1641 val actions = mediaDataCaptor.value!!.semanticActions!!
1642
1643 assertThat(actions.playOrPause).isNotNull()
1644 assertThat(actions.playOrPause!!.contentDescription)
1645 .isEqualTo(context.getString(R.string.controls_media_button_play))
1646
1647 assertThat(actions.prevOrCustom).isNull()
1648 assertThat(actions.nextOrCustom).isNull()
1649
1650 assertThat(actions.custom0).isNotNull()
1651 assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[0])
1652
1653 assertThat(actions.custom1).isNotNull()
1654 assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1])
1655
1656 assertThat(actions.reserveNext).isTrue()
1657 assertThat(actions.reservePrev).isTrue()
1658 }
1659
1660 @Test
testPlaybackActions_playPause_hasButtonnull1661 fun testPlaybackActions_playPause_hasButton() {
1662 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
1663 val stateActions = PlaybackState.ACTION_PLAY_PAUSE
1664 val stateBuilder = PlaybackState.Builder().setActions(stateActions)
1665 whenever(controller.playbackState).thenReturn(stateBuilder.build())
1666
1667 addNotificationAndLoad()
1668
1669 assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
1670 val actions = mediaDataCaptor.value!!.semanticActions!!
1671
1672 assertThat(actions.playOrPause).isNotNull()
1673 assertThat(actions.playOrPause!!.contentDescription)
1674 .isEqualTo(context.getString(R.string.controls_media_button_play))
1675 actions.playOrPause!!.action!!.run()
1676 verify(transportControls).play()
1677 }
1678
1679 @Test
testPlaybackLocationChange_isLoggednull1680 fun testPlaybackLocationChange_isLogged() {
1681 // Media control added for local playback
1682 addNotificationAndLoad()
1683 val instanceId = mediaDataCaptor.value.instanceId
1684
1685 // Location is updated to local cast
1686 whenever(playbackInfo.playbackType)
1687 .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
1688 addNotificationAndLoad()
1689 verify(logger)
1690 .logPlaybackLocationChange(
1691 anyInt(),
1692 eq(PACKAGE_NAME),
1693 eq(instanceId),
1694 eq(MediaData.PLAYBACK_CAST_LOCAL)
1695 )
1696
1697 // update to remote cast
1698 mediaDataManager.onNotificationAdded(KEY, remoteCastNotification)
1699 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
1700 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
1701 verify(logger)
1702 .logPlaybackLocationChange(
1703 anyInt(),
1704 eq(SYSTEM_PACKAGE_NAME),
1705 eq(instanceId),
1706 eq(MediaData.PLAYBACK_CAST_REMOTE)
1707 )
1708 }
1709
1710 @Test
testPlaybackStateChange_keyExists_callsListenernull1711 fun testPlaybackStateChange_keyExists_callsListener() {
1712 // Notification has been added
1713 addNotificationAndLoad()
1714
1715 // Callback gets an updated state
1716 val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
1717 stateCallbackCaptor.value.invoke(KEY, state)
1718
1719 // Listener is notified of updated state
1720 verify(listener)
1721 .onMediaDataLoaded(
1722 eq(KEY),
1723 eq(KEY),
1724 capture(mediaDataCaptor),
1725 eq(true),
1726 eq(0),
1727 eq(false)
1728 )
1729 assertThat(mediaDataCaptor.value.isPlaying).isTrue()
1730 }
1731
1732 @Test
testPlaybackStateChange_keyDoesNotExist_doesNothingnull1733 fun testPlaybackStateChange_keyDoesNotExist_doesNothing() {
1734 val state = PlaybackState.Builder().build()
1735
1736 // No media added with this key
1737
1738 stateCallbackCaptor.value.invoke(KEY, state)
1739 verify(listener, never())
1740 .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
1741 }
1742
1743 @Test
testPlaybackStateChange_keyHasNullToken_doesNothingnull1744 fun testPlaybackStateChange_keyHasNullToken_doesNothing() {
1745 // When we get an update that sets the data's token to null
1746 addNotificationAndLoad()
1747 val data = mediaDataCaptor.value
1748 assertThat(data.resumption).isFalse()
1749 mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(token = null))
1750
1751 // And then get a state update
1752 val state = PlaybackState.Builder().build()
1753
1754 // Then no changes are made
1755 stateCallbackCaptor.value.invoke(KEY, state)
1756 verify(listener, never())
1757 .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
1758 }
1759
1760 @Test
testPlaybackState_PauseWhenFlagTrue_keyExists_callsListenernull1761 fun testPlaybackState_PauseWhenFlagTrue_keyExists_callsListener() {
1762 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
1763 val state = PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 1f).build()
1764 whenever(controller.playbackState).thenReturn(state)
1765
1766 addNotificationAndLoad()
1767 stateCallbackCaptor.value.invoke(KEY, state)
1768
1769 verify(listener)
1770 .onMediaDataLoaded(
1771 eq(KEY),
1772 eq(KEY),
1773 capture(mediaDataCaptor),
1774 eq(true),
1775 eq(0),
1776 eq(false)
1777 )
1778 assertThat(mediaDataCaptor.value.isPlaying).isFalse()
1779 assertThat(mediaDataCaptor.value.semanticActions).isNotNull()
1780 }
1781
1782 @Test
testPlaybackState_PauseStateAfterAddingResumption_keyExists_callsListenernull1783 fun testPlaybackState_PauseStateAfterAddingResumption_keyExists_callsListener() {
1784 val desc =
1785 MediaDescription.Builder().run {
1786 setTitle(SESSION_TITLE)
1787 build()
1788 }
1789 val state =
1790 PlaybackState.Builder()
1791 .setState(PlaybackState.STATE_PAUSED, 0L, 1f)
1792 .setActions(PlaybackState.ACTION_PLAY_PAUSE)
1793 .build()
1794
1795 // Add resumption controls in order to have semantic actions.
1796 // To make sure that they are not null after changing state.
1797 mediaDataManager.addResumptionControls(
1798 USER_ID,
1799 desc,
1800 Runnable {},
1801 session.sessionToken,
1802 APP_NAME,
1803 pendingIntent,
1804 PACKAGE_NAME
1805 )
1806 backgroundExecutor.runAllReady()
1807 foregroundExecutor.runAllReady()
1808
1809 stateCallbackCaptor.value.invoke(PACKAGE_NAME, state)
1810
1811 verify(listener)
1812 .onMediaDataLoaded(
1813 eq(PACKAGE_NAME),
1814 eq(PACKAGE_NAME),
1815 capture(mediaDataCaptor),
1816 eq(true),
1817 eq(0),
1818 eq(false)
1819 )
1820 assertThat(mediaDataCaptor.value.isPlaying).isFalse()
1821 assertThat(mediaDataCaptor.value.semanticActions).isNotNull()
1822 }
1823
1824 @Test
testPlaybackStateNull_Pause_keyExists_callsListenernull1825 fun testPlaybackStateNull_Pause_keyExists_callsListener() {
1826 whenever(controller.playbackState).thenReturn(null)
1827 val state =
1828 PlaybackState.Builder()
1829 .setState(PlaybackState.STATE_PAUSED, 0L, 1f)
1830 .setActions(PlaybackState.ACTION_PLAY_PAUSE)
1831 .build()
1832
1833 addNotificationAndLoad()
1834 stateCallbackCaptor.value.invoke(KEY, state)
1835
1836 verify(listener)
1837 .onMediaDataLoaded(
1838 eq(KEY),
1839 eq(KEY),
1840 capture(mediaDataCaptor),
1841 eq(true),
1842 eq(0),
1843 eq(false)
1844 )
1845 assertThat(mediaDataCaptor.value.isPlaying).isFalse()
1846 assertThat(mediaDataCaptor.value.semanticActions).isNull()
1847 }
1848
1849 @Test
testNoClearNotOngoing_canDismissnull1850 fun testNoClearNotOngoing_canDismiss() {
1851 mediaNotification =
1852 SbnBuilder().run {
1853 setPkg(PACKAGE_NAME)
1854 modifyNotification(context).also {
1855 it.setSmallIcon(android.R.drawable.ic_media_pause)
1856 it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
1857 it.setOngoing(false)
1858 it.setFlag(FLAG_NO_CLEAR, true)
1859 }
1860 build()
1861 }
1862 addNotificationAndLoad()
1863 assertThat(mediaDataCaptor.value.isClearable).isTrue()
1864 }
1865
1866 @Test
testOngoing_cannotDismissnull1867 fun testOngoing_cannotDismiss() {
1868 mediaNotification =
1869 SbnBuilder().run {
1870 setPkg(PACKAGE_NAME)
1871 modifyNotification(context).also {
1872 it.setSmallIcon(android.R.drawable.ic_media_pause)
1873 it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
1874 it.setOngoing(true)
1875 }
1876 build()
1877 }
1878 addNotificationAndLoad()
1879 assertThat(mediaDataCaptor.value.isClearable).isFalse()
1880 }
1881
1882 @Test
testRetain_notifPlayer_notifRemoved_setToResumenull1883 fun testRetain_notifPlayer_notifRemoved_setToResume() {
1884 whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
1885
1886 // When a media control based on notification is added, times out, and then removed
1887 addNotificationAndLoad()
1888 mediaDataManager.setTimedOut(KEY, timedOut = true)
1889 assertThat(mediaDataCaptor.value.active).isFalse()
1890 mediaDataManager.onNotificationRemoved(KEY)
1891
1892 // It is converted to a resume player
1893 verify(listener)
1894 .onMediaDataLoaded(
1895 eq(PACKAGE_NAME),
1896 eq(KEY),
1897 capture(mediaDataCaptor),
1898 eq(true),
1899 eq(0),
1900 eq(false)
1901 )
1902 assertThat(mediaDataCaptor.value.resumption).isTrue()
1903 assertThat(mediaDataCaptor.value.active).isFalse()
1904 verify(logger)
1905 .logActiveConvertedToResume(
1906 anyInt(),
1907 eq(PACKAGE_NAME),
1908 eq(mediaDataCaptor.value.instanceId)
1909 )
1910 }
1911
1912 @Test
testRetain_notifPlayer_sessionDestroyed_doesNotChangenull1913 fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() {
1914 whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
1915
1916 // When a media control based on notification is added and times out
1917 addNotificationAndLoad()
1918 mediaDataManager.setTimedOut(KEY, timedOut = true)
1919 assertThat(mediaDataCaptor.value.active).isFalse()
1920
1921 // and then the session is destroyed
1922 sessionCallbackCaptor.value.invoke(KEY)
1923
1924 // It remains as a regular player
1925 verify(listener, never()).onMediaDataRemoved(eq(KEY))
1926 verify(listener, never())
1927 .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
1928 }
1929
1930 @Test
testRetain_notifPlayer_removeWhileActive_fullyRemovednull1931 fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() {
1932 whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
1933
1934 // When a media control based on notification is added and then removed, without timing out
1935 addNotificationAndLoad()
1936 val data = mediaDataCaptor.value
1937 assertThat(data.active).isTrue()
1938 mediaDataManager.onNotificationRemoved(KEY)
1939
1940 // It is fully removed
1941 verify(listener).onMediaDataRemoved(eq(KEY))
1942 verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
1943 verify(listener, never())
1944 .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
1945 }
1946
1947 @Test
testRetain_canResume_removeWhileActive_setToResumenull1948 fun testRetain_canResume_removeWhileActive_setToResume() {
1949 whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
1950
1951 // When a media control that supports resumption is added
1952 addNotificationAndLoad()
1953 val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {})
1954 mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable)
1955
1956 // And then removed while still active
1957 mediaDataManager.onNotificationRemoved(KEY)
1958
1959 // It is converted to a resume player
1960 verify(listener)
1961 .onMediaDataLoaded(
1962 eq(PACKAGE_NAME),
1963 eq(KEY),
1964 capture(mediaDataCaptor),
1965 eq(true),
1966 eq(0),
1967 eq(false)
1968 )
1969 assertThat(mediaDataCaptor.value.resumption).isTrue()
1970 assertThat(mediaDataCaptor.value.active).isFalse()
1971 verify(logger)
1972 .logActiveConvertedToResume(
1973 anyInt(),
1974 eq(PACKAGE_NAME),
1975 eq(mediaDataCaptor.value.instanceId)
1976 )
1977 }
1978
1979 @Test
testRetain_sessionPlayer_notifRemoved_doesNotChangenull1980 fun testRetain_sessionPlayer_notifRemoved_doesNotChange() {
1981 whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
1982 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
1983 addPlaybackStateAction()
1984
1985 // When a media control with PlaybackState actions is added, times out,
1986 // and then the notification is removed
1987 addNotificationAndLoad()
1988 val data = mediaDataCaptor.value
1989 assertThat(data.active).isTrue()
1990 mediaDataManager.setTimedOut(KEY, timedOut = true)
1991 mediaDataManager.onNotificationRemoved(KEY)
1992
1993 // It remains as a regular player
1994 verify(listener, never()).onMediaDataRemoved(eq(KEY))
1995 verify(listener, never())
1996 .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
1997 }
1998
1999 @Test
testRetain_sessionPlayer_sessionDestroyed_setToResumenull2000 fun testRetain_sessionPlayer_sessionDestroyed_setToResume() {
2001 whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
2002 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
2003 addPlaybackStateAction()
2004
2005 // When a media control with PlaybackState actions is added, times out,
2006 // and then the session is destroyed
2007 addNotificationAndLoad()
2008 val data = mediaDataCaptor.value
2009 assertThat(data.active).isTrue()
2010 mediaDataManager.setTimedOut(KEY, timedOut = true)
2011 sessionCallbackCaptor.value.invoke(KEY)
2012
2013 // It is converted to a resume player
2014 verify(listener)
2015 .onMediaDataLoaded(
2016 eq(PACKAGE_NAME),
2017 eq(KEY),
2018 capture(mediaDataCaptor),
2019 eq(true),
2020 eq(0),
2021 eq(false)
2022 )
2023 assertThat(mediaDataCaptor.value.resumption).isTrue()
2024 assertThat(mediaDataCaptor.value.active).isFalse()
2025 verify(logger)
2026 .logActiveConvertedToResume(
2027 anyInt(),
2028 eq(PACKAGE_NAME),
2029 eq(mediaDataCaptor.value.instanceId)
2030 )
2031 }
2032
2033 @Test
testRetain_sessionPlayer_destroyedWhileActive_noResume_fullyRemovednull2034 fun testRetain_sessionPlayer_destroyedWhileActive_noResume_fullyRemoved() {
2035 whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
2036 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
2037 addPlaybackStateAction()
2038
2039 // When a media control using session actions is added, and then the session is destroyed
2040 // without timing out first
2041 addNotificationAndLoad()
2042 val data = mediaDataCaptor.value
2043 assertThat(data.active).isTrue()
2044 sessionCallbackCaptor.value.invoke(KEY)
2045
2046 // It is fully removed
2047 verify(listener).onMediaDataRemoved(eq(KEY))
2048 verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
2049 verify(listener, never())
2050 .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
2051 }
2052
2053 @Test
testRetain_sessionPlayer_canResume_destroyedWhileActive_setToResumenull2054 fun testRetain_sessionPlayer_canResume_destroyedWhileActive_setToResume() {
2055 whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
2056 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
2057 addPlaybackStateAction()
2058
2059 // When a media control using session actions and that does allow resumption is added,
2060 addNotificationAndLoad()
2061 val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {})
2062 mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable)
2063
2064 // And then the session is destroyed without timing out first
2065 sessionCallbackCaptor.value.invoke(KEY)
2066
2067 // It is converted to a resume player
2068 verify(listener)
2069 .onMediaDataLoaded(
2070 eq(PACKAGE_NAME),
2071 eq(KEY),
2072 capture(mediaDataCaptor),
2073 eq(true),
2074 eq(0),
2075 eq(false)
2076 )
2077 assertThat(mediaDataCaptor.value.resumption).isTrue()
2078 assertThat(mediaDataCaptor.value.active).isFalse()
2079 verify(logger)
2080 .logActiveConvertedToResume(
2081 anyInt(),
2082 eq(PACKAGE_NAME),
2083 eq(mediaDataCaptor.value.instanceId)
2084 )
2085 }
2086
2087 @Test
testSessionDestroyed_noNotificationKey_stillRemovednull2088 fun testSessionDestroyed_noNotificationKey_stillRemoved() {
2089 whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
2090 whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
2091
2092 // When a notiifcation is added and then removed before it is fully processed
2093 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
2094 backgroundExecutor.runAllReady()
2095 mediaDataManager.onNotificationRemoved(KEY)
2096
2097 // We still make sure to remove it
2098 verify(listener).onMediaDataRemoved(eq(KEY))
2099 }
2100
2101 /** Helper function to add a basic media notification and capture the resulting MediaData */
addNotificationAndLoadnull2102 private fun addNotificationAndLoad() {
2103 addNotificationAndLoad(mediaNotification)
2104 }
2105
2106 /** Helper function to add the given notification and capture the resulting MediaData */
addNotificationAndLoadnull2107 private fun addNotificationAndLoad(sbn: StatusBarNotification) {
2108 mediaDataManager.onNotificationAdded(KEY, sbn)
2109 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
2110 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
2111 verify(listener)
2112 .onMediaDataLoaded(
2113 eq(KEY),
2114 eq(null),
2115 capture(mediaDataCaptor),
2116 eq(true),
2117 eq(0),
2118 eq(false)
2119 )
2120 }
2121
2122 /** Helper function to set up a PlaybackState with action */
addPlaybackStateActionnull2123 private fun addPlaybackStateAction() {
2124 val stateActions = PlaybackState.ACTION_PLAY_PAUSE
2125 val stateBuilder = PlaybackState.Builder().setActions(stateActions)
2126 stateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 1.0f)
2127 whenever(controller.playbackState).thenReturn(stateBuilder.build())
2128 }
2129
2130 /** Helper function to add a resumption control and capture the resulting MediaData */
addResumeControlAndLoadnull2131 private fun addResumeControlAndLoad(
2132 desc: MediaDescription,
2133 packageName: String = PACKAGE_NAME
2134 ) {
2135 mediaDataManager.addResumptionControls(
2136 USER_ID,
2137 desc,
2138 Runnable {},
2139 session.sessionToken,
2140 APP_NAME,
2141 pendingIntent,
2142 packageName
2143 )
2144 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
2145 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
2146
2147 verify(listener)
2148 .onMediaDataLoaded(
2149 eq(packageName),
2150 eq(null),
2151 capture(mediaDataCaptor),
2152 eq(true),
2153 eq(0),
2154 eq(false)
2155 )
2156 }
2157 }
2158