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