• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.binder
18 
19 import android.content.Context
20 import android.graphics.BlendMode
21 import android.graphics.Color
22 import android.graphics.ColorMatrix
23 import android.graphics.ColorMatrixColorFilter
24 import android.graphics.drawable.Animatable
25 import android.graphics.drawable.ColorDrawable
26 import android.graphics.drawable.GradientDrawable
27 import android.graphics.drawable.LayerDrawable
28 import android.graphics.drawable.TransitionDrawable
29 import android.os.Trace
30 import android.util.Pair
31 import android.view.Gravity
32 import android.view.View
33 import android.widget.ImageButton
34 import androidx.constraintlayout.widget.ConstraintSet
35 import androidx.lifecycle.Lifecycle
36 import androidx.lifecycle.repeatOnLifecycle
37 import com.android.app.tracing.coroutines.launchTraced as launch
38 import com.android.settingslib.widget.AdaptiveIcon
39 import com.android.systemui.Flags
40 import com.android.systemui.animation.Expandable
41 import com.android.systemui.common.shared.model.Icon
42 import com.android.systemui.dagger.qualifiers.Background
43 import com.android.systemui.dagger.qualifiers.Main
44 import com.android.systemui.lifecycle.repeatWhenAttached
45 import com.android.systemui.media.controls.ui.animation.AnimationBindHandler
46 import com.android.systemui.media.controls.ui.animation.ColorSchemeTransition
47 import com.android.systemui.media.controls.ui.controller.MediaViewController
48 import com.android.systemui.media.controls.ui.util.MediaArtworkHelper
49 import com.android.systemui.media.controls.ui.view.MediaViewHolder
50 import com.android.systemui.media.controls.ui.viewmodel.MediaActionViewModel
51 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel
52 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_END_ALPHA
53 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY
54 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_START_ALPHA
55 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY
56 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.SEMANTIC_ACTIONS_ALL
57 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.SEMANTIC_ACTIONS_COMPACT
58 import com.android.systemui.media.controls.ui.viewmodel.MediaOutputSwitcherViewModel
59 import com.android.systemui.media.controls.ui.viewmodel.MediaPlayerViewModel
60 import com.android.systemui.media.controls.util.MediaDataUtils
61 import com.android.systemui.monet.ColorScheme
62 import com.android.systemui.monet.Style
63 import com.android.systemui.plugins.FalsingManager
64 import com.android.systemui.res.R
65 import com.android.systemui.surfaceeffects.ripple.MultiRippleView
66 import com.android.systemui.surfaceeffects.ripple.RippleAnimation
67 import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig
68 import com.android.systemui.surfaceeffects.ripple.RippleShader
69 import kotlinx.coroutines.CoroutineDispatcher
70 import kotlinx.coroutines.flow.collectLatest
71 import kotlinx.coroutines.withContext
72 
73 private const val TAG = "MediaControlViewBinder"
74 
75 object MediaControlViewBinder {
76 
77     fun bind(
78         viewHolder: MediaViewHolder,
79         viewModel: MediaControlViewModel,
80         viewController: MediaViewController,
81         falsingManager: FalsingManager,
82         @Background backgroundDispatcher: CoroutineDispatcher,
83         @Main mainDispatcher: CoroutineDispatcher,
84     ) {
85         val mediaCard = viewHolder.player
86         mediaCard.repeatWhenAttached {
87             repeatOnLifecycle(Lifecycle.State.STARTED) {
88                 launch {
89                     viewModel.player.collectLatest { player ->
90                         player?.let {
91                             if (viewModel.setPlayer(it)) {
92                                 bindMediaCard(
93                                     viewHolder,
94                                     viewController,
95                                     it,
96                                     falsingManager,
97                                     backgroundDispatcher,
98                                     mainDispatcher,
99                                 )
100                                 viewModel.onMediaControlIsBound(it.artistName, it.titleName)
101                             }
102                         }
103                     }
104                 }
105             }
106         }
107     }
108 
109     suspend fun bindMediaCard(
110         viewHolder: MediaViewHolder,
111         viewController: MediaViewController,
112         viewModel: MediaPlayerViewModel,
113         falsingManager: FalsingManager,
114         backgroundDispatcher: CoroutineDispatcher,
115         mainDispatcher: CoroutineDispatcher,
116     ) {
117         // Set up media control location and its listener.
118         viewModel.onLocationChanged(viewController.currentEndLocation)
119         viewController.locationChangeListener = viewModel.onLocationChanged
120 
121         with(viewHolder) {
122             // AlbumView uses a hardware layer so that clipping of the foreground is handled with
123             // clipping the album art. Otherwise album art shows through at the edges.
124             albumView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
125             turbulenceNoiseView.setBlendMode(BlendMode.SCREEN)
126             loadingEffectView.setBlendMode(BlendMode.SCREEN)
127             loadingEffectView.visibility = View.INVISIBLE
128 
129             player.contentDescription =
130                 viewModel.contentDescription.invoke(viewController.isGutsVisible)
131             player.setOnClickListener {
132                 if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
133                     if (!viewController.isGutsVisible) {
134                         viewModel.onClicked(Expandable.fromView(player))
135                     }
136                 }
137             }
138             player.setOnLongClickListener {
139                 if (!falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
140                     if (!viewController.isGutsVisible) {
141                         openGuts(viewHolder, viewController, viewModel)
142                     } else {
143                         closeGuts(viewHolder, viewController, viewModel)
144                     }
145                 }
146                 return@setOnLongClickListener true
147             }
148         }
149 
150         viewController.bindSeekBar(viewModel.onSeek, viewModel.onBindSeekbar)
151         bindOutputSwitcherModel(
152             viewHolder,
153             viewModel.outputSwitcher,
154             viewController,
155             falsingManager,
156         )
157         bindGutsViewModel(viewHolder, viewModel, viewController, falsingManager)
158         bindActionButtons(viewHolder, viewModel, viewController, falsingManager)
159         bindScrubbingTime(viewHolder, viewModel, viewController)
160 
161         val isSongUpdated = bindSongMetadata(viewHolder, viewModel, viewController)
162 
163         bindArtworkAndColor(
164             viewHolder,
165             viewModel,
166             viewController,
167             backgroundDispatcher,
168             mainDispatcher,
169             isSongUpdated,
170         )
171 
172         if (viewModel.playTurbulenceNoise) {
173             viewController.setUpTurbulenceNoise()
174         }
175 
176         // TODO: We don't need to refresh this state constantly, only if the state actually changed
177         // to something which might impact the measurement
178         // State refresh interferes with the translation animation, only run it if it's not running.
179         if (!viewController.metadataAnimationHandler.isRunning) {
180             viewController.refreshState()
181         }
182     }
183 
184     private fun bindOutputSwitcherModel(
185         viewHolder: MediaViewHolder,
186         viewModel: MediaOutputSwitcherViewModel,
187         viewController: MediaViewController,
188         falsingManager: FalsingManager,
189     ) {
190         with(viewHolder.seamless) {
191             visibility = View.VISIBLE
192             isEnabled = viewModel.isTapEnabled
193             contentDescription = viewModel.deviceString
194             setOnClickListener {
195                 if (!falsingManager.isFalseTap(FalsingManager.MODERATE_PENALTY)) {
196                     viewModel.onClicked.invoke(Expandable.fromView(viewHolder.seamlessButton))
197                 }
198             }
199         }
200         when (viewModel.deviceIcon) {
201             is Icon.Loaded -> {
202                 val icon = viewModel.deviceIcon.drawable
203                 if (icon is AdaptiveIcon) {
204                     icon.setBackgroundColor(
205                         viewController.colorSchemeTransition.getDeviceIconColor()
206                     )
207                 }
208                 viewHolder.seamlessIcon.setImageDrawable(icon)
209             }
210             is Icon.Resource -> viewHolder.seamlessIcon.setImageResource(viewModel.deviceIcon.res)
211         }
212         viewHolder.seamlessButton.alpha = viewModel.alpha
213         viewHolder.seamlessText.text = viewModel.deviceString
214     }
215 
216     private fun bindGutsViewModel(
217         viewHolder: MediaViewHolder,
218         viewModel: MediaPlayerViewModel,
219         viewController: MediaViewController,
220         falsingManager: FalsingManager,
221     ) {
222         val gutsViewHolder = viewHolder.gutsViewHolder
223         val model = viewModel.gutsMenu
224         with(gutsViewHolder) {
225             gutsText.text = model.gutsText
226             dismissText.visibility = if (model.isDismissEnabled) View.VISIBLE else View.GONE
227             dismiss.isEnabled = model.isDismissEnabled
228             dismiss.setOnClickListener {
229                 if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
230                     model.onDismissClicked()
231                 }
232             }
233             cancelText.background = model.cancelTextBackground
234             cancel.setOnClickListener {
235                 if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
236                     closeGuts(viewHolder, viewController, viewModel)
237                 }
238             }
239             settings.setOnClickListener {
240                 if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
241                     model.onSettingsClicked.invoke()
242                 }
243             }
244             setDismissible(model.isDismissEnabled)
245         }
246     }
247 
248     private fun bindActionButtons(
249         viewHolder: MediaViewHolder,
250         viewModel: MediaPlayerViewModel,
251         viewController: MediaViewController,
252         falsingManager: FalsingManager,
253     ) {
254         val genericButtons = MediaViewHolder.genericButtonIds.map { viewHolder.getAction(it) }
255         val expandedSet = viewController.expandedLayout
256         val collapsedSet = viewController.collapsedLayout
257         if (viewModel.useSemanticActions) {
258             // Hide all generic buttons
259             genericButtons.forEach {
260                 setVisibleAndAlpha(expandedSet, it.id, false)
261                 setVisibleAndAlpha(collapsedSet, it.id, false)
262             }
263 
264             SEMANTIC_ACTIONS_ALL.forEachIndexed { index, id ->
265                 val buttonView = viewHolder.getAction(id)
266                 val buttonModel = viewModel.actionButtons[index]
267                 if (buttonView.id == R.id.actionPrev) {
268                     viewController.setUpPrevButtonInfo(
269                         buttonModel.isEnabled,
270                         buttonModel.notVisibleValue,
271                     )
272                 } else if (buttonView.id == R.id.actionNext) {
273                     viewController.setUpNextButtonInfo(
274                         buttonModel.isEnabled,
275                         buttonModel.notVisibleValue,
276                     )
277                 }
278                 val animHandler = (buttonView.tag ?: AnimationBindHandler()) as AnimationBindHandler
279                 animHandler.tryExecute {
280                     if (buttonModel.isEnabled) {
281                         if (animHandler.updateRebindId(buttonModel.rebindId)) {
282                             animHandler.unregisterAll()
283                             animHandler.tryRegister(buttonModel.icon)
284                             animHandler.tryRegister(buttonModel.background)
285                             bindButtonCommon(
286                                 buttonView,
287                                 viewHolder.multiRippleView,
288                                 buttonModel,
289                                 viewController,
290                                 falsingManager,
291                             )
292                         }
293                     } else {
294                         animHandler.unregisterAll()
295                         clearButton(buttonView)
296                     }
297                     val visible =
298                         buttonModel.isEnabled &&
299                             (buttonModel.isVisibleWhenScrubbing || !viewController.isScrubbing)
300                     setSemanticButtonVisibleAndAlpha(
301                         viewHolder.getAction(id),
302                         viewController.expandedLayout,
303                         viewController.collapsedLayout,
304                         visible,
305                         buttonModel.notVisibleValue,
306                         buttonModel.showInCollapsed,
307                     )
308                 }
309             }
310         } else {
311             // Hide buttons that only appear for semantic actions
312             SEMANTIC_ACTIONS_COMPACT.forEach { buttonId ->
313                 setVisibleAndAlpha(expandedSet, buttonId, visible = false)
314                 setVisibleAndAlpha(expandedSet, buttonId, visible = false)
315             }
316 
317             // Set all generic buttons
318             genericButtons.forEachIndexed { index, button ->
319                 if (index < viewModel.actionButtons.size) {
320                     val action = viewModel.actionButtons[index]
321                     bindButtonCommon(
322                         button,
323                         viewHolder.multiRippleView,
324                         action,
325                         viewController,
326                         falsingManager,
327                     )
328                     setVisibleAndAlpha(expandedSet, button.id, visible = true)
329                     setVisibleAndAlpha(collapsedSet, button.id, visible = action.showInCollapsed)
330                 } else {
331                     // Hide any unused buttons
332                     clearButton(button)
333                     setVisibleAndAlpha(expandedSet, button.id, visible = false)
334                     setVisibleAndAlpha(collapsedSet, button.id, visible = false)
335                 }
336             }
337         }
338         updateSeekBarVisibility(viewController.expandedLayout, viewController.isSeekBarEnabled)
339     }
340 
341     private fun bindButtonCommon(
342         button: ImageButton,
343         multiRippleView: MultiRippleView,
344         actionViewModel: MediaActionViewModel,
345         viewController: MediaViewController,
346         falsingManager: FalsingManager,
347     ) {
348         button.setImageDrawable(actionViewModel.icon)
349         button.background = actionViewModel.background
350         button.contentDescription = actionViewModel.contentDescription
351         button.isEnabled = actionViewModel.isEnabled
352         if (actionViewModel.isEnabled) {
353             button.setOnClickListener {
354                 if (!falsingManager.isFalseTap(FalsingManager.MODERATE_PENALTY)) {
355                     actionViewModel.onClicked(it.id)
356 
357                     viewController.multiRippleController.play(
358                         createTouchRippleAnimation(
359                             button,
360                             viewController.colorSchemeTransition,
361                             multiRippleView,
362                         )
363                     )
364 
365                     if (actionViewModel.icon is Animatable) {
366                         actionViewModel.icon.start()
367                     }
368 
369                     if (actionViewModel.background is Animatable) {
370                         actionViewModel.background.start()
371                     }
372                 }
373             }
374         }
375     }
376 
377     private fun bindSongMetadata(
378         viewHolder: MediaViewHolder,
379         viewModel: MediaPlayerViewModel,
380         viewController: MediaViewController,
381     ): Boolean {
382         val expandedSet = viewController.expandedLayout
383         val collapsedSet = viewController.collapsedLayout
384 
385         return viewController.metadataAnimationHandler.setNext(
386             Triple(viewModel.titleName, viewModel.artistName, viewModel.isExplicitVisible),
387             {
388                 viewHolder.titleText.text = viewModel.titleName
389                 viewHolder.artistText.text = viewModel.artistName
390                 setVisibleAndAlpha(
391                     expandedSet,
392                     R.id.media_explicit_indicator,
393                     viewModel.isExplicitVisible,
394                 )
395                 setVisibleAndAlpha(
396                     collapsedSet,
397                     R.id.media_explicit_indicator,
398                     viewModel.isExplicitVisible,
399                 )
400 
401                 // refreshState is required here to resize the text views (and prevent ellipsis)
402                 viewController.refreshState()
403             },
404             {
405                 // After finishing the enter animation, we refresh state. This could pop if
406                 // something is incorrectly bound, but needs to be run if other elements were
407                 // updated while the enter animation was running
408                 viewController.refreshState()
409             },
410         )
411     }
412 
413     private suspend fun bindArtworkAndColor(
414         viewHolder: MediaViewHolder,
415         viewModel: MediaPlayerViewModel,
416         viewController: MediaViewController,
417         backgroundDispatcher: CoroutineDispatcher,
418         mainDispatcher: CoroutineDispatcher,
419         updateBackground: Boolean,
420     ) {
421         val traceCookie = viewHolder.hashCode()
422         val traceName = "MediaControlViewBinder#bindArtworkAndColor"
423         Trace.beginAsyncSection(traceName, traceCookie)
424         if (updateBackground) {
425             viewController.isArtworkBound = false
426         }
427         // Capture width & height from views in foreground for artwork scaling in background
428         val width = viewController.widthInSceneContainerPx
429         val height = viewController.heightInSceneContainerPx
430         withContext(backgroundDispatcher) {
431             val wallpaperColors =
432                 MediaArtworkHelper.getWallpaperColor(
433                     viewHolder.albumView.context,
434                     backgroundDispatcher,
435                     viewModel.backgroundCover,
436                     TAG,
437                 )
438             val isArtworkBound = wallpaperColors != null
439             val darkTheme = !Flags.mediaControlsA11yColors()
440             val scheme =
441                 wallpaperColors?.let { ColorScheme(it, darkTheme, Style.CONTENT) }
442                     ?: let {
443                         if (viewModel.launcherIcon is Icon.Loaded) {
444                             MediaArtworkHelper.getColorScheme(
445                                 viewModel.launcherIcon.drawable,
446                                 TAG,
447                                 darkTheme,
448                             )
449                         } else {
450                             null
451                         }
452                     }
453             val artwork =
454                 wallpaperColors?.let {
455                     addGradientToPlayerAlbum(
456                         viewHolder.albumView.context,
457                         viewModel.backgroundCover!!,
458                         scheme!!,
459                         width,
460                         height,
461                     )
462                 } ?: ColorDrawable(Color.TRANSPARENT)
463             withContext(mainDispatcher) {
464                 // Transition Colors to current color scheme
465                 val colorSchemeChanged =
466                     viewController.colorSchemeTransition.updateColorScheme(scheme)
467                 val albumView = viewHolder.albumView
468 
469                 // Set up width of album view constraint.
470                 viewController.expandedLayout.getConstraint(albumView.id).layout.mWidth = width
471                 viewController.collapsedLayout.getConstraint(albumView.id).layout.mWidth = width
472 
473                 albumView.setPadding(0, 0, 0, 0)
474                 if (
475                     updateBackground ||
476                         colorSchemeChanged ||
477                         (!viewController.isArtworkBound && isArtworkBound)
478                 ) {
479                     viewController.prevArtwork?.let {
480                         // Since we throw away the last transition, this will pop if your
481                         // backgrounds are cycled too fast (or the correct background arrives very
482                         // soon after the metadata changes).
483                         val transitionDrawable = TransitionDrawable(arrayOf(it, artwork))
484 
485                         scaleTransitionDrawableLayer(transitionDrawable, 0, width, height)
486                         scaleTransitionDrawableLayer(transitionDrawable, 1, width, height)
487                         transitionDrawable.setLayerGravity(0, Gravity.CENTER)
488                         transitionDrawable.setLayerGravity(1, Gravity.CENTER)
489                         transitionDrawable.isCrossFadeEnabled = true
490 
491                         albumView.setImageDrawable(transitionDrawable)
492                         transitionDrawable.startTransition(if (isArtworkBound) 333 else 80)
493                     } ?: albumView.setImageDrawable(artwork)
494                 }
495                 viewController.isArtworkBound = isArtworkBound
496                 viewController.prevArtwork = artwork
497 
498                 if (viewModel.useGrayColorFilter) {
499                     // Used for resume players to use launcher icon
500                     viewHolder.appIcon.colorFilter = getGrayscaleFilter()
501                     when (viewModel.launcherIcon) {
502                         is Icon.Loaded ->
503                             viewHolder.appIcon.setImageDrawable(viewModel.launcherIcon.drawable)
504                         is Icon.Resource ->
505                             viewHolder.appIcon.setImageResource(viewModel.launcherIcon.res)
506                     }
507                 } else {
508                     viewHolder.appIcon.setColorFilter(
509                         viewController.colorSchemeTransition.getAppIconColor()
510                     )
511                     viewHolder.appIcon.setImageIcon(viewModel.appIcon)
512                 }
513                 Trace.endAsyncSection(traceName, traceCookie)
514             }
515         }
516     }
517 
518     private fun scaleTransitionDrawableLayer(
519         transitionDrawable: TransitionDrawable,
520         layer: Int,
521         targetWidth: Int,
522         targetHeight: Int,
523     ) {
524         val drawable = transitionDrawable.getDrawable(layer) ?: return
525         val width = drawable.intrinsicWidth
526         val height = drawable.intrinsicHeight
527         val scale =
528             MediaDataUtils.getScaleFactor(Pair(width, height), Pair(targetWidth, targetHeight))
529         if (scale == 0f) return
530         transitionDrawable.setLayerSize(layer, (scale * width).toInt(), (scale * height).toInt())
531     }
532 
533     private fun addGradientToPlayerAlbum(
534         context: Context,
535         artworkIcon: android.graphics.drawable.Icon,
536         mutableColorScheme: ColorScheme,
537         width: Int,
538         height: Int,
539     ): LayerDrawable {
540         val albumArt = MediaArtworkHelper.getScaledBackground(context, artworkIcon, width, height)
541         val startAlpha =
542             if (Flags.mediaControlsA11yColors()) {
543                 MEDIA_PLAYER_SCRIM_START_ALPHA
544             } else {
545                 MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY
546             }
547         val endAlpha =
548             if (Flags.mediaControlsA11yColors()) {
549                 MEDIA_PLAYER_SCRIM_END_ALPHA
550             } else {
551                 MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY
552             }
553         return MediaArtworkHelper.setUpGradientColorOnDrawable(
554             albumArt,
555             context.getDrawable(R.drawable.qs_media_scrim)?.mutate() as GradientDrawable,
556             mutableColorScheme,
557             startAlpha,
558             endAlpha,
559         )
560     }
561 
562     private fun clearButton(button: ImageButton) {
563         button.setImageDrawable(null)
564         button.contentDescription = null
565         button.isEnabled = false
566         button.background = null
567     }
568 
569     private fun bindScrubbingTime(
570         viewHolder: MediaViewHolder,
571         viewModel: MediaPlayerViewModel,
572         viewController: MediaViewController,
573     ) {
574         val expandedSet = viewController.expandedLayout
575         val visible = viewModel.canShowTime && viewController.isScrubbing
576         viewController.canShowScrubbingTime = viewModel.canShowTime
577         setVisibleAndAlpha(expandedSet, viewHolder.scrubbingElapsedTimeView.id, visible)
578         setVisibleAndAlpha(expandedSet, viewHolder.scrubbingTotalTimeView.id, visible)
579         // Collapsed view is always GONE as set in XML, so doesn't need to be updated dynamically.
580     }
581 
582     private fun createTouchRippleAnimation(
583         button: ImageButton,
584         colorSchemeTransition: ColorSchemeTransition,
585         multiRippleView: MultiRippleView,
586     ): RippleAnimation {
587         val maxSize = (multiRippleView.width * 2).toFloat()
588         return RippleAnimation(
589             RippleAnimationConfig(
590                 RippleShader.RippleShape.CIRCLE,
591                 duration = 1500L,
592                 centerX = button.x + button.width * 0.5f,
593                 centerY = button.y + button.height * 0.5f,
594                 maxSize,
595                 maxSize,
596                 button.context.resources.displayMetrics.density,
597                 colorSchemeTransition.getSurfaceEffectColor(),
598                 opacity = 100,
599                 sparkleStrength = 0f,
600                 baseRingFadeParams = null,
601                 sparkleRingFadeParams = null,
602                 centerFillFadeParams = null,
603                 shouldDistort = false,
604             )
605         )
606     }
607 
608     private fun openGuts(
609         viewHolder: MediaViewHolder,
610         viewController: MediaViewController,
611         viewModel: MediaPlayerViewModel,
612     ) {
613         viewHolder.marquee(true, MediaViewController.GUTS_ANIMATION_DURATION)
614         viewController.openGuts()
615         viewHolder.player.contentDescription = viewModel.contentDescription.invoke(true)
616         viewModel.onLongClicked.invoke()
617     }
618 
619     private fun closeGuts(
620         viewHolder: MediaViewHolder,
621         viewController: MediaViewController,
622         viewModel: MediaPlayerViewModel,
623     ) {
624         viewHolder.marquee(false, MediaViewController.GUTS_ANIMATION_DURATION)
625         viewController.closeGuts(false)
626         viewHolder.player.contentDescription = viewModel.contentDescription.invoke(false)
627     }
628 
629     fun setVisibleAndAlpha(set: ConstraintSet, resId: Int, visible: Boolean) {
630         setVisibleAndAlpha(set, resId, visible, ConstraintSet.GONE)
631     }
632 
633     private fun setVisibleAndAlpha(
634         set: ConstraintSet,
635         resId: Int,
636         visible: Boolean,
637         notVisibleValue: Int,
638     ) {
639         set.setVisibility(resId, if (visible) ConstraintSet.VISIBLE else notVisibleValue)
640         set.setAlpha(resId, if (visible) 1.0f else 0.0f)
641     }
642 
643     fun updateSeekBarVisibility(constraintSet: ConstraintSet, isSeekBarEnabled: Boolean) {
644         if (isSeekBarEnabled) {
645             constraintSet.setVisibility(R.id.media_progress_bar, ConstraintSet.VISIBLE)
646             constraintSet.setAlpha(R.id.media_progress_bar, 1.0f)
647         } else {
648             constraintSet.setVisibility(R.id.media_progress_bar, ConstraintSet.INVISIBLE)
649             constraintSet.setAlpha(R.id.media_progress_bar, 0.0f)
650         }
651     }
652 
653     fun setSemanticButtonVisibleAndAlpha(
654         button: ImageButton,
655         expandedSet: ConstraintSet,
656         collapsedSet: ConstraintSet,
657         visible: Boolean,
658         notVisibleValue: Int,
659         showInCollapsed: Boolean,
660     ) {
661         if (notVisibleValue == ConstraintSet.INVISIBLE) {
662             // Since time views should appear instead of buttons.
663             button.isFocusable = visible
664             button.isClickable = visible
665         }
666         setVisibleAndAlpha(expandedSet, button.id, visible, notVisibleValue)
667         setVisibleAndAlpha(collapsedSet, button.id, visible = visible && showInCollapsed)
668     }
669 
670     private fun getGrayscaleFilter(): ColorMatrixColorFilter {
671         val matrix = ColorMatrix()
672         matrix.setSaturation(0f)
673         return ColorMatrixColorFilter(matrix)
674     }
675 }
676