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