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