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