1 /*
<lambda>null2  * Copyright (C) 2020 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.ui.controller
18 
19 import android.animation.Animator
20 import android.animation.AnimatorSet
21 import android.app.PendingIntent
22 import android.app.smartspace.SmartspaceAction
23 import android.content.Context
24 import android.content.Intent
25 import android.content.pm.ApplicationInfo
26 import android.content.pm.PackageManager
27 import android.content.res.Configuration
28 import android.graphics.Bitmap
29 import android.graphics.Canvas
30 import android.graphics.Color
31 import android.graphics.Matrix
32 import android.graphics.drawable.Animatable2
33 import android.graphics.drawable.AnimatedVectorDrawable
34 import android.graphics.drawable.Drawable
35 import android.graphics.drawable.GradientDrawable
36 import android.graphics.drawable.Icon
37 import android.graphics.drawable.RippleDrawable
38 import android.graphics.drawable.TransitionDrawable
39 import android.media.MediaMetadata
40 import android.media.session.MediaSession
41 import android.media.session.PlaybackState
42 import android.os.Bundle
43 import android.platform.test.annotations.DisableFlags
44 import android.platform.test.annotations.EnableFlags
45 import android.platform.test.annotations.RequiresFlagsEnabled
46 import android.platform.test.flag.junit.DeviceFlagsValueProvider
47 import android.provider.Settings
48 import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
49 import android.testing.TestableLooper
50 import android.util.TypedValue
51 import android.view.View
52 import android.view.ViewGroup
53 import android.view.animation.Interpolator
54 import android.widget.FrameLayout
55 import android.widget.ImageButton
56 import android.widget.ImageView
57 import android.widget.SeekBar
58 import android.widget.TextView
59 import androidx.constraintlayout.widget.Barrier
60 import androidx.constraintlayout.widget.ConstraintSet
61 import androidx.lifecycle.LiveData
62 import androidx.media.utils.MediaConstants
63 import androidx.test.ext.junit.runners.AndroidJUnit4
64 import androidx.test.filters.SmallTest
65 import com.android.internal.logging.InstanceId
66 import com.android.internal.widget.CachingIconView
67 import com.android.systemui.ActivityIntentHelper
68 import com.android.systemui.Flags
69 import com.android.systemui.SysuiTestCase
70 import com.android.systemui.bluetooth.BroadcastDialogController
71 import com.android.systemui.broadcast.BroadcastSender
72 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
73 import com.android.systemui.flags.DisableSceneContainer
74 import com.android.systemui.media.controls.MediaTestUtils
75 import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
76 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
77 import com.android.systemui.media.controls.shared.model.KEY_SMARTSPACE_APP_NAME
78 import com.android.systemui.media.controls.shared.model.MediaAction
79 import com.android.systemui.media.controls.shared.model.MediaButton
80 import com.android.systemui.media.controls.shared.model.MediaData
81 import com.android.systemui.media.controls.shared.model.MediaDeviceData
82 import com.android.systemui.media.controls.shared.model.MediaNotificationAction
83 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
84 import com.android.systemui.media.controls.ui.binder.SeekBarObserver
85 import com.android.systemui.media.controls.ui.view.GutsViewHolder
86 import com.android.systemui.media.controls.ui.view.MediaViewHolder
87 import com.android.systemui.media.controls.ui.view.RecommendationViewHolder
88 import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
89 import com.android.systemui.media.controls.util.MediaUiEventLogger
90 import com.android.systemui.media.dialog.MediaOutputDialogManager
91 import com.android.systemui.monet.ColorScheme
92 import com.android.systemui.monet.Style
93 import com.android.systemui.plugins.ActivityStarter
94 import com.android.systemui.plugins.FalsingManager
95 import com.android.systemui.res.R
96 import com.android.systemui.statusbar.NotificationLockscreenUserManager
97 import com.android.systemui.statusbar.policy.KeyguardStateController
98 import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView
99 import com.android.systemui.surfaceeffects.ripple.MultiRippleView
100 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
101 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
102 import com.android.systemui.util.animation.TransitionLayout
103 import com.android.systemui.util.concurrency.FakeExecutor
104 import com.android.systemui.util.settings.GlobalSettings
105 import com.android.systemui.util.time.FakeSystemClock
106 import com.google.common.truth.Truth.assertThat
107 import dagger.Lazy
108 import junit.framework.Assert.assertTrue
109 import org.junit.After
110 import org.junit.Before
111 import org.junit.Rule
112 import org.junit.Test
113 import org.junit.runner.RunWith
114 import org.mockito.ArgumentCaptor
115 import org.mockito.ArgumentMatchers.anyInt
116 import org.mockito.ArgumentMatchers.anyLong
117 import org.mockito.Mock
118 import org.mockito.Mockito.anyString
119 import org.mockito.Mockito.mock
120 import org.mockito.Mockito.never
121 import org.mockito.Mockito.reset
122 import org.mockito.Mockito.times
123 import org.mockito.Mockito.verify
124 import org.mockito.Mockito.`when` as whenever
125 import org.mockito.junit.MockitoJUnit
126 import org.mockito.kotlin.any
127 import org.mockito.kotlin.argumentCaptor
128 import org.mockito.kotlin.eq
129 
130 private const val KEY = "TEST_KEY"
131 private const val PACKAGE = "PKG"
132 private const val ARTIST = "ARTIST"
133 private const val TITLE = "TITLE"
134 private const val DEVICE_NAME = "DEVICE_NAME"
135 private const val SESSION_KEY = "SESSION_KEY"
136 private const val SESSION_ARTIST = "SESSION_ARTIST"
137 private const val SESSION_TITLE = "SESSION_TITLE"
138 private const val DISABLED_DEVICE_NAME = "DISABLED_DEVICE_NAME"
139 private const val REC_APP_NAME = "REC APP NAME"
140 private const val APP_NAME = "APP_NAME"
141 
142 @SmallTest
143 @RunWith(AndroidJUnit4::class)
144 @TestableLooper.RunWithLooper(setAsMainLooper = true)
145 @DisableSceneContainer
146 public class MediaControlPanelTest : SysuiTestCase() {
147     @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
148 
149     private lateinit var player: MediaControlPanel
150 
151     private lateinit var bgExecutor: FakeExecutor
152     private lateinit var mainExecutor: FakeExecutor
153     @Mock private lateinit var activityStarter: ActivityStarter
154     @Mock private lateinit var broadcastSender: BroadcastSender
155 
156     @Mock private lateinit var gutsViewHolder: GutsViewHolder
157     @Mock private lateinit var viewHolder: MediaViewHolder
158     @Mock private lateinit var view: TransitionLayout
159     @Mock private lateinit var seekBarViewModel: SeekBarViewModel
160     @Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress>
161     @Mock private lateinit var mediaViewController: MediaViewController
162     @Mock private lateinit var mediaDataManager: MediaDataManager
163     @Mock private lateinit var expandedSet: ConstraintSet
164     @Mock private lateinit var collapsedSet: ConstraintSet
165     @Mock private lateinit var mediaOutputDialogManager: MediaOutputDialogManager
166     @Mock private lateinit var mediaCarouselController: MediaCarouselController
167     @Mock private lateinit var falsingManager: FalsingManager
168     @Mock private lateinit var transitionParent: ViewGroup
169     @Mock private lateinit var broadcastDialogController: BroadcastDialogController
170     private lateinit var appIcon: ImageView
171     @Mock private lateinit var albumView: ImageView
172     private lateinit var titleText: TextView
173     private lateinit var artistText: TextView
174     private lateinit var explicitIndicator: CachingIconView
175     private lateinit var seamless: ViewGroup
176     private lateinit var seamlessButton: View
177     @Mock private lateinit var seamlessBackground: RippleDrawable
178     private lateinit var seamlessIcon: ImageView
179     private lateinit var seamlessText: TextView
180     private lateinit var seekBar: SeekBar
181     private lateinit var action0: ImageButton
182     private lateinit var action1: ImageButton
183     private lateinit var action2: ImageButton
184     private lateinit var action3: ImageButton
185     private lateinit var action4: ImageButton
186     private lateinit var actionPlayPause: ImageButton
187     private lateinit var actionNext: ImageButton
188     private lateinit var actionPrev: ImageButton
189     private lateinit var scrubbingElapsedTimeView: TextView
190     private lateinit var scrubbingTotalTimeView: TextView
191     private lateinit var actionsTopBarrier: Barrier
192     @Mock private lateinit var gutsText: TextView
193     @Mock private lateinit var mockAnimator: AnimatorSet
194     private lateinit var settings: ImageButton
195     private lateinit var cancel: View
196     private lateinit var cancelText: TextView
197     private lateinit var dismiss: FrameLayout
198     private lateinit var dismissText: TextView
199     private lateinit var multiRippleView: MultiRippleView
200     private lateinit var turbulenceNoiseView: TurbulenceNoiseView
201     private lateinit var loadingEffectView: LoadingEffectView
202 
203     private lateinit var session: MediaSession
204     private lateinit var device: MediaDeviceData
205     private val disabledDevice =
206         MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null, showBroadcastButton = false)
207     private lateinit var mediaData: MediaData
208     private val clock = FakeSystemClock()
209     @Mock private lateinit var logger: MediaUiEventLogger
210     @Mock private lateinit var instanceId: InstanceId
211     @Mock private lateinit var packageManager: PackageManager
212     @Mock private lateinit var applicationInfo: ApplicationInfo
213     @Mock private lateinit var keyguardStateController: KeyguardStateController
214     @Mock private lateinit var activityIntentHelper: ActivityIntentHelper
215     @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
216 
217     @Mock private lateinit var communalSceneInteractor: CommunalSceneInteractor
218 
219     @Mock private lateinit var recommendationViewHolder: RecommendationViewHolder
220     @Mock private lateinit var smartspaceAction: SmartspaceAction
221     private lateinit var smartspaceData: SmartspaceMediaData
222     @Mock private lateinit var coverContainer1: ViewGroup
223     @Mock private lateinit var coverContainer2: ViewGroup
224     @Mock private lateinit var coverContainer3: ViewGroup
225     @Mock private lateinit var recAppIconItem: CachingIconView
226     @Mock private lateinit var recCardTitle: TextView
227     @Mock private lateinit var coverItem: ImageView
228     @Mock private lateinit var matrix: Matrix
229     private lateinit var recTitle1: TextView
230     private lateinit var recTitle2: TextView
231     private lateinit var recTitle3: TextView
232     private lateinit var recSubtitle1: TextView
233     private lateinit var recSubtitle2: TextView
234     private lateinit var recSubtitle3: TextView
235     @Mock private lateinit var recProgressBar1: SeekBar
236     @Mock private lateinit var recProgressBar2: SeekBar
237     @Mock private lateinit var recProgressBar3: SeekBar
238     @Mock private lateinit var globalSettings: GlobalSettings
239 
240     private val intent =
241         Intent().apply {
242             putExtras(Bundle().also { it.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME) })
243             setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
244         }
245     private val pendingIntent =
246         PendingIntent.getActivity(
247             mContext,
248             0,
249             intent.setPackage(mContext.packageName),
250             PendingIntent.FLAG_MUTABLE
251         )
252 
253     @JvmField @Rule val mockito = MockitoJUnit.rule()
254 
255     @Before
256     fun setUp() {
257         bgExecutor = FakeExecutor(clock)
258         mainExecutor = FakeExecutor(clock)
259         whenever(mediaViewController.expandedLayout).thenReturn(expandedSet)
260         whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
261 
262         // Set up package manager mocks
263         val icon = context.getDrawable(R.drawable.ic_android)
264         whenever(packageManager.getApplicationIcon(anyString())).thenReturn(icon)
265         whenever(packageManager.getApplicationIcon(any<ApplicationInfo>())).thenReturn(icon)
266         whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt()))
267             .thenReturn(applicationInfo)
268         whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE)
269         context.setMockPackageManager(packageManager)
270 
271         player =
272             object :
273                 MediaControlPanel(
274                     context,
275                     bgExecutor,
276                     mainExecutor,
277                     activityStarter,
278                     broadcastSender,
279                     mediaViewController,
280                     seekBarViewModel,
281                     Lazy { mediaDataManager },
282                     mediaOutputDialogManager,
283                     mediaCarouselController,
284                     falsingManager,
285                     clock,
286                     logger,
287                     keyguardStateController,
288                     activityIntentHelper,
289                     communalSceneInteractor,
290                     lockscreenUserManager,
291                     broadcastDialogController,
292                     globalSettings,
293                 ) {
294                 override fun loadAnimator(
295                     animId: Int,
296                     otionInterpolator: Interpolator,
297                     vararg targets: View
298                 ): AnimatorSet {
299                     return mockAnimator
300                 }
301             }
302 
303         initGutsViewHolderMocks()
304         initMediaViewHolderMocks()
305 
306         initDeviceMediaData(false, DEVICE_NAME)
307 
308         // Set up recommendation view
309         initRecommendationViewHolderMocks()
310 
311         // Set valid recommendation data
312         val extras = Bundle()
313         extras.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME)
314         val intent =
315             Intent().apply {
316                 putExtras(extras)
317                 setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
318             }
319         whenever(smartspaceAction.intent).thenReturn(intent)
320         whenever(smartspaceAction.extras).thenReturn(extras)
321         smartspaceData =
322             EMPTY_SMARTSPACE_MEDIA_DATA.copy(
323                 packageName = PACKAGE,
324                 instanceId = instanceId,
325                 recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction),
326                 cardAction = smartspaceAction
327             )
328     }
329 
330     private fun initGutsViewHolderMocks() {
331         settings = ImageButton(context)
332         cancel = View(context)
333         cancelText = TextView(context)
334         dismiss = FrameLayout(context)
335         dismissText = TextView(context)
336         whenever(gutsViewHolder.gutsText).thenReturn(gutsText)
337         whenever(gutsViewHolder.settings).thenReturn(settings)
338         whenever(gutsViewHolder.cancel).thenReturn(cancel)
339         whenever(gutsViewHolder.cancelText).thenReturn(cancelText)
340         whenever(gutsViewHolder.dismiss).thenReturn(dismiss)
341         whenever(gutsViewHolder.dismissText).thenReturn(dismissText)
342     }
343 
344     private fun initDeviceMediaData(shouldShowBroadcastButton: Boolean, name: String) {
345         device =
346             MediaDeviceData(true, null, name, null, showBroadcastButton = shouldShowBroadcastButton)
347 
348         // Create media session
349         val metadataBuilder =
350             MediaMetadata.Builder().apply {
351                 putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
352                 putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
353             }
354         val playbackBuilder =
355             PlaybackState.Builder().apply {
356                 setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
357                 setActions(PlaybackState.ACTION_PLAY)
358             }
359         session =
360             MediaSession(context, SESSION_KEY).apply {
361                 setMetadata(metadataBuilder.build())
362                 setPlaybackState(playbackBuilder.build())
363             }
364         session.setActive(true)
365 
366         mediaData =
367             MediaTestUtils.emptyMediaData.copy(
368                 artist = ARTIST,
369                 song = TITLE,
370                 packageName = PACKAGE,
371                 token = session.sessionToken,
372                 device = device,
373                 instanceId = instanceId
374             )
375     }
376 
377     /** Initialize elements in media view holder */
378     private fun initMediaViewHolderMocks() {
379         whenever(seekBarViewModel.progress).thenReturn(seekBarData)
380 
381         // Set up mock views for the players
382         appIcon = ImageView(context)
383         titleText = TextView(context)
384         artistText = TextView(context)
385         explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator }
386         seamless = FrameLayout(context)
387         seamless.foreground = seamlessBackground
388         seamlessButton = View(context)
389         seamlessIcon = ImageView(context)
390         seamlessText = TextView(context)
391         seekBar = SeekBar(context).also { it.id = R.id.media_progress_bar }
392 
393         action0 = ImageButton(context).also { it.setId(R.id.action0) }
394         action1 = ImageButton(context).also { it.setId(R.id.action1) }
395         action2 = ImageButton(context).also { it.setId(R.id.action2) }
396         action3 = ImageButton(context).also { it.setId(R.id.action3) }
397         action4 = ImageButton(context).also { it.setId(R.id.action4) }
398 
399         actionPlayPause = ImageButton(context).also { it.setId(R.id.actionPlayPause) }
400         actionPrev = ImageButton(context).also { it.setId(R.id.actionPrev) }
401         actionNext = ImageButton(context).also { it.setId(R.id.actionNext) }
402         scrubbingElapsedTimeView =
403             TextView(context).also { it.setId(R.id.media_scrubbing_elapsed_time) }
404         scrubbingTotalTimeView =
405             TextView(context).also { it.setId(R.id.media_scrubbing_total_time) }
406 
407         actionsTopBarrier =
408             Barrier(context).also {
409                 it.id = R.id.media_action_barrier_top
410                 it.referencedIds =
411                     intArrayOf(
412                         actionPrev.id,
413                         seekBar.id,
414                         actionNext.id,
415                         action0.id,
416                         action1.id,
417                         action2.id,
418                         action3.id,
419                         action4.id
420                     )
421             }
422 
423         multiRippleView = MultiRippleView(context, null)
424         turbulenceNoiseView = TurbulenceNoiseView(context, null)
425         loadingEffectView = LoadingEffectView(context, null)
426 
427         whenever(viewHolder.player).thenReturn(view)
428         whenever(viewHolder.appIcon).thenReturn(appIcon)
429         whenever(viewHolder.albumView).thenReturn(albumView)
430         whenever(albumView.foreground).thenReturn(mock(Drawable::class.java))
431         whenever(viewHolder.titleText).thenReturn(titleText)
432         whenever(viewHolder.artistText).thenReturn(artistText)
433         whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator)
434         whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java))
435         whenever(viewHolder.seamless).thenReturn(seamless)
436         whenever(viewHolder.seamlessButton).thenReturn(seamlessButton)
437         whenever(viewHolder.seamlessIcon).thenReturn(seamlessIcon)
438         whenever(viewHolder.seamlessText).thenReturn(seamlessText)
439         whenever(viewHolder.seekBar).thenReturn(seekBar)
440         whenever(viewHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView)
441         whenever(viewHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView)
442 
443         whenever(viewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
444 
445         // Transition View
446         whenever(view.parent).thenReturn(transitionParent)
447         whenever(view.rootView).thenReturn(transitionParent)
448 
449         // Action buttons
450         whenever(viewHolder.actionPlayPause).thenReturn(actionPlayPause)
451         whenever(viewHolder.getAction(R.id.actionPlayPause)).thenReturn(actionPlayPause)
452         whenever(viewHolder.actionNext).thenReturn(actionNext)
453         whenever(viewHolder.getAction(R.id.actionNext)).thenReturn(actionNext)
454         whenever(viewHolder.actionPrev).thenReturn(actionPrev)
455         whenever(viewHolder.getAction(R.id.actionPrev)).thenReturn(actionPrev)
456         whenever(viewHolder.action0).thenReturn(action0)
457         whenever(viewHolder.getAction(R.id.action0)).thenReturn(action0)
458         whenever(viewHolder.action1).thenReturn(action1)
459         whenever(viewHolder.getAction(R.id.action1)).thenReturn(action1)
460         whenever(viewHolder.action2).thenReturn(action2)
461         whenever(viewHolder.getAction(R.id.action2)).thenReturn(action2)
462         whenever(viewHolder.action3).thenReturn(action3)
463         whenever(viewHolder.getAction(R.id.action3)).thenReturn(action3)
464         whenever(viewHolder.action4).thenReturn(action4)
465         whenever(viewHolder.getAction(R.id.action4)).thenReturn(action4)
466 
467         whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier)
468 
469         whenever(viewHolder.multiRippleView).thenReturn(multiRippleView)
470         whenever(viewHolder.turbulenceNoiseView).thenReturn(turbulenceNoiseView)
471         whenever(viewHolder.loadingEffectView).thenReturn(loadingEffectView)
472     }
473 
474     /** Initialize elements for the recommendation view holder */
475     private fun initRecommendationViewHolderMocks() {
476         recTitle1 = TextView(context)
477         recTitle2 = TextView(context)
478         recTitle3 = TextView(context)
479         recSubtitle1 = TextView(context)
480         recSubtitle2 = TextView(context)
481         recSubtitle3 = TextView(context)
482 
483         whenever(recommendationViewHolder.recommendations).thenReturn(view)
484         whenever(recommendationViewHolder.mediaAppIcons)
485             .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
486         whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
487         whenever(recommendationViewHolder.mediaCoverItems)
488             .thenReturn(listOf(coverItem, coverItem, coverItem))
489         whenever(recommendationViewHolder.mediaCoverContainers)
490             .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3))
491         whenever(recommendationViewHolder.mediaTitles)
492             .thenReturn(listOf(recTitle1, recTitle2, recTitle3))
493         whenever(recommendationViewHolder.mediaSubtitles)
494             .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3))
495         whenever(recommendationViewHolder.mediaProgressBars)
496             .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
497         whenever(coverItem.imageMatrix).thenReturn(matrix)
498 
499         // set ids for recommendation containers
500         whenever(coverContainer1.id).thenReturn(1)
501         whenever(coverContainer2.id).thenReturn(2)
502         whenever(coverContainer3.id).thenReturn(3)
503 
504         whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
505 
506         val actionIcon = Icon.createWithResource(context, R.drawable.ic_android)
507         whenever(smartspaceAction.icon).thenReturn(actionIcon)
508 
509         // Needed for card and item action click
510         val mockContext = mock(Context::class.java)
511         whenever(view.context).thenReturn(mockContext)
512         whenever(coverContainer1.context).thenReturn(mockContext)
513         whenever(coverContainer2.context).thenReturn(mockContext)
514         whenever(coverContainer3.context).thenReturn(mockContext)
515     }
516 
517     @After
518     fun tearDown() {
519         session.release()
520         player.onDestroy()
521     }
522 
523     @Test
524     fun bindWhenUnattached() {
525         val state = mediaData.copy(token = null)
526         player.bindPlayer(state, PACKAGE)
527         assertThat(player.isPlaying()).isFalse()
528     }
529 
530     @Test
531     fun bindSemanticActions() {
532         val icon = context.getDrawable(android.R.drawable.ic_media_play)
533         val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
534         val semanticActions =
535             MediaButton(
536                 playOrPause = MediaAction(icon, Runnable {}, "play", bg),
537                 nextOrCustom = MediaAction(icon, Runnable {}, "next", bg),
538                 custom0 = MediaAction(icon, null, "custom 0", bg),
539                 custom1 = MediaAction(icon, null, "custom 1", bg)
540             )
541         val state = mediaData.copy(semanticActions = semanticActions)
542         player.attachPlayer(viewHolder)
543         player.bindPlayer(state, PACKAGE)
544 
545         assertThat(actionPrev.isEnabled()).isFalse()
546         assertThat(actionPrev.drawable).isNull()
547         verify(collapsedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
548 
549         assertThat(actionPlayPause.isEnabled()).isTrue()
550         assertThat(actionPlayPause.contentDescription).isEqualTo("play")
551         verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.VISIBLE)
552 
553         assertThat(actionNext.isEnabled()).isTrue()
554         assertThat(actionNext.isFocusable()).isTrue()
555         assertThat(actionNext.isClickable()).isTrue()
556         assertThat(actionNext.contentDescription).isEqualTo("next")
557         verify(collapsedSet).setVisibility(R.id.actionNext, ConstraintSet.VISIBLE)
558 
559         // Called twice since these IDs are used as generic buttons
560         assertThat(action0.contentDescription).isEqualTo("custom 0")
561         assertThat(action0.isEnabled()).isFalse()
562         verify(collapsedSet, times(2)).setVisibility(R.id.action0, ConstraintSet.GONE)
563 
564         assertThat(action1.contentDescription).isEqualTo("custom 1")
565         assertThat(action1.isEnabled()).isFalse()
566         verify(collapsedSet, times(2)).setVisibility(R.id.action1, ConstraintSet.GONE)
567 
568         // Verify generic buttons are hidden
569         verify(collapsedSet).setVisibility(R.id.action2, ConstraintSet.GONE)
570         verify(expandedSet).setVisibility(R.id.action2, ConstraintSet.GONE)
571 
572         verify(collapsedSet).setVisibility(R.id.action3, ConstraintSet.GONE)
573         verify(expandedSet).setVisibility(R.id.action3, ConstraintSet.GONE)
574 
575         verify(collapsedSet).setVisibility(R.id.action4, ConstraintSet.GONE)
576         verify(expandedSet).setVisibility(R.id.action4, ConstraintSet.GONE)
577     }
578 
579     @Test
580     fun bindSemanticActions_reservedPrev() {
581         val icon = context.getDrawable(android.R.drawable.ic_media_play)
582         val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
583 
584         // Setup button state: no prev or next button and their slots reserved
585         val semanticActions =
586             MediaButton(
587                 playOrPause = MediaAction(icon, Runnable {}, "play", bg),
588                 nextOrCustom = null,
589                 prevOrCustom = null,
590                 custom0 = MediaAction(icon, null, "custom 0", bg),
591                 custom1 = MediaAction(icon, null, "custom 1", bg),
592                 false,
593                 true
594             )
595         val state = mediaData.copy(semanticActions = semanticActions)
596 
597         player.attachPlayer(viewHolder)
598         player.bindPlayer(state, PACKAGE)
599 
600         assertThat(actionPrev.isEnabled()).isFalse()
601         assertThat(actionPrev.drawable).isNull()
602         assertThat(actionPrev.isFocusable()).isFalse()
603         assertThat(actionPrev.isClickable()).isFalse()
604         verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.INVISIBLE)
605 
606         assertThat(actionNext.isEnabled()).isFalse()
607         assertThat(actionNext.drawable).isNull()
608         verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
609     }
610 
611     @Test
612     fun bindSemanticActions_reservedNext() {
613         val icon = context.getDrawable(android.R.drawable.ic_media_play)
614         val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
615 
616         // Setup button state: no prev or next button and their slots reserved
617         val semanticActions =
618             MediaButton(
619                 playOrPause = MediaAction(icon, Runnable {}, "play", bg),
620                 nextOrCustom = null,
621                 prevOrCustom = null,
622                 custom0 = MediaAction(icon, null, "custom 0", bg),
623                 custom1 = MediaAction(icon, null, "custom 1", bg),
624                 true,
625                 false
626             )
627         val state = mediaData.copy(semanticActions = semanticActions)
628 
629         player.attachPlayer(viewHolder)
630         player.bindPlayer(state, PACKAGE)
631 
632         assertThat(actionPrev.isEnabled()).isFalse()
633         assertThat(actionPrev.drawable).isNull()
634         verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
635 
636         assertThat(actionNext.isEnabled()).isFalse()
637         assertThat(actionNext.drawable).isNull()
638         assertThat(actionNext.isFocusable()).isFalse()
639         assertThat(actionNext.isClickable()).isFalse()
640         verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.INVISIBLE)
641     }
642 
643     @Test
644     fun bindAlbumView_testHardwareAfterAttach() {
645         player.attachPlayer(viewHolder)
646 
647         verify(albumView).setLayerType(View.LAYER_TYPE_HARDWARE, null)
648     }
649 
650     @Test
651     fun bindAlbumView_artUsesResource() {
652         val albumArt = Icon.createWithResource(context, R.drawable.ic_android)
653         val state = mediaData.copy(artwork = albumArt)
654 
655         player.attachPlayer(viewHolder)
656         player.bindPlayer(state, PACKAGE)
657         bgExecutor.runAllReady()
658         mainExecutor.runAllReady()
659 
660         verify(albumView).setImageDrawable(any<Drawable>())
661     }
662 
663     @Test
664     fun bindAlbumView_setAfterExecutors() {
665         val albumArt = getColorIcon(Color.RED)
666         val state = mediaData.copy(artwork = albumArt)
667 
668         player.attachPlayer(viewHolder)
669         player.bindPlayer(state, PACKAGE)
670         bgExecutor.runAllReady()
671         mainExecutor.runAllReady()
672 
673         verify(albumView).setImageDrawable(any<Drawable>())
674     }
675 
676     @Test
677     fun bindAlbumView_bitmapInLaterStates_setAfterExecutors() {
678         val redArt = getColorIcon(Color.RED)
679         val greenArt = getColorIcon(Color.GREEN)
680 
681         val state0 = mediaData.copy(artwork = null)
682         val state1 = mediaData.copy(artwork = redArt)
683         val state2 = mediaData.copy(artwork = redArt)
684         val state3 = mediaData.copy(artwork = greenArt)
685         player.attachPlayer(viewHolder)
686 
687         // First binding sets (empty) drawable
688         player.bindPlayer(state0, PACKAGE)
689         bgExecutor.runAllReady()
690         mainExecutor.runAllReady()
691         verify(albumView).setImageDrawable(any<Drawable>())
692 
693         // Run Metadata update so that later states don't update
694         val captor = argumentCaptor<Animator.AnimatorListener>()
695         verify(mockAnimator, times(2)).addListener(captor.capture())
696         captor.lastValue.onAnimationEnd(mockAnimator)
697         assertThat(titleText.getText()).isEqualTo(TITLE)
698         assertThat(artistText.getText()).isEqualTo(ARTIST)
699 
700         // Second binding sets transition drawable
701         player.bindPlayer(state1, PACKAGE)
702         bgExecutor.runAllReady()
703         mainExecutor.runAllReady()
704         val drawableCaptor = argumentCaptor<Drawable>()
705         verify(albumView, times(2)).setImageDrawable(drawableCaptor.capture())
706         assertTrue(drawableCaptor.allValues[1] is TransitionDrawable)
707 
708         // Third binding doesn't run transition or update background
709         player.bindPlayer(state2, PACKAGE)
710         bgExecutor.runAllReady()
711         mainExecutor.runAllReady()
712         verify(albumView, times(2)).setImageDrawable(any<Drawable>())
713 
714         // Fourth binding to new image runs transition due to color scheme change
715         player.bindPlayer(state3, PACKAGE)
716         bgExecutor.runAllReady()
717         mainExecutor.runAllReady()
718         verify(albumView, times(3)).setImageDrawable(any<Drawable>())
719     }
720 
721     @Test
722     fun addTwoPlayerGradients_differentStates() {
723         // Setup redArtwork and its color scheme.
724         val redArt = getColorIcon(Color.RED)
725         val redWallpaperColor = player.getWallpaperColor(redArt)
726         val redColorScheme = ColorScheme(redWallpaperColor, true, Style.CONTENT)
727 
728         // Setup greenArt and its color scheme.
729         val greenArt = getColorIcon(Color.GREEN)
730         val greenWallpaperColor = player.getWallpaperColor(greenArt)
731         val greenColorScheme = ColorScheme(greenWallpaperColor, true, Style.CONTENT)
732 
733         // Add gradient to both icons.
734         val redArtwork = player.addGradientToPlayerAlbum(redArt, redColorScheme, 10, 10)
735         val greenArtwork = player.addGradientToPlayerAlbum(greenArt, greenColorScheme, 10, 10)
736 
737         // They should have different constant states as they have different gradient color.
738         assertThat(redArtwork.getDrawable(1).constantState)
739             .isNotEqualTo(greenArtwork.getDrawable(1).constantState)
740     }
741 
742     @Test
743     fun getWallpaperColor_recycledBitmap_notCrashing() {
744         // Setup redArt icon.
745         val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
746         val redArt = Icon.createWithBitmap(redBmp)
747 
748         // Recycle bitmap of redArt icon.
749         redArt.bitmap.recycle()
750 
751         // get wallpaperColor without illegal exception.
752         player.getWallpaperColor(redArt)
753     }
754 
755     @Test
756     fun bind_seekBarDisabled_hasActions_seekBarVisibilityIsSetToInvisible() {
757         useRealConstraintSets()
758 
759         val icon = context.getDrawable(android.R.drawable.ic_media_play)
760         val semanticActions =
761             MediaButton(
762                 playOrPause = MediaAction(icon, Runnable {}, "play", null),
763                 nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
764             )
765         val state = mediaData.copy(semanticActions = semanticActions)
766 
767         player.attachPlayer(viewHolder)
768         getEnabledChangeListener().onEnabledChanged(enabled = false)
769 
770         player.bindPlayer(state, PACKAGE)
771 
772         assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
773     }
774 
775     @Test
776     fun bind_seekBarDisabled_noActions_seekBarVisibilityIsSetToInvisible() {
777         useRealConstraintSets()
778 
779         val state = mediaData.copy(semanticActions = MediaButton())
780         player.attachPlayer(viewHolder)
781         getEnabledChangeListener().onEnabledChanged(enabled = false)
782 
783         player.bindPlayer(state, PACKAGE)
784 
785         assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
786     }
787 
788     @Test
789     fun bind_seekBarEnabled_seekBarVisible() {
790         useRealConstraintSets()
791 
792         val state = mediaData.copy(semanticActions = MediaButton())
793         player.attachPlayer(viewHolder)
794         getEnabledChangeListener().onEnabledChanged(enabled = true)
795 
796         player.bindPlayer(state, PACKAGE)
797 
798         assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.VISIBLE)
799     }
800 
801     @Test
802     fun seekBarChangesToEnabledAfterBind_seekBarChangesToVisible() {
803         useRealConstraintSets()
804 
805         val state = mediaData.copy(semanticActions = MediaButton())
806         player.attachPlayer(viewHolder)
807         player.bindPlayer(state, PACKAGE)
808 
809         getEnabledChangeListener().onEnabledChanged(enabled = true)
810 
811         assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.VISIBLE)
812     }
813 
814     @Test
815     fun seekBarChangesToDisabledAfterBind_noActions_seekBarChangesToInvisible() {
816         useRealConstraintSets()
817 
818         val state = mediaData.copy(semanticActions = MediaButton())
819 
820         player.attachPlayer(viewHolder)
821         getEnabledChangeListener().onEnabledChanged(enabled = true)
822         player.bindPlayer(state, PACKAGE)
823 
824         getEnabledChangeListener().onEnabledChanged(enabled = false)
825 
826         assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
827     }
828 
829     @Test
830     fun seekBarChangesToDisabledAfterBind_hasActions_seekBarChangesToInvisible() {
831         useRealConstraintSets()
832 
833         val icon = context.getDrawable(android.R.drawable.ic_media_play)
834         val semanticActions =
835             MediaButton(nextOrCustom = MediaAction(icon, Runnable {}, "next", null))
836         val state = mediaData.copy(semanticActions = semanticActions)
837 
838         player.attachPlayer(viewHolder)
839         getEnabledChangeListener().onEnabledChanged(enabled = true)
840         player.bindPlayer(state, PACKAGE)
841 
842         getEnabledChangeListener().onEnabledChanged(enabled = false)
843 
844         assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
845     }
846 
847     @Test
848     fun bind_notScrubbing_scrubbingViewsGone() {
849         val icon = context.getDrawable(android.R.drawable.ic_media_play)
850         val semanticActions =
851             MediaButton(
852                 prevOrCustom = MediaAction(icon, {}, "prev", null),
853                 nextOrCustom = MediaAction(icon, {}, "next", null)
854             )
855         val state = mediaData.copy(semanticActions = semanticActions)
856 
857         player.attachPlayer(viewHolder)
858         player.bindPlayer(state, PACKAGE)
859 
860         verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.GONE)
861         verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.GONE)
862     }
863 
864     @Test
865     fun setIsScrubbing_noSemanticActions_viewsNotChanged() {
866         val state = mediaData.copy(semanticActions = null)
867         player.attachPlayer(viewHolder)
868         player.bindPlayer(state, PACKAGE)
869         reset(expandedSet)
870 
871         val listener = getScrubbingChangeListener()
872 
873         listener.onScrubbingChanged(true)
874         mainExecutor.runAllReady()
875 
876         verify(expandedSet, never()).setVisibility(eq(R.id.actionPrev), anyInt())
877         verify(expandedSet, never()).setVisibility(eq(R.id.actionNext), anyInt())
878         verify(expandedSet, never()).setVisibility(eq(R.id.media_scrubbing_elapsed_time), anyInt())
879         verify(expandedSet, never()).setVisibility(eq(R.id.media_scrubbing_total_time), anyInt())
880     }
881 
882     @Test
883     fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() {
884         val icon = context.getDrawable(android.R.drawable.ic_media_play)
885         val semanticActions =
886             MediaButton(prevOrCustom = null, nextOrCustom = MediaAction(icon, {}, "next", null))
887         val state = mediaData.copy(semanticActions = semanticActions)
888         player.attachPlayer(viewHolder)
889         player.bindPlayer(state, PACKAGE)
890         reset(expandedSet)
891 
892         getScrubbingChangeListener().onScrubbingChanged(true)
893         mainExecutor.runAllReady()
894 
895         verify(expandedSet).setVisibility(R.id.actionNext, View.VISIBLE)
896         verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, View.GONE)
897         verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, View.GONE)
898     }
899 
900     @Test
901     fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() {
902         val icon = context.getDrawable(android.R.drawable.ic_media_play)
903         val semanticActions =
904             MediaButton(prevOrCustom = MediaAction(icon, {}, "prev", null), nextOrCustom = null)
905         val state = mediaData.copy(semanticActions = semanticActions)
906         player.attachPlayer(viewHolder)
907         player.bindPlayer(state, PACKAGE)
908         reset(expandedSet)
909 
910         getScrubbingChangeListener().onScrubbingChanged(true)
911         mainExecutor.runAllReady()
912 
913         verify(expandedSet).setVisibility(R.id.actionPrev, View.VISIBLE)
914         verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, View.GONE)
915         verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, View.GONE)
916     }
917 
918     @Test
919     fun setIsScrubbing_true_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() {
920         val icon = context.getDrawable(android.R.drawable.ic_media_play)
921         val semanticActions =
922             MediaButton(
923                 prevOrCustom = MediaAction(icon, {}, "prev", null),
924                 nextOrCustom = MediaAction(icon, {}, "next", null)
925             )
926         val state = mediaData.copy(semanticActions = semanticActions)
927         player.attachPlayer(viewHolder)
928         player.bindPlayer(state, PACKAGE)
929         reset(expandedSet)
930 
931         getScrubbingChangeListener().onScrubbingChanged(true)
932         mainExecutor.runAllReady()
933 
934         // Only in expanded, we should show the scrubbing times and hide prev+next
935         verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.VISIBLE)
936         verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.VISIBLE)
937         verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
938         verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
939     }
940 
941     @Test
942     fun setIsScrubbing_trueThenFalse_scrubbingTimeGoneAtEnd() {
943         val icon = context.getDrawable(android.R.drawable.ic_media_play)
944         val semanticActions =
945             MediaButton(
946                 prevOrCustom = MediaAction(icon, {}, "prev", null),
947                 nextOrCustom = MediaAction(icon, {}, "next", null)
948             )
949         val state = mediaData.copy(semanticActions = semanticActions)
950 
951         player.attachPlayer(viewHolder)
952         player.bindPlayer(state, PACKAGE)
953 
954         getScrubbingChangeListener().onScrubbingChanged(true)
955         mainExecutor.runAllReady()
956         reset(expandedSet)
957 
958         getScrubbingChangeListener().onScrubbingChanged(false)
959         mainExecutor.runAllReady()
960 
961         // Only in expanded, we should hide the scrubbing times and show prev+next
962         verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.GONE)
963         verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.GONE)
964         verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.VISIBLE)
965         verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.VISIBLE)
966     }
967 
968     @Test
969     fun bind_resumeState_withProgress() {
970         val progress = 0.5
971         val state = mediaData.copy(resumption = true, resumeProgress = progress)
972 
973         player.attachPlayer(viewHolder)
974         player.bindPlayer(state, PACKAGE)
975 
976         verify(seekBarViewModel).updateStaticProgress(progress)
977     }
978 
979     @Test
980     fun animationSettingChange_updateSeekbar() {
981         // When animations are enabled
982         globalSettings.putFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f)
983         val progress = 0.5
984         val state = mediaData.copy(resumption = true, resumeProgress = progress)
985         player.attachPlayer(viewHolder)
986         player.bindPlayer(state, PACKAGE)
987 
988         val captor = argumentCaptor<SeekBarObserver>()
989         verify(seekBarData).observeForever(captor.capture())
990         val seekBarObserver = captor.lastValue
991 
992         // Then the seekbar is set to animate
993         assertThat(seekBarObserver.animationEnabled).isTrue()
994 
995         // When the setting changes,
996         globalSettings.putFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 0f)
997         player.updateAnimatorDurationScale()
998 
999         // Then the seekbar is set to not animate
1000         assertThat(seekBarObserver.animationEnabled).isFalse()
1001     }
1002 
1003     @Test
1004     fun bindNotificationActions() {
1005         val icon = context.getDrawable(android.R.drawable.ic_media_play)
1006         val actions =
1007             listOf(
1008                 MediaNotificationAction(true, actionIntent = pendingIntent, icon, "previous"),
1009                 MediaNotificationAction(true, actionIntent = pendingIntent, icon, "play"),
1010                 MediaNotificationAction(true, actionIntent = null, icon, "next"),
1011                 MediaNotificationAction(true, actionIntent = null, icon, "custom 0"),
1012                 MediaNotificationAction(true, actionIntent = pendingIntent, icon, "custom 1")
1013             )
1014         val state =
1015             mediaData.copy(
1016                 actions = actions,
1017                 actionsToShowInCompact = listOf(1, 2),
1018                 semanticActions = null
1019             )
1020 
1021         player.attachPlayer(viewHolder)
1022         player.bindPlayer(state, PACKAGE)
1023 
1024         // Verify semantic actions are hidden
1025         verify(collapsedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
1026         verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
1027 
1028         verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.GONE)
1029         verify(expandedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.GONE)
1030 
1031         verify(collapsedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
1032         verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
1033 
1034         // Generic actions all enabled
1035         assertThat(action0.contentDescription).isEqualTo("previous")
1036         assertThat(action0.isEnabled()).isTrue()
1037         verify(collapsedSet).setVisibility(R.id.action0, ConstraintSet.GONE)
1038 
1039         assertThat(action1.contentDescription).isEqualTo("play")
1040         assertThat(action1.isEnabled()).isTrue()
1041         verify(collapsedSet).setVisibility(R.id.action1, ConstraintSet.VISIBLE)
1042 
1043         assertThat(action2.contentDescription).isEqualTo("next")
1044         assertThat(action2.isEnabled()).isFalse()
1045         verify(collapsedSet).setVisibility(R.id.action2, ConstraintSet.VISIBLE)
1046 
1047         assertThat(action3.contentDescription).isEqualTo("custom 0")
1048         assertThat(action3.isEnabled()).isFalse()
1049         verify(collapsedSet).setVisibility(R.id.action3, ConstraintSet.GONE)
1050 
1051         assertThat(action4.contentDescription).isEqualTo("custom 1")
1052         assertThat(action4.isEnabled()).isTrue()
1053         verify(collapsedSet).setVisibility(R.id.action4, ConstraintSet.GONE)
1054     }
1055 
1056     @Test
1057     fun bindAnimatedSemanticActions() {
1058         val mockAvd0 = mock(AnimatedVectorDrawable::class.java)
1059         val mockAvd1 = mock(AnimatedVectorDrawable::class.java)
1060         val mockAvd2 = mock(AnimatedVectorDrawable::class.java)
1061         whenever(mockAvd0.mutate()).thenReturn(mockAvd0)
1062         whenever(mockAvd1.mutate()).thenReturn(mockAvd1)
1063         whenever(mockAvd2.mutate()).thenReturn(mockAvd2)
1064 
1065         val icon = context.getDrawable(R.drawable.ic_media_play)
1066         val bg = context.getDrawable(R.drawable.ic_media_play_container)
1067         val semanticActions0 =
1068             MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null))
1069         val semanticActions1 =
1070             MediaButton(playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null))
1071         val semanticActions2 =
1072             MediaButton(playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null))
1073         val state0 = mediaData.copy(semanticActions = semanticActions0)
1074         val state1 = mediaData.copy(semanticActions = semanticActions1)
1075         val state2 = mediaData.copy(semanticActions = semanticActions2)
1076 
1077         player.attachPlayer(viewHolder)
1078         player.bindPlayer(state0, PACKAGE)
1079 
1080         // Validate first binding
1081         assertThat(actionPlayPause.isEnabled()).isTrue()
1082         assertThat(actionPlayPause.contentDescription).isEqualTo("play")
1083         assertThat(actionPlayPause.getBackground()).isNull()
1084         verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.VISIBLE)
1085         assertThat(actionPlayPause.hasOnClickListeners()).isTrue()
1086 
1087         // Trigger animation & update mock
1088         actionPlayPause.performClick()
1089         verify(mockAvd0, times(1)).start()
1090         whenever(mockAvd0.isRunning()).thenReturn(true)
1091 
1092         // Validate states no longer bind
1093         player.bindPlayer(state1, PACKAGE)
1094         player.bindPlayer(state2, PACKAGE)
1095         assertThat(actionPlayPause.contentDescription).isEqualTo("play")
1096 
1097         // Complete animation and run callbacks
1098         whenever(mockAvd0.isRunning()).thenReturn(false)
1099         val captor = ArgumentCaptor.forClass(Animatable2.AnimationCallback::class.java)
1100         verify(mockAvd0, times(1)).registerAnimationCallback(captor.capture())
1101         verify(mockAvd1, never()).registerAnimationCallback(any<Animatable2.AnimationCallback>())
1102         verify(mockAvd2, never()).registerAnimationCallback(any<Animatable2.AnimationCallback>())
1103         captor.getValue().onAnimationEnd(mockAvd0)
1104 
1105         // Validate correct state was bound
1106         assertThat(actionPlayPause.contentDescription).isEqualTo("loading")
1107         assertThat(actionPlayPause.getBackground()).isNull()
1108         verify(mockAvd0, times(1)).registerAnimationCallback(any<Animatable2.AnimationCallback>())
1109         verify(mockAvd1, times(1)).registerAnimationCallback(any<Animatable2.AnimationCallback>())
1110         verify(mockAvd2, times(1)).registerAnimationCallback(any<Animatable2.AnimationCallback>())
1111         verify(mockAvd0, times(1)).unregisterAnimationCallback(any<Animatable2.AnimationCallback>())
1112         verify(mockAvd1, times(1)).unregisterAnimationCallback(any<Animatable2.AnimationCallback>())
1113         verify(mockAvd2, never()).unregisterAnimationCallback(any<Animatable2.AnimationCallback>())
1114     }
1115 
1116     @Test
1117     fun bindText() {
1118         useRealConstraintSets()
1119         player.attachPlayer(viewHolder)
1120         player.bindPlayer(mediaData, PACKAGE)
1121 
1122         // Capture animation handler
1123         val captor = argumentCaptor<Animator.AnimatorListener>()
1124         verify(mockAnimator, times(2)).addListener(captor.capture())
1125         val handler = captor.lastValue
1126 
1127         // Validate text views unchanged but animation started
1128         assertThat(titleText.getText()).isEqualTo("")
1129         assertThat(artistText.getText()).isEqualTo("")
1130         verify(mockAnimator, times(1)).start()
1131 
1132         // Binding only after animator runs
1133         handler.onAnimationEnd(mockAnimator)
1134         assertThat(titleText.getText()).isEqualTo(TITLE)
1135         assertThat(artistText.getText()).isEqualTo(ARTIST)
1136         assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE)
1137         assertThat(collapsedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE)
1138 
1139         // Rebinding should not trigger animation
1140         player.bindPlayer(mediaData, PACKAGE)
1141         verify(mockAnimator, times(2)).start()
1142     }
1143 
1144     @Test
1145     fun bindTextWithExplicitIndicator() {
1146         useRealConstraintSets()
1147         val mediaDataWitExp = mediaData.copy(isExplicit = true)
1148         player.attachPlayer(viewHolder)
1149         player.bindPlayer(mediaDataWitExp, PACKAGE)
1150 
1151         // Capture animation handler
1152         val captor = argumentCaptor<Animator.AnimatorListener>()
1153         verify(mockAnimator, times(2)).addListener(captor.capture())
1154         val handler = captor.lastValue
1155 
1156         // Validate text views unchanged but animation started
1157         assertThat(titleText.getText()).isEqualTo("")
1158         assertThat(artistText.getText()).isEqualTo("")
1159         verify(mockAnimator, times(1)).start()
1160 
1161         // Binding only after animator runs
1162         handler.onAnimationEnd(mockAnimator)
1163         assertThat(titleText.getText()).isEqualTo(TITLE)
1164         assertThat(artistText.getText()).isEqualTo(ARTIST)
1165         assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.VISIBLE)
1166         assertThat(collapsedSet.getVisibility(explicitIndicator.id))
1167             .isEqualTo(ConstraintSet.VISIBLE)
1168 
1169         // Rebinding should not trigger animation
1170         player.bindPlayer(mediaData, PACKAGE)
1171         verify(mockAnimator, times(3)).start()
1172     }
1173 
1174     @Test
1175     fun bindTextInterrupted() {
1176         val data0 = mediaData.copy(artist = "ARTIST_0")
1177         val data1 = mediaData.copy(artist = "ARTIST_1")
1178         val data2 = mediaData.copy(artist = "ARTIST_2")
1179 
1180         player.attachPlayer(viewHolder)
1181         player.bindPlayer(data0, PACKAGE)
1182 
1183         // Capture animation handler
1184         val captor = argumentCaptor<Animator.AnimatorListener>()
1185         verify(mockAnimator, times(2)).addListener(captor.capture())
1186         val handler = captor.lastValue
1187 
1188         handler.onAnimationEnd(mockAnimator)
1189         assertThat(artistText.getText()).isEqualTo("ARTIST_0")
1190 
1191         // Bind trigges new animation
1192         player.bindPlayer(data1, PACKAGE)
1193         verify(mockAnimator, times(3)).start()
1194         whenever(mockAnimator.isRunning()).thenReturn(true)
1195 
1196         // Rebind before animation end binds corrct data
1197         player.bindPlayer(data2, PACKAGE)
1198         handler.onAnimationEnd(mockAnimator)
1199         assertThat(artistText.getText()).isEqualTo("ARTIST_2")
1200     }
1201 
1202     @Test
1203     fun bindDevice() {
1204         player.attachPlayer(viewHolder)
1205         player.bindPlayer(mediaData, PACKAGE)
1206         assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
1207         assertThat(seamless.contentDescription).isEqualTo(DEVICE_NAME)
1208         assertThat(seamless.isEnabled()).isTrue()
1209     }
1210 
1211     @Test
1212     fun bindDisabledDevice() {
1213         seamless.id = 1
1214         player.attachPlayer(viewHolder)
1215         val state = mediaData.copy(device = disabledDevice)
1216         player.bindPlayer(state, PACKAGE)
1217         assertThat(seamless.isEnabled()).isFalse()
1218         assertThat(seamlessText.getText()).isEqualTo(DISABLED_DEVICE_NAME)
1219         assertThat(seamless.contentDescription).isEqualTo(DISABLED_DEVICE_NAME)
1220     }
1221 
1222     @Test
1223     fun bindNullDevice() {
1224         val fallbackString = context.getResources().getString(R.string.media_seamless_other_device)
1225         player.attachPlayer(viewHolder)
1226         val state = mediaData.copy(device = null)
1227         player.bindPlayer(state, PACKAGE)
1228         assertThat(seamless.isEnabled()).isTrue()
1229         assertThat(seamlessText.getText()).isEqualTo(fallbackString)
1230         assertThat(seamless.contentDescription).isEqualTo(fallbackString)
1231     }
1232 
1233     @Test
1234     fun bindDeviceWithNullName() {
1235         val fallbackString = context.getResources().getString(R.string.media_seamless_other_device)
1236         player.attachPlayer(viewHolder)
1237         val state = mediaData.copy(device = device.copy(name = null))
1238         player.bindPlayer(state, PACKAGE)
1239         assertThat(seamless.isEnabled()).isTrue()
1240         assertThat(seamlessText.getText()).isEqualTo(fallbackString)
1241         assertThat(seamless.contentDescription).isEqualTo(fallbackString)
1242     }
1243 
1244     @Test
1245     fun bindDeviceResumptionPlayer() {
1246         player.attachPlayer(viewHolder)
1247         val state = mediaData.copy(resumption = true)
1248         player.bindPlayer(state, PACKAGE)
1249         assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
1250         assertThat(seamless.isEnabled()).isFalse()
1251     }
1252 
1253     @Test
1254     @RequiresFlagsEnabled(com.android.settingslib.flags.Flags.FLAG_LEGACY_LE_AUDIO_SHARING)
1255     fun bindBroadcastButton() {
1256         initMediaViewHolderMocks()
1257         initDeviceMediaData(true, APP_NAME)
1258 
1259         val mockAvd0 = mock(AnimatedVectorDrawable::class.java)
1260         whenever(mockAvd0.mutate()).thenReturn(mockAvd0)
1261         val semanticActions0 =
1262             MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null))
1263         val state =
1264             mediaData.copy(resumption = true, semanticActions = semanticActions0, isPlaying = false)
1265         player.attachPlayer(viewHolder)
1266         player.bindPlayer(state, PACKAGE)
1267         assertThat(seamlessText.getText()).isEqualTo(APP_NAME)
1268         assertThat(seamless.isEnabled()).isTrue()
1269 
1270         seamless.callOnClick()
1271 
1272         verify(logger).logOpenBroadcastDialog(anyInt(), eq(PACKAGE), eq(instanceId))
1273     }
1274 
1275     /* ***** Guts tests for the player ***** */
1276 
1277     @Test
1278     fun player_longClick_isFalse() {
1279         whenever(falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)).thenReturn(true)
1280         player.attachPlayer(viewHolder)
1281 
1282         val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
1283         verify(viewHolder.player).onLongClickListener = captor.capture()
1284 
1285         captor.value.onLongClick(viewHolder.player)
1286         verify(mediaViewController, never()).openGuts()
1287         verify(mediaViewController, never()).closeGuts()
1288     }
1289 
1290     @Test
1291     fun player_longClickWhenGutsClosed_gutsOpens() {
1292         player.attachPlayer(viewHolder)
1293         player.bindPlayer(mediaData, KEY)
1294         whenever(mediaViewController.isGutsVisible).thenReturn(false)
1295 
1296         val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
1297         verify(viewHolder.player).setOnLongClickListener(captor.capture())
1298 
1299         captor.value.onLongClick(viewHolder.player)
1300         verify(mediaViewController).openGuts()
1301         verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId))
1302     }
1303 
1304     @Test
1305     fun player_longClickWhenGutsOpen_gutsCloses() {
1306         player.attachPlayer(viewHolder)
1307         whenever(mediaViewController.isGutsVisible).thenReturn(true)
1308 
1309         val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
1310         verify(viewHolder.player).setOnLongClickListener(captor.capture())
1311 
1312         captor.value.onLongClick(viewHolder.player)
1313         verify(mediaViewController, never()).openGuts()
1314         verify(mediaViewController).closeGuts(false)
1315     }
1316 
1317     @Test
1318     fun player_cancelButtonClick_animation() {
1319         player.attachPlayer(viewHolder)
1320         player.bindPlayer(mediaData, KEY)
1321 
1322         cancel.callOnClick()
1323 
1324         verify(mediaViewController).closeGuts(false)
1325     }
1326 
1327     @Test
1328     fun player_settingsButtonClick() {
1329         player.attachPlayer(viewHolder)
1330         player.bindPlayer(mediaData, KEY)
1331 
1332         settings.callOnClick()
1333         verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId))
1334 
1335         val captor = ArgumentCaptor.forClass(Intent::class.java)
1336         verify(activityStarter).startActivity(captor.capture(), eq(true))
1337 
1338         assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS)
1339     }
1340 
1341     @Test
1342     fun player_dismissButtonClick() {
1343         val mediaKey = "key for dismissal"
1344         player.attachPlayer(viewHolder)
1345         val state = mediaData.copy(notificationKey = KEY)
1346         player.bindPlayer(state, mediaKey)
1347 
1348         assertThat(dismiss.isEnabled).isEqualTo(true)
1349         dismiss.callOnClick()
1350         verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId))
1351         verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong(), eq(true))
1352     }
1353 
1354     @Test
1355     fun player_dismissButtonDisabled() {
1356         val mediaKey = "key for dismissal"
1357         player.attachPlayer(viewHolder)
1358         val state = mediaData.copy(isClearable = false, notificationKey = KEY)
1359         player.bindPlayer(state, mediaKey)
1360 
1361         assertThat(dismiss.isEnabled).isEqualTo(false)
1362     }
1363 
1364     @Test
1365     fun player_dismissButtonClick_notInManager() {
1366         val mediaKey = "key for dismissal"
1367         whenever(mediaDataManager.dismissMediaData(eq(mediaKey), anyLong(), eq(true)))
1368             .thenReturn(false)
1369 
1370         player.attachPlayer(viewHolder)
1371         val state = mediaData.copy(notificationKey = KEY)
1372         player.bindPlayer(state, mediaKey)
1373 
1374         assertThat(dismiss.isEnabled).isEqualTo(true)
1375         dismiss.callOnClick()
1376 
1377         verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong(), eq(true))
1378         verify(mediaCarouselController).removePlayer(eq(mediaKey), eq(false), eq(false), eq(true))
1379     }
1380 
1381     @Test
1382     fun player_gutsOpen_contentDescriptionIsForGuts() {
1383         whenever(mediaViewController.isGutsVisible).thenReturn(true)
1384         player.attachPlayer(viewHolder)
1385 
1386         val gutsTextString = "gutsText"
1387         whenever(gutsText.text).thenReturn(gutsTextString)
1388         player.bindPlayer(mediaData, KEY)
1389 
1390         val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
1391         verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
1392         val description = descriptionCaptor.value.toString()
1393 
1394         assertThat(description).isEqualTo(gutsTextString)
1395     }
1396 
1397     @Test
1398     fun player_gutsClosed_contentDescriptionIsForPlayer() {
1399         whenever(mediaViewController.isGutsVisible).thenReturn(false)
1400         player.attachPlayer(viewHolder)
1401 
1402         val app = "appName"
1403         player.bindPlayer(mediaData.copy(app = app), KEY)
1404 
1405         val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
1406         verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
1407         val description = descriptionCaptor.value.toString()
1408 
1409         assertThat(description).contains(mediaData.song!!)
1410         assertThat(description).contains(mediaData.artist!!)
1411         assertThat(description).contains(app)
1412     }
1413 
1414     @Test
1415     fun player_gutsChangesFromOpenToClosed_contentDescriptionUpdated() {
1416         // Start out open
1417         whenever(mediaViewController.isGutsVisible).thenReturn(true)
1418         whenever(gutsText.text).thenReturn("gutsText")
1419         player.attachPlayer(viewHolder)
1420         val app = "appName"
1421         player.bindPlayer(mediaData.copy(app = app), KEY)
1422 
1423         // Update to closed by long pressing
1424         val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
1425         verify(viewHolder.player).onLongClickListener = captor.capture()
1426         reset(viewHolder.player)
1427 
1428         whenever(mediaViewController.isGutsVisible).thenReturn(false)
1429         captor.value.onLongClick(viewHolder.player)
1430 
1431         // Then content description is now the player content description
1432         val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
1433         verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
1434         val description = descriptionCaptor.value.toString()
1435 
1436         assertThat(description).contains(mediaData.song!!)
1437         assertThat(description).contains(mediaData.artist!!)
1438         assertThat(description).contains(app)
1439     }
1440 
1441     @Test
1442     fun player_gutsChangesFromClosedToOpen_contentDescriptionUpdated() {
1443         // Start out closed
1444         whenever(mediaViewController.isGutsVisible).thenReturn(false)
1445         val gutsTextString = "gutsText"
1446         whenever(gutsText.text).thenReturn(gutsTextString)
1447         player.attachPlayer(viewHolder)
1448         player.bindPlayer(mediaData.copy(app = "appName"), KEY)
1449 
1450         // Update to open by long pressing
1451         val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
1452         verify(viewHolder.player).onLongClickListener = captor.capture()
1453         reset(viewHolder.player)
1454 
1455         whenever(mediaViewController.isGutsVisible).thenReturn(true)
1456         captor.value.onLongClick(viewHolder.player)
1457 
1458         // Then content description is now the guts content description
1459         val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
1460         verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
1461         val description = descriptionCaptor.value.toString()
1462 
1463         assertThat(description).isEqualTo(gutsTextString)
1464     }
1465 
1466     /* ***** END guts tests for the player ***** */
1467 
1468     /* ***** Guts tests for the recommendations ***** */
1469 
1470     @Test
1471     fun recommendations_longClick_isFalse() {
1472         whenever(falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)).thenReturn(true)
1473         player.attachRecommendation(recommendationViewHolder)
1474         player.bindRecommendation(smartspaceData)
1475 
1476         val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
1477         verify(viewHolder.player).onLongClickListener = captor.capture()
1478 
1479         captor.value.onLongClick(viewHolder.player)
1480         verify(mediaViewController, never()).openGuts()
1481         verify(mediaViewController, never()).closeGuts()
1482     }
1483 
1484     @Test
1485     fun recommendations_longClickWhenGutsClosed_gutsOpens() {
1486         player.attachRecommendation(recommendationViewHolder)
1487         player.bindRecommendation(smartspaceData)
1488         whenever(mediaViewController.isGutsVisible).thenReturn(false)
1489 
1490         val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
1491         verify(viewHolder.player).onLongClickListener = captor.capture()
1492 
1493         captor.value.onLongClick(viewHolder.player)
1494         verify(mediaViewController).openGuts()
1495         verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId))
1496     }
1497 
1498     @Test
1499     fun recommendations_longClickWhenGutsOpen_gutsCloses() {
1500         player.attachRecommendation(recommendationViewHolder)
1501         player.bindRecommendation(smartspaceData)
1502         whenever(mediaViewController.isGutsVisible).thenReturn(true)
1503 
1504         val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
1505         verify(viewHolder.player).onLongClickListener = captor.capture()
1506 
1507         captor.value.onLongClick(viewHolder.player)
1508         verify(mediaViewController, never()).openGuts()
1509         verify(mediaViewController).closeGuts(false)
1510     }
1511 
1512     @Test
1513     fun recommendations_cancelButtonClick_animation() {
1514         player.attachRecommendation(recommendationViewHolder)
1515         player.bindRecommendation(smartspaceData)
1516 
1517         cancel.callOnClick()
1518 
1519         verify(mediaViewController).closeGuts(false)
1520     }
1521 
1522     @Test
1523     fun recommendations_settingsButtonClick() {
1524         player.attachRecommendation(recommendationViewHolder)
1525         player.bindRecommendation(smartspaceData)
1526 
1527         settings.callOnClick()
1528         verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId))
1529 
1530         val captor = ArgumentCaptor.forClass(Intent::class.java)
1531         verify(activityStarter).startActivity(captor.capture(), eq(true))
1532 
1533         assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS)
1534     }
1535 
1536     @Test
1537     fun recommendations_dismissButtonClick() {
1538         val mediaKey = "key for dismissal"
1539         player.attachRecommendation(recommendationViewHolder)
1540         player.bindRecommendation(smartspaceData.copy(targetId = mediaKey))
1541 
1542         assertThat(dismiss.isEnabled).isEqualTo(true)
1543         dismiss.callOnClick()
1544         verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId))
1545         verify(mediaDataManager).dismissSmartspaceRecommendation(eq(mediaKey), anyLong())
1546     }
1547 
1548     @Test
1549     fun recommendation_gutsOpen_contentDescriptionIsForGuts() {
1550         whenever(mediaViewController.isGutsVisible).thenReturn(true)
1551         player.attachRecommendation(recommendationViewHolder)
1552 
1553         val gutsTextString = "gutsText"
1554         whenever(gutsText.text).thenReturn(gutsTextString)
1555         player.bindRecommendation(smartspaceData)
1556 
1557         val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
1558         verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
1559         val description = descriptionCaptor.value.toString()
1560 
1561         assertThat(description).isEqualTo(gutsTextString)
1562     }
1563 
1564     @Test
1565     fun recommendation_gutsClosed_contentDescriptionIsForPlayer() {
1566         whenever(mediaViewController.isGutsVisible).thenReturn(false)
1567         player.attachRecommendation(recommendationViewHolder)
1568 
1569         player.bindRecommendation(smartspaceData)
1570 
1571         val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
1572         verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
1573         val description = descriptionCaptor.value.toString()
1574 
1575         assertThat(description)
1576             .isEqualTo(context.getString(R.string.controls_media_smartspace_rec_header))
1577     }
1578 
1579     @Test
1580     fun recommendation_gutsChangesFromOpenToClosed_contentDescriptionUpdated() {
1581         // Start out open
1582         whenever(mediaViewController.isGutsVisible).thenReturn(true)
1583         whenever(gutsText.text).thenReturn("gutsText")
1584         player.attachRecommendation(recommendationViewHolder)
1585         player.bindRecommendation(smartspaceData)
1586 
1587         // Update to closed by long pressing
1588         val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
1589         verify(viewHolder.player).onLongClickListener = captor.capture()
1590         reset(viewHolder.player)
1591 
1592         whenever(mediaViewController.isGutsVisible).thenReturn(false)
1593         captor.value.onLongClick(viewHolder.player)
1594 
1595         // Then content description is now the player content description
1596         val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
1597         verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
1598         val description = descriptionCaptor.value.toString()
1599 
1600         assertThat(description)
1601             .isEqualTo(context.getString(R.string.controls_media_smartspace_rec_header))
1602     }
1603 
1604     @Test
1605     fun recommendation_gutsChangesFromClosedToOpen_contentDescriptionUpdated() {
1606         // Start out closed
1607         whenever(mediaViewController.isGutsVisible).thenReturn(false)
1608         val gutsTextString = "gutsText"
1609         whenever(gutsText.text).thenReturn(gutsTextString)
1610         player.attachRecommendation(recommendationViewHolder)
1611         player.bindRecommendation(smartspaceData)
1612 
1613         // Update to open by long pressing
1614         val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
1615         verify(viewHolder.player).onLongClickListener = captor.capture()
1616         reset(viewHolder.player)
1617 
1618         whenever(mediaViewController.isGutsVisible).thenReturn(true)
1619         captor.value.onLongClick(viewHolder.player)
1620 
1621         // Then content description is now the guts content description
1622         val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
1623         verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
1624         val description = descriptionCaptor.value.toString()
1625 
1626         assertThat(description).isEqualTo(gutsTextString)
1627     }
1628 
1629     /* ***** END guts tests for the recommendations ***** */
1630 
1631     @Test
1632     fun actionPlayPauseClick_isLogged() {
1633         val semanticActions =
1634             MediaButton(playOrPause = MediaAction(null, Runnable {}, "play", null))
1635         val data = mediaData.copy(semanticActions = semanticActions)
1636 
1637         player.attachPlayer(viewHolder)
1638         player.bindPlayer(data, KEY)
1639 
1640         viewHolder.actionPlayPause.callOnClick()
1641         verify(logger).logTapAction(eq(R.id.actionPlayPause), anyInt(), eq(PACKAGE), eq(instanceId))
1642     }
1643 
1644     @Test
1645     fun actionPrevClick_isLogged() {
1646         val semanticActions =
1647             MediaButton(prevOrCustom = MediaAction(null, Runnable {}, "previous", null))
1648         val data = mediaData.copy(semanticActions = semanticActions)
1649 
1650         player.attachPlayer(viewHolder)
1651         player.bindPlayer(data, KEY)
1652 
1653         viewHolder.actionPrev.callOnClick()
1654         verify(logger).logTapAction(eq(R.id.actionPrev), anyInt(), eq(PACKAGE), eq(instanceId))
1655     }
1656 
1657     @Test
1658     fun actionNextClick_isLogged() {
1659         val semanticActions =
1660             MediaButton(nextOrCustom = MediaAction(null, Runnable {}, "next", null))
1661         val data = mediaData.copy(semanticActions = semanticActions)
1662 
1663         player.attachPlayer(viewHolder)
1664         player.bindPlayer(data, KEY)
1665 
1666         viewHolder.actionNext.callOnClick()
1667         verify(logger).logTapAction(eq(R.id.actionNext), anyInt(), eq(PACKAGE), eq(instanceId))
1668     }
1669 
1670     @Test
1671     fun actionCustom0Click_isLogged() {
1672         val semanticActions =
1673             MediaButton(custom0 = MediaAction(null, Runnable {}, "custom 0", null))
1674         val data = mediaData.copy(semanticActions = semanticActions)
1675 
1676         player.attachPlayer(viewHolder)
1677         player.bindPlayer(data, KEY)
1678 
1679         viewHolder.action0.callOnClick()
1680         verify(logger).logTapAction(eq(R.id.action0), anyInt(), eq(PACKAGE), eq(instanceId))
1681     }
1682 
1683     @Test
1684     fun actionCustom1Click_isLogged() {
1685         val semanticActions =
1686             MediaButton(custom1 = MediaAction(null, Runnable {}, "custom 1", null))
1687         val data = mediaData.copy(semanticActions = semanticActions)
1688 
1689         player.attachPlayer(viewHolder)
1690         player.bindPlayer(data, KEY)
1691 
1692         viewHolder.action1.callOnClick()
1693         verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId))
1694     }
1695 
1696     @Test
1697     fun actionCustom2Click_isLogged() {
1698         val actions =
1699             listOf(
1700                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"),
1701                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"),
1702                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"),
1703                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"),
1704                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4")
1705             )
1706         val data = mediaData.copy(actions = actions)
1707 
1708         player.attachPlayer(viewHolder)
1709         player.bindPlayer(data, KEY)
1710 
1711         viewHolder.action2.callOnClick()
1712         verify(logger).logTapAction(eq(R.id.action2), anyInt(), eq(PACKAGE), eq(instanceId))
1713     }
1714 
1715     @Test
1716     fun actionCustom3Click_isLogged() {
1717         val actions =
1718             listOf(
1719                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"),
1720                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"),
1721                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"),
1722                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"),
1723                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4")
1724             )
1725         val data = mediaData.copy(actions = actions)
1726 
1727         player.attachPlayer(viewHolder)
1728         player.bindPlayer(data, KEY)
1729 
1730         viewHolder.action1.callOnClick()
1731         verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId))
1732     }
1733 
1734     @Test
1735     fun actionCustom4Click_isLogged() {
1736         val actions =
1737             listOf(
1738                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"),
1739                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"),
1740                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"),
1741                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"),
1742                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4")
1743             )
1744         val data = mediaData.copy(actions = actions)
1745 
1746         player.attachPlayer(viewHolder)
1747         player.bindPlayer(data, KEY)
1748 
1749         viewHolder.action1.callOnClick()
1750         verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId))
1751     }
1752 
1753     @Test
1754     fun openOutputSwitcher_isLogged() {
1755         player.attachPlayer(viewHolder)
1756         player.bindPlayer(mediaData, KEY)
1757 
1758         seamless.callOnClick()
1759 
1760         verify(logger).logOpenOutputSwitcher(anyInt(), eq(PACKAGE), eq(instanceId))
1761     }
1762 
1763     @Test
1764     fun tapContentView_isLogged() {
1765         val pendingIntent = mock(PendingIntent::class.java)
1766         val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
1767         val data = mediaData.copy(clickIntent = pendingIntent)
1768         player.attachPlayer(viewHolder)
1769         player.bindPlayer(data, KEY)
1770         verify(viewHolder.player).setOnClickListener(captor.capture())
1771 
1772         captor.value.onClick(viewHolder.player)
1773 
1774         verify(logger).logTapContentView(anyInt(), eq(PACKAGE), eq(instanceId))
1775     }
1776 
1777     @Test
1778     fun logSeek() {
1779         player.attachPlayer(viewHolder)
1780         player.bindPlayer(mediaData, KEY)
1781 
1782         val captor = argumentCaptor<() -> Unit>()
1783         verify(seekBarViewModel).logSeek = captor.capture()
1784         captor.lastValue.invoke()
1785 
1786         verify(logger).logSeek(anyInt(), eq(PACKAGE), eq(instanceId))
1787     }
1788 
1789     @EnableFlags(Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION)
1790     @Test
1791     fun tapContentView_showOverLockscreen_openActivity_withOriginAnimation() {
1792         // WHEN we are on lockscreen and this activity can show over lockscreen
1793         whenever(keyguardStateController.isShowing).thenReturn(true)
1794         whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any())).thenReturn(true)
1795 
1796         val clickIntent = mock(Intent::class.java)
1797         val pendingIntent = mock(PendingIntent::class.java)
1798         whenever(pendingIntent.intent).thenReturn(clickIntent)
1799         val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
1800         val data = mediaData.copy(clickIntent = pendingIntent)
1801         player.attachPlayer(viewHolder)
1802         player.bindPlayer(data, KEY)
1803         verify(viewHolder.player).setOnClickListener(captor.capture())
1804 
1805         // THEN it sends the PendingIntent without dismissing keyguard first,
1806         // and does not use the Intent directly (see b/271845008)
1807         captor.value.onClick(viewHolder.player)
1808         verify(activityStarter)
1809             .startPendingIntentMaybeDismissingKeyguard(
1810                 eq(pendingIntent),
1811                 eq(true),
1812                 eq(null),
1813                 any(),
1814                 eq(null),
1815                 eq(null),
1816                 eq(null),
1817             )
1818         verify(activityStarter, never()).postStartActivityDismissingKeyguard(eq(clickIntent), any())
1819     }
1820 
1821     @DisableFlags(Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION)
1822     @Test
1823     fun tapContentView_showOverLockscreen_openActivity_withoutOriginAnimation() {
1824         // WHEN we are on lockscreen and this activity can show over lockscreen
1825         whenever(keyguardStateController.isShowing).thenReturn(true)
1826         whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any())).thenReturn(true)
1827 
1828         val clickIntent = mock(Intent::class.java)
1829         val pendingIntent = mock(PendingIntent::class.java)
1830         whenever(pendingIntent.intent).thenReturn(clickIntent)
1831         val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
1832         val data = mediaData.copy(clickIntent = pendingIntent)
1833         player.attachPlayer(viewHolder)
1834         player.bindPlayer(data, KEY)
1835         verify(viewHolder.player).setOnClickListener(captor.capture())
1836 
1837         // THEN it sends the PendingIntent without dismissing keyguard first,
1838         // and does not use the Intent directly (see b/271845008)
1839         captor.value.onClick(viewHolder.player)
1840         verify(pendingIntent).send(any<Bundle>())
1841         verify(pendingIntent, never()).getIntent()
1842         verify(activityStarter, never()).postStartActivityDismissingKeyguard(eq(clickIntent), any())
1843     }
1844 
1845     @Test
1846     fun tapContentView_noShowOverLockscreen_dismissKeyguard() {
1847         // WHEN we are on lockscreen and the activity cannot show over lockscreen
1848         whenever(keyguardStateController.isShowing).thenReturn(true)
1849         whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any()))
1850             .thenReturn(false)
1851 
1852         val clickIntent = mock(Intent::class.java)
1853         val pendingIntent = mock(PendingIntent::class.java)
1854         whenever(pendingIntent.intent).thenReturn(clickIntent)
1855         val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
1856         val data = mediaData.copy(clickIntent = pendingIntent)
1857         player.attachPlayer(viewHolder)
1858         player.bindPlayer(data, KEY)
1859         verify(viewHolder.player).setOnClickListener(captor.capture())
1860 
1861         // THEN keyguard has to be dismissed
1862         captor.value.onClick(viewHolder.player)
1863         verify(activityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any())
1864     }
1865 
1866     @Test
1867     fun recommendation_gutsClosed_longPressOpens() {
1868         player.attachRecommendation(recommendationViewHolder)
1869         player.bindRecommendation(smartspaceData)
1870         whenever(mediaViewController.isGutsVisible).thenReturn(false)
1871 
1872         val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
1873         verify(recommendationViewHolder.recommendations).setOnLongClickListener(captor.capture())
1874 
1875         captor.value.onLongClick(recommendationViewHolder.recommendations)
1876         verify(mediaViewController).openGuts()
1877         verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId))
1878     }
1879 
1880     @Test
1881     fun recommendation_settingsButtonClick_isLogged() {
1882         player.attachRecommendation(recommendationViewHolder)
1883         player.bindRecommendation(smartspaceData)
1884 
1885         settings.callOnClick()
1886         verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId))
1887 
1888         val captor = ArgumentCaptor.forClass(Intent::class.java)
1889         verify(activityStarter).startActivity(captor.capture(), eq(true))
1890 
1891         assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS)
1892     }
1893 
1894     @Test
1895     fun recommendation_dismissButton_isLogged() {
1896         player.attachRecommendation(recommendationViewHolder)
1897         player.bindRecommendation(smartspaceData)
1898 
1899         dismiss.callOnClick()
1900         verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId))
1901     }
1902 
1903     @Test
1904     fun recommendation_tapOnCard_isLogged() {
1905         val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
1906         player.attachRecommendation(recommendationViewHolder)
1907         player.bindRecommendation(smartspaceData)
1908 
1909         verify(recommendationViewHolder.recommendations).setOnClickListener(captor.capture())
1910         captor.value.onClick(recommendationViewHolder.recommendations)
1911 
1912         verify(logger).logRecommendationCardTap(eq(PACKAGE), eq(instanceId))
1913     }
1914 
1915     @Test
1916     fun recommendation_tapOnItem_isLogged() {
1917         val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
1918         player.attachRecommendation(recommendationViewHolder)
1919         player.bindRecommendation(smartspaceData)
1920 
1921         verify(coverContainer1).setOnClickListener(captor.capture())
1922         captor.value.onClick(recommendationViewHolder.recommendations)
1923 
1924         verify(logger).logRecommendationItemTap(eq(PACKAGE), eq(instanceId), eq(0))
1925     }
1926 
1927     @Test
1928     fun bindRecommendation_listHasTooFewRecs_notDisplayed() {
1929         player.attachRecommendation(recommendationViewHolder)
1930         val icon =
1931             Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata)
1932         val data =
1933             smartspaceData.copy(
1934                 recommendations =
1935                     listOf(
1936                         SmartspaceAction.Builder("id1", "title1")
1937                             .setSubtitle("subtitle1")
1938                             .setIcon(icon)
1939                             .setExtras(Bundle.EMPTY)
1940                             .build(),
1941                         SmartspaceAction.Builder("id2", "title2")
1942                             .setSubtitle("subtitle2")
1943                             .setIcon(icon)
1944                             .setExtras(Bundle.EMPTY)
1945                             .build(),
1946                     )
1947             )
1948 
1949         player.bindRecommendation(data)
1950 
1951         assertThat(recTitle1.text).isEqualTo("")
1952         verify(mediaViewController, never()).refreshState()
1953     }
1954 
1955     @Test
1956     fun bindRecommendation_listHasTooFewRecsWithIcons_notDisplayed() {
1957         player.attachRecommendation(recommendationViewHolder)
1958         val icon =
1959             Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata)
1960         val data =
1961             smartspaceData.copy(
1962                 recommendations =
1963                     listOf(
1964                         SmartspaceAction.Builder("id1", "title1")
1965                             .setSubtitle("subtitle1")
1966                             .setIcon(icon)
1967                             .setExtras(Bundle.EMPTY)
1968                             .build(),
1969                         SmartspaceAction.Builder("id2", "title2")
1970                             .setSubtitle("subtitle2")
1971                             .setIcon(icon)
1972                             .setExtras(Bundle.EMPTY)
1973                             .build(),
1974                         SmartspaceAction.Builder("id2", "empty icon 1")
1975                             .setSubtitle("subtitle2")
1976                             .setIcon(null)
1977                             .setExtras(Bundle.EMPTY)
1978                             .build(),
1979                         SmartspaceAction.Builder("id2", "empty icon 2")
1980                             .setSubtitle("subtitle2")
1981                             .setIcon(null)
1982                             .setExtras(Bundle.EMPTY)
1983                             .build(),
1984                     )
1985             )
1986 
1987         player.bindRecommendation(data)
1988 
1989         assertThat(recTitle1.text).isEqualTo("")
1990         verify(mediaViewController, never()).refreshState()
1991     }
1992 
1993     @Test
1994     fun bindRecommendation_hasTitlesAndSubtitles() {
1995         player.attachRecommendation(recommendationViewHolder)
1996 
1997         val title1 = "Title1"
1998         val title2 = "Title2"
1999         val title3 = "Title3"
2000         val subtitle1 = "Subtitle1"
2001         val subtitle2 = "Subtitle2"
2002         val subtitle3 = "Subtitle3"
2003         val icon =
2004             Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata)
2005 
2006         val data =
2007             smartspaceData.copy(
2008                 recommendations =
2009                     listOf(
2010                         SmartspaceAction.Builder("id1", title1)
2011                             .setSubtitle(subtitle1)
2012                             .setIcon(icon)
2013                             .setExtras(Bundle.EMPTY)
2014                             .build(),
2015                         SmartspaceAction.Builder("id2", title2)
2016                             .setSubtitle(subtitle2)
2017                             .setIcon(icon)
2018                             .setExtras(Bundle.EMPTY)
2019                             .build(),
2020                         SmartspaceAction.Builder("id3", title3)
2021                             .setSubtitle(subtitle3)
2022                             .setIcon(icon)
2023                             .setExtras(Bundle.EMPTY)
2024                             .build()
2025                     )
2026             )
2027         player.bindRecommendation(data)
2028 
2029         assertThat(recTitle1.text).isEqualTo(title1)
2030         assertThat(recTitle2.text).isEqualTo(title2)
2031         assertThat(recTitle3.text).isEqualTo(title3)
2032         assertThat(recSubtitle1.text).isEqualTo(subtitle1)
2033         assertThat(recSubtitle2.text).isEqualTo(subtitle2)
2034         assertThat(recSubtitle3.text).isEqualTo(subtitle3)
2035     }
2036 
2037     @Test
2038     fun bindRecommendation_noTitle_subtitleNotShown() {
2039         player.attachRecommendation(recommendationViewHolder)
2040 
2041         val data =
2042             smartspaceData.copy(
2043                 recommendations =
2044                     listOf(
2045                         SmartspaceAction.Builder("id1", "")
2046                             .setSubtitle("fake subtitle")
2047                             .setIcon(
2048                                 Icon.createWithResource(
2049                                     context,
2050                                     com.android.settingslib.R.drawable.ic_1x_mobiledata
2051                                 )
2052                             )
2053                             .setExtras(Bundle.EMPTY)
2054                             .build()
2055                     )
2056             )
2057         player.bindRecommendation(data)
2058 
2059         assertThat(recSubtitle1.text).isEqualTo("")
2060     }
2061 
2062     @Test
2063     fun bindRecommendation_someHaveTitles_allTitleViewsShown() {
2064         useRealConstraintSets()
2065         player.attachRecommendation(recommendationViewHolder)
2066 
2067         val icon =
2068             Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata)
2069         val data =
2070             smartspaceData.copy(
2071                 recommendations =
2072                     listOf(
2073                         SmartspaceAction.Builder("id1", "")
2074                             .setSubtitle("fake subtitle")
2075                             .setIcon(icon)
2076                             .setExtras(Bundle.EMPTY)
2077                             .build(),
2078                         SmartspaceAction.Builder("id2", "title2")
2079                             .setSubtitle("fake subtitle")
2080                             .setIcon(icon)
2081                             .setExtras(Bundle.EMPTY)
2082                             .build(),
2083                         SmartspaceAction.Builder("id3", "")
2084                             .setSubtitle("fake subtitle")
2085                             .setIcon(icon)
2086                             .setExtras(Bundle.EMPTY)
2087                             .build()
2088                     )
2089             )
2090         player.bindRecommendation(data)
2091 
2092         assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.VISIBLE)
2093         assertThat(expandedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.VISIBLE)
2094         assertThat(expandedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.VISIBLE)
2095     }
2096 
2097     @Test
2098     fun bindRecommendation_someHaveSubtitles_allSubtitleViewsShown() {
2099         useRealConstraintSets()
2100         player.attachRecommendation(recommendationViewHolder)
2101 
2102         val icon =
2103             Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata)
2104         val data =
2105             smartspaceData.copy(
2106                 recommendations =
2107                     listOf(
2108                         SmartspaceAction.Builder("id1", "")
2109                             .setSubtitle("")
2110                             .setIcon(icon)
2111                             .setExtras(Bundle.EMPTY)
2112                             .build(),
2113                         SmartspaceAction.Builder("id2", "title2")
2114                             .setSubtitle("subtitle2")
2115                             .setIcon(icon)
2116                             .setExtras(Bundle.EMPTY)
2117                             .build(),
2118                         SmartspaceAction.Builder("id3", "title3")
2119                             .setSubtitle("")
2120                             .setIcon(icon)
2121                             .setExtras(Bundle.EMPTY)
2122                             .build()
2123                     )
2124             )
2125         player.bindRecommendation(data)
2126 
2127         assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.VISIBLE)
2128         assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.VISIBLE)
2129         assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.VISIBLE)
2130     }
2131 
2132     @Test
2133     fun bindRecommendation_noneHaveSubtitles_subtitleViewsGone() {
2134         useRealConstraintSets()
2135         player.attachRecommendation(recommendationViewHolder)
2136         val data =
2137             smartspaceData.copy(
2138                 recommendations =
2139                     listOf(
2140                         SmartspaceAction.Builder("id1", "title1")
2141                             .setSubtitle("")
2142                             .setIcon(
2143                                 Icon.createWithResource(
2144                                     context,
2145                                     com.android.settingslib.R.drawable.ic_1x_mobiledata
2146                                 )
2147                             )
2148                             .setExtras(Bundle.EMPTY)
2149                             .build(),
2150                         SmartspaceAction.Builder("id2", "title2")
2151                             .setSubtitle("")
2152                             .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
2153                             .setExtras(Bundle.EMPTY)
2154                             .build(),
2155                         SmartspaceAction.Builder("id3", "title3")
2156                             .setSubtitle("")
2157                             .setIcon(
2158                                 Icon.createWithResource(
2159                                     context,
2160                                     com.android.settingslib.R.drawable.ic_3g_mobiledata
2161                                 )
2162                             )
2163                             .setExtras(Bundle.EMPTY)
2164                             .build()
2165                     )
2166             )
2167 
2168         player.bindRecommendation(data)
2169 
2170         assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE)
2171         assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE)
2172         assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE)
2173     }
2174 
2175     @Test
2176     fun bindRecommendation_noneHaveTitles_titleAndSubtitleViewsGone() {
2177         useRealConstraintSets()
2178         player.attachRecommendation(recommendationViewHolder)
2179         val data =
2180             smartspaceData.copy(
2181                 recommendations =
2182                     listOf(
2183                         SmartspaceAction.Builder("id1", "")
2184                             .setSubtitle("subtitle1")
2185                             .setIcon(
2186                                 Icon.createWithResource(
2187                                     context,
2188                                     com.android.settingslib.R.drawable.ic_1x_mobiledata
2189                                 )
2190                             )
2191                             .setExtras(Bundle.EMPTY)
2192                             .build(),
2193                         SmartspaceAction.Builder("id2", "")
2194                             .setSubtitle("subtitle2")
2195                             .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
2196                             .setExtras(Bundle.EMPTY)
2197                             .build(),
2198                         SmartspaceAction.Builder("id3", "")
2199                             .setSubtitle("subtitle3")
2200                             .setIcon(
2201                                 Icon.createWithResource(
2202                                     context,
2203                                     com.android.settingslib.R.drawable.ic_3g_mobiledata
2204                                 )
2205                             )
2206                             .setExtras(Bundle.EMPTY)
2207                             .build()
2208                     )
2209             )
2210 
2211         player.bindRecommendation(data)
2212 
2213         assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.GONE)
2214         assertThat(expandedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.GONE)
2215         assertThat(expandedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.GONE)
2216         assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE)
2217         assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE)
2218         assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE)
2219         assertThat(collapsedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.GONE)
2220         assertThat(collapsedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.GONE)
2221         assertThat(collapsedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.GONE)
2222         assertThat(collapsedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE)
2223         assertThat(collapsedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE)
2224         assertThat(collapsedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE)
2225     }
2226 
2227     @Test
2228     fun bindRecommendation_setAfterExecutors() {
2229         val albumArt = getColorIcon(Color.RED)
2230         val data =
2231             smartspaceData.copy(
2232                 recommendations =
2233                     listOf(
2234                         SmartspaceAction.Builder("id1", "title1")
2235                             .setSubtitle("subtitle1")
2236                             .setIcon(albumArt)
2237                             .setExtras(Bundle.EMPTY)
2238                             .build(),
2239                         SmartspaceAction.Builder("id2", "title2")
2240                             .setSubtitle("subtitle1")
2241                             .setIcon(albumArt)
2242                             .setExtras(Bundle.EMPTY)
2243                             .build(),
2244                         SmartspaceAction.Builder("id3", "title3")
2245                             .setSubtitle("subtitle1")
2246                             .setIcon(albumArt)
2247                             .setExtras(Bundle.EMPTY)
2248                             .build()
2249                     )
2250             )
2251 
2252         player.attachRecommendation(recommendationViewHolder)
2253         player.bindRecommendation(data)
2254         bgExecutor.runAllReady()
2255         mainExecutor.runAllReady()
2256 
2257         verify(recCardTitle).setTextColor(any<Int>())
2258         verify(recAppIconItem, times(3)).setImageDrawable(any<Drawable>())
2259         verify(coverItem, times(3)).setImageDrawable(any<Drawable>())
2260         verify(coverItem, times(3)).imageMatrix = any()
2261     }
2262 
2263     @Test
2264     fun bindRecommendationWithProgressBars() {
2265         useRealConstraintSets()
2266         val albumArt = getColorIcon(Color.RED)
2267         val bundle =
2268             Bundle().apply {
2269                 putInt(
2270                     MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
2271                     MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
2272                 )
2273                 putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.5)
2274             }
2275         val data =
2276             smartspaceData.copy(
2277                 recommendations =
2278                     listOf(
2279                         SmartspaceAction.Builder("id1", "title1")
2280                             .setSubtitle("subtitle1")
2281                             .setIcon(albumArt)
2282                             .setExtras(bundle)
2283                             .build(),
2284                         SmartspaceAction.Builder("id2", "title2")
2285                             .setSubtitle("subtitle1")
2286                             .setIcon(albumArt)
2287                             .setExtras(Bundle.EMPTY)
2288                             .build(),
2289                         SmartspaceAction.Builder("id3", "title3")
2290                             .setSubtitle("subtitle1")
2291                             .setIcon(albumArt)
2292                             .setExtras(Bundle.EMPTY)
2293                             .build()
2294                     )
2295             )
2296 
2297         player.attachRecommendation(recommendationViewHolder)
2298         player.bindRecommendation(data)
2299 
2300         verify(recProgressBar1).setProgress(50)
2301         verify(recProgressBar1).visibility = View.VISIBLE
2302         verify(recProgressBar2).visibility = View.GONE
2303         verify(recProgressBar3).visibility = View.GONE
2304         assertThat(recSubtitle1.visibility).isEqualTo(View.GONE)
2305         assertThat(recSubtitle2.visibility).isEqualTo(View.VISIBLE)
2306         assertThat(recSubtitle3.visibility).isEqualTo(View.VISIBLE)
2307     }
2308 
2309     @Test
2310     fun bindRecommendation_carouselNotFitThreeRecs_OrientationPortrait() {
2311         useRealConstraintSets()
2312         val albumArt = getColorIcon(Color.RED)
2313         val data =
2314             smartspaceData.copy(
2315                 recommendations =
2316                     listOf(
2317                         SmartspaceAction.Builder("id1", "title1")
2318                             .setSubtitle("subtitle1")
2319                             .setIcon(albumArt)
2320                             .setExtras(Bundle.EMPTY)
2321                             .build(),
2322                         SmartspaceAction.Builder("id2", "title2")
2323                             .setSubtitle("subtitle1")
2324                             .setIcon(albumArt)
2325                             .setExtras(Bundle.EMPTY)
2326                             .build(),
2327                         SmartspaceAction.Builder("id3", "title3")
2328                             .setSubtitle("subtitle1")
2329                             .setIcon(albumArt)
2330                             .setExtras(Bundle.EMPTY)
2331                             .build()
2332                     )
2333             )
2334 
2335         // set the screen width less than the width of media controls.
2336         player.context.resources.configuration.screenWidthDp = 350
2337         player.context.resources.configuration.orientation = Configuration.ORIENTATION_PORTRAIT
2338         player.attachRecommendation(recommendationViewHolder)
2339         player.bindRecommendation(data)
2340 
2341         val res = player.context.resources
2342         val displayAvailableWidth =
2343             TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 350f, res.displayMetrics).toInt()
2344         val recCoverWidth: Int =
2345             (res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) +
2346                 res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2)
2347         val numOfRecs = displayAvailableWidth / recCoverWidth
2348 
2349         assertThat(player.numberOfFittedRecommendations).isEqualTo(numOfRecs)
2350         recommendationViewHolder.mediaCoverContainers.forEachIndexed { index, container ->
2351             if (index < numOfRecs) {
2352                 assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.VISIBLE)
2353                 assertThat(collapsedSet.getVisibility(container.id))
2354                     .isEqualTo(ConstraintSet.VISIBLE)
2355             } else {
2356                 assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE)
2357                 assertThat(collapsedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE)
2358             }
2359         }
2360     }
2361 
2362     @Test
2363     fun bindRecommendation_carouselNotFitThreeRecs_OrientationLandscape() {
2364         useRealConstraintSets()
2365         val albumArt = getColorIcon(Color.RED)
2366         val data =
2367             smartspaceData.copy(
2368                 recommendations =
2369                     listOf(
2370                         SmartspaceAction.Builder("id1", "title1")
2371                             .setSubtitle("subtitle1")
2372                             .setIcon(albumArt)
2373                             .setExtras(Bundle.EMPTY)
2374                             .build(),
2375                         SmartspaceAction.Builder("id2", "title2")
2376                             .setSubtitle("subtitle1")
2377                             .setIcon(albumArt)
2378                             .setExtras(Bundle.EMPTY)
2379                             .build(),
2380                         SmartspaceAction.Builder("id3", "title3")
2381                             .setSubtitle("subtitle1")
2382                             .setIcon(albumArt)
2383                             .setExtras(Bundle.EMPTY)
2384                             .build()
2385                     )
2386             )
2387 
2388         // set the screen width less than the width of media controls.
2389         // We should have dp width less than 378 to test. In landscape we should have 2x.
2390         player.context.resources.configuration.screenWidthDp = 700
2391         player.context.resources.configuration.orientation = Configuration.ORIENTATION_LANDSCAPE
2392         player.attachRecommendation(recommendationViewHolder)
2393         player.bindRecommendation(data)
2394 
2395         val res = player.context.resources
2396         val displayAvailableWidth =
2397             TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 350f, res.displayMetrics).toInt()
2398         val recCoverWidth: Int =
2399             (res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) +
2400                 res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2)
2401         val numOfRecs = displayAvailableWidth / recCoverWidth
2402 
2403         assertThat(player.numberOfFittedRecommendations).isEqualTo(numOfRecs)
2404         recommendationViewHolder.mediaCoverContainers.forEachIndexed { index, container ->
2405             if (index < numOfRecs) {
2406                 assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.VISIBLE)
2407                 assertThat(collapsedSet.getVisibility(container.id))
2408                     .isEqualTo(ConstraintSet.VISIBLE)
2409             } else {
2410                 assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE)
2411                 assertThat(collapsedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE)
2412             }
2413         }
2414     }
2415 
2416     @Test
2417     fun addTwoRecommendationGradients_differentStates() {
2418         // Setup redArtwork and its color scheme.
2419         val redArt = getColorIcon(Color.RED)
2420         val redWallpaperColor = player.getWallpaperColor(redArt)
2421         val redColorScheme = ColorScheme(redWallpaperColor, true, Style.CONTENT)
2422 
2423         // Setup greenArt and its color scheme.
2424         val greenArt = getColorIcon(Color.GREEN)
2425         val greenWallpaperColor = player.getWallpaperColor(greenArt)
2426         val greenColorScheme = ColorScheme(greenWallpaperColor, true, Style.CONTENT)
2427 
2428         // Add gradient to both icons.
2429         val redArtwork = player.addGradientToRecommendationAlbum(redArt, redColorScheme, 10, 10)
2430         val greenArtwork =
2431             player.addGradientToRecommendationAlbum(greenArt, greenColorScheme, 10, 10)
2432 
2433         // They should have different constant states as they have different gradient color.
2434         assertThat(redArtwork.getDrawable(1).constantState)
2435             .isNotEqualTo(greenArtwork.getDrawable(1).constantState)
2436     }
2437 
2438     @Test
2439     fun onButtonClick_playsTouchRipple() {
2440         val semanticActions =
2441             MediaButton(
2442                 playOrPause =
2443                     MediaAction(
2444                         icon = null,
2445                         action = {},
2446                         contentDescription = "play",
2447                         background = null
2448                     )
2449             )
2450         val data = mediaData.copy(semanticActions = semanticActions)
2451         player.attachPlayer(viewHolder)
2452         player.bindPlayer(data, KEY)
2453 
2454         viewHolder.actionPlayPause.callOnClick()
2455 
2456         assertThat(viewHolder.multiRippleView.ripples.size).isEqualTo(1)
2457     }
2458 
2459     @Test
2460     fun playTurbulenceNoise_finishesAfterDuration() {
2461         val semanticActions =
2462             MediaButton(
2463                 playOrPause =
2464                     MediaAction(
2465                         icon = null,
2466                         action = {},
2467                         contentDescription = "play",
2468                         background = null
2469                     )
2470             )
2471         val data = mediaData.copy(semanticActions = semanticActions)
2472         player.attachPlayer(viewHolder)
2473         player.bindPlayer(data, KEY)
2474 
2475         viewHolder.actionPlayPause.callOnClick()
2476 
2477         mainExecutor.execute {
2478             assertThat(turbulenceNoiseView.visibility).isEqualTo(View.VISIBLE)
2479             assertThat(loadingEffectView.visibility).isEqualTo(View.INVISIBLE)
2480 
2481             clock.advanceTime(
2482                 MediaControlPanel.TURBULENCE_NOISE_PLAY_DURATION +
2483                     TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS.toLong()
2484             )
2485 
2486             assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
2487             assertThat(loadingEffectView.visibility).isEqualTo(View.INVISIBLE)
2488         }
2489     }
2490 
2491     @Test
2492     @EnableFlags(Flags.FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR)
2493     fun playTurbulenceNoise_newLoadingEffect_finishesAfterDuration() {
2494         val semanticActions =
2495             MediaButton(
2496                 playOrPause =
2497                     MediaAction(
2498                         icon = null,
2499                         action = {},
2500                         contentDescription = "play",
2501                         background = null
2502                     )
2503             )
2504         val data = mediaData.copy(semanticActions = semanticActions)
2505         player.attachPlayer(viewHolder)
2506         player.bindPlayer(data, KEY)
2507 
2508         viewHolder.actionPlayPause.callOnClick()
2509 
2510         mainExecutor.execute {
2511             assertThat(loadingEffectView.visibility).isEqualTo(View.VISIBLE)
2512             assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
2513 
2514             clock.advanceTime(
2515                 MediaControlPanel.TURBULENCE_NOISE_PLAY_DURATION +
2516                     TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS.toLong()
2517             )
2518 
2519             assertThat(loadingEffectView.visibility).isEqualTo(View.INVISIBLE)
2520             assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
2521         }
2522     }
2523 
2524     @Test
2525     fun playTurbulenceNoise_whenPlaybackStateIsNotPlaying_doesNotPlayTurbulence() {
2526         val semanticActions =
2527             MediaButton(
2528                 custom0 =
2529                     MediaAction(
2530                         icon = null,
2531                         action = {},
2532                         contentDescription = "custom0",
2533                         background = null
2534                     ),
2535             )
2536         val data = mediaData.copy(semanticActions = semanticActions)
2537         player.attachPlayer(viewHolder)
2538         player.bindPlayer(data, KEY)
2539 
2540         viewHolder.action0.callOnClick()
2541 
2542         assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
2543         assertThat(loadingEffectView.visibility).isEqualTo(View.INVISIBLE)
2544     }
2545 
2546     @Test
2547     @EnableFlags(Flags.FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR)
2548     fun playTurbulenceNoise_newLoadingEffect_whenPlaybackStateIsNotPlaying_doesNotPlayTurbulence() {
2549         val semanticActions =
2550             MediaButton(
2551                 custom0 =
2552                     MediaAction(
2553                         icon = null,
2554                         action = {},
2555                         contentDescription = "custom0",
2556                         background = null
2557                     ),
2558             )
2559         val data = mediaData.copy(semanticActions = semanticActions)
2560         player.attachPlayer(viewHolder)
2561         player.bindPlayer(data, KEY)
2562 
2563         viewHolder.action0.callOnClick()
2564 
2565         assertThat(loadingEffectView.visibility).isEqualTo(View.INVISIBLE)
2566         assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
2567     }
2568 
2569     @Test
2570     fun outputSwitcher_hasCustomIntent_openOverLockscreen() {
2571         // When the device for a media player has an intent that opens over lockscreen
2572         val pendingIntent = mock(PendingIntent::class.java)
2573         whenever(pendingIntent.isActivity).thenReturn(true)
2574         whenever(keyguardStateController.isShowing).thenReturn(true)
2575         whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any())).thenReturn(true)
2576 
2577         val customDevice = device.copy(intent = pendingIntent)
2578         val dataWithDevice = mediaData.copy(device = customDevice)
2579         player.attachPlayer(viewHolder)
2580         player.bindPlayer(dataWithDevice, KEY)
2581 
2582         // When the user taps on the output switcher,
2583         seamless.callOnClick()
2584 
2585         // Then we send the pending intent as is, without modifying the original intent
2586         verify(pendingIntent).send(any<Bundle>())
2587         verify(pendingIntent, never()).getIntent()
2588     }
2589 
2590     @Test
2591     fun outputSwitcher_hasCustomIntent_requiresUnlock() {
2592         // When the device for a media player has an intent that cannot open over lockscreen
2593         val pendingIntent = mock(PendingIntent::class.java)
2594         whenever(pendingIntent.isActivity).thenReturn(true)
2595         whenever(keyguardStateController.isShowing).thenReturn(true)
2596         whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any()))
2597             .thenReturn(false)
2598 
2599         val customDevice = device.copy(intent = pendingIntent)
2600         val dataWithDevice = mediaData.copy(device = customDevice)
2601         player.attachPlayer(viewHolder)
2602         player.bindPlayer(dataWithDevice, KEY)
2603 
2604         // When the user taps on the output switcher,
2605         seamless.callOnClick()
2606 
2607         // Then we request keyguard dismissal
2608         verify(activityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent))
2609     }
2610 
2611     private fun getColorIcon(color: Int): Icon {
2612         val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
2613         val canvas = Canvas(bmp)
2614         canvas.drawColor(color)
2615         return Icon.createWithBitmap(bmp)
2616     }
2617 
2618     private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener {
2619         val captor = argumentCaptor<SeekBarViewModel.ScrubbingChangeListener>()
2620         verify(seekBarViewModel).setScrubbingChangeListener(captor.capture())
2621         return captor.lastValue
2622     }
2623 
2624     private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener {
2625         val captor = argumentCaptor<SeekBarViewModel.EnabledChangeListener>()
2626         verify(seekBarViewModel).setEnabledChangeListener(captor.capture())
2627         return captor.lastValue
2628     }
2629 
2630     /**
2631      * Update our test to use real ConstraintSets instead of mocks.
2632      *
2633      * Some item visibilities, such as the seekbar visibility, are dependent on other action's
2634      * visibilities. If we use mocks for the ConstraintSets, then action visibility changes are just
2635      * thrown away instead of being saved for reference later. This method sets us up to use
2636      * ConstraintSets so that we do save visibility changes.
2637      *
2638      * TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests?
2639      */
2640     private fun useRealConstraintSets() {
2641         expandedSet = ConstraintSet()
2642         collapsedSet = ConstraintSet()
2643         whenever(mediaViewController.expandedLayout).thenReturn(expandedSet)
2644         whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
2645     }
2646 }
2647