• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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