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