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.content.res.ColorStateList
21 import android.content.res.Configuration
22 import android.graphics.Matrix
23 import android.util.TypedValue
24 import android.view.View
25 import android.view.ViewGroup
26 import androidx.constraintlayout.widget.ConstraintSet
27 import androidx.lifecycle.Lifecycle
28 import androidx.lifecycle.lifecycleScope
29 import androidx.lifecycle.repeatOnLifecycle
30 import com.android.systemui.animation.Expandable
31 import com.android.systemui.lifecycle.repeatWhenAttached
32 import com.android.systemui.media.controls.shared.model.NUM_REQUIRED_RECOMMENDATIONS
33 import com.android.systemui.media.controls.ui.controller.MediaViewController
34 import com.android.systemui.media.controls.ui.view.RecommendationViewHolder
35 import com.android.systemui.media.controls.ui.viewmodel.MediaRecViewModel
36 import com.android.systemui.media.controls.ui.viewmodel.MediaRecommendationsViewModel
37 import com.android.systemui.media.controls.ui.viewmodel.MediaRecsCardViewModel
38 import com.android.systemui.plugins.FalsingManager
39 import com.android.systemui.res.R
40 import com.android.systemui.util.animation.TransitionLayout
41 import kotlin.math.min
42 import kotlinx.coroutines.flow.collectLatest
43 import kotlinx.coroutines.launch
44 
45 object MediaRecommendationsViewBinder {
46 
47     /** Binds recommendations view holder to the given view-model */
48     fun bind(
49         viewHolder: RecommendationViewHolder,
50         viewModel: MediaRecommendationsViewModel,
51         mediaViewController: MediaViewController,
52         falsingManager: FalsingManager,
53     ) {
54         mediaViewController.recsConfigurationChangeListener = this::updateRecommendationsVisibility
55         val cardView = viewHolder.recommendations
56         cardView.repeatWhenAttached {
57             lifecycleScope.launch {
58                 repeatOnLifecycle(Lifecycle.State.STARTED) {
59                     launch {
60                         viewModel.mediaRecsCard.collectLatest { viewModel ->
61                             viewModel?.let {
62                                 bindRecsCard(viewHolder, it, mediaViewController, falsingManager)
63                             }
64                         }
65                     }
66                 }
67             }
68         }
69     }
70 
71     fun bindRecsCard(
72         viewHolder: RecommendationViewHolder,
73         viewModel: MediaRecsCardViewModel,
74         mediaViewController: MediaViewController,
75         falsingManager: FalsingManager,
76     ) {
77         // Bind main card.
78         viewHolder.cardTitle.setTextColor(viewModel.cardTitleColor)
79         viewHolder.recommendations.backgroundTintList = ColorStateList.valueOf(viewModel.cardColor)
80         viewHolder.recommendations.contentDescription =
81             viewModel.contentDescription.invoke(mediaViewController.isGutsVisible)
82 
83         viewHolder.recommendations.setOnClickListener {
84             if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@setOnClickListener
85             viewModel.onClicked(Expandable.fromView(it))
86         }
87 
88         viewHolder.recommendations.setOnLongClickListener {
89             if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY))
90                 return@setOnLongClickListener true
91             if (!mediaViewController.isGutsVisible) {
92                 openGuts(viewHolder, viewModel, mediaViewController)
93             } else {
94                 closeGuts(viewHolder, viewModel, mediaViewController)
95             }
96             return@setOnLongClickListener true
97         }
98 
99         // Bind all recommendations.
100         bindRecommendationsList(viewHolder, viewModel.mediaRecs, falsingManager)
101         updateRecommendationsVisibility(mediaViewController, viewHolder.recommendations)
102 
103         // Set visibility of recommendations.
104         val expandedSet: ConstraintSet = mediaViewController.expandedLayout
105         val collapsedSet: ConstraintSet = mediaViewController.collapsedLayout
106         viewHolder.mediaTitles.forEach {
107             setVisibleAndAlpha(expandedSet, it.id, viewModel.areTitlesVisible)
108             setVisibleAndAlpha(collapsedSet, it.id, viewModel.areTitlesVisible)
109         }
110         viewHolder.mediaSubtitles.forEach {
111             setVisibleAndAlpha(expandedSet, it.id, viewModel.areSubtitlesVisible)
112             setVisibleAndAlpha(collapsedSet, it.id, viewModel.areSubtitlesVisible)
113         }
114 
115         bindRecommendationsGuts(viewHolder, viewModel, mediaViewController, falsingManager)
116 
117         mediaViewController.refreshState()
118     }
119 
120     private fun bindRecommendationsGuts(
121         viewHolder: RecommendationViewHolder,
122         viewModel: MediaRecsCardViewModel,
123         mediaViewController: MediaViewController,
124         falsingManager: FalsingManager,
125     ) {
126         val gutsViewHolder = viewHolder.gutsViewHolder
127         val gutsViewModel = viewModel.gutsMenu
128 
129         gutsViewHolder.gutsText.text = gutsViewModel.gutsText
130         gutsViewHolder.dismissText.visibility = View.VISIBLE
131         gutsViewHolder.dismiss.isEnabled = true
132         gutsViewHolder.dismiss.setOnClickListener {
133             if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@setOnClickListener
134             closeGuts(viewHolder, viewModel, mediaViewController)
135             gutsViewModel.onDismissClicked.invoke()
136         }
137 
138         gutsViewHolder.cancelText.background = gutsViewModel.cancelTextBackground
139         gutsViewHolder.cancel.setOnClickListener {
140             if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
141                 closeGuts(viewHolder, viewModel, mediaViewController)
142             }
143         }
144 
145         gutsViewHolder.settings.setOnClickListener {
146             if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
147                 gutsViewModel.onSettingsClicked.invoke()
148             }
149         }
150 
151         gutsViewHolder.setDismissible(gutsViewModel.isDismissEnabled)
152         gutsViewHolder.setTextPrimaryColor(gutsViewModel.textPrimaryColor)
153         gutsViewHolder.setAccentPrimaryColor(gutsViewModel.accentPrimaryColor)
154         gutsViewHolder.setSurfaceColor(gutsViewModel.surfaceColor)
155     }
156 
157     private fun bindRecommendationsList(
158         viewHolder: RecommendationViewHolder,
159         mediaRecs: List<MediaRecViewModel>,
160         falsingManager: FalsingManager
161     ) {
162         mediaRecs.forEachIndexed { index, mediaRecViewModel ->
163             if (index >= NUM_REQUIRED_RECOMMENDATIONS) return@forEachIndexed
164 
165             val appIconView = viewHolder.mediaAppIcons[index]
166             appIconView.clearColorFilter()
167             if (mediaRecViewModel.appIcon != null) {
168                 appIconView.setImageDrawable(mediaRecViewModel.appIcon)
169             } else {
170                 appIconView.setImageResource(R.drawable.ic_music_note)
171             }
172 
173             val mediaCoverContainer = viewHolder.mediaCoverContainers[index]
174             mediaCoverContainer.setOnClickListener {
175                 if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@setOnClickListener
176                 mediaRecViewModel.onClicked.invoke(Expandable.fromView(it), index)
177             }
178             mediaCoverContainer.setOnLongClickListener {
179                 if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY))
180                     return@setOnLongClickListener true
181                 (it.parent as View).performLongClick()
182                 return@setOnLongClickListener true
183             }
184 
185             val mediaCover = viewHolder.mediaCoverItems[index]
186             val width: Int =
187                 mediaCover.context.resources.getDimensionPixelSize(R.dimen.qs_media_rec_album_width)
188             val height: Int =
189                 mediaCover.context.resources.getDimensionPixelSize(
190                     R.dimen.qs_media_rec_album_height_expanded
191                 )
192             val coverMatrix = Matrix(mediaCover.imageMatrix)
193             coverMatrix.postScale(1.25f, 1.25f, 0.5f * width, 0.5f * height)
194             mediaCover.imageMatrix = coverMatrix
195             mediaCover.setImageDrawable(mediaRecViewModel.albumIcon)
196             mediaCover.contentDescription = mediaRecViewModel.contentDescription
197 
198             val title = viewHolder.mediaTitles[index]
199             title.text = mediaRecViewModel.title
200             title.setTextColor(ColorStateList.valueOf(mediaRecViewModel.titleColor))
201 
202             val subtitle = viewHolder.mediaSubtitles[index]
203             subtitle.text = mediaRecViewModel.subtitle
204             subtitle.setTextColor(ColorStateList.valueOf(mediaRecViewModel.subtitleColor))
205 
206             val progressBar = viewHolder.mediaProgressBars[index]
207             progressBar.progress = mediaRecViewModel.progress
208             progressBar.progressTintList = ColorStateList.valueOf(mediaRecViewModel.progressColor)
209             if (mediaRecViewModel.progress == 0) {
210                 progressBar.visibility = View.GONE
211             }
212         }
213     }
214 
215     private fun openGuts(
216         viewHolder: RecommendationViewHolder,
217         viewModel: MediaRecsCardViewModel,
218         mediaViewController: MediaViewController,
219     ) {
220         viewHolder.marquee(true, MediaViewController.GUTS_ANIMATION_DURATION)
221         mediaViewController.openGuts()
222         viewHolder.recommendations.contentDescription = viewModel.contentDescription.invoke(true)
223         viewModel.onLongClicked.invoke()
224     }
225 
226     private fun closeGuts(
227         viewHolder: RecommendationViewHolder,
228         mediaRecsCardViewModel: MediaRecsCardViewModel,
229         mediaViewController: MediaViewController,
230     ) {
231         viewHolder.marquee(false, MediaViewController.GUTS_ANIMATION_DURATION)
232         mediaViewController.closeGuts(false)
233         viewHolder.recommendations.contentDescription =
234             mediaRecsCardViewModel.contentDescription.invoke(false)
235     }
236 
237     private fun setVisibleAndAlpha(set: ConstraintSet, resId: Int, visible: Boolean) {
238         set.setVisibility(resId, if (visible) ConstraintSet.VISIBLE else ConstraintSet.GONE)
239         set.setAlpha(resId, if (visible) 1.0f else 0.0f)
240     }
241 
242     fun updateRecommendationsVisibility(
243         mediaViewController: MediaViewController,
244         cardView: TransitionLayout,
245     ) {
246         val fittedRecsNum = getNumberOfFittedRecommendations(cardView.context)
247         val expandedSet = mediaViewController.expandedLayout
248         val collapsedSet = mediaViewController.collapsedLayout
249         val mediaCoverContainers = getMediaCoverContainers(cardView)
250         // Hide media cover that cannot fit in the recommendation card.
251         mediaCoverContainers.forEachIndexed { index, container ->
252             setVisibleAndAlpha(expandedSet, container.id, index < fittedRecsNum)
253             setVisibleAndAlpha(collapsedSet, container.id, index < fittedRecsNum)
254         }
255     }
256 
257     private fun getMediaCoverContainers(cardView: TransitionLayout): List<ViewGroup> {
258         return listOf<ViewGroup>(
259             cardView.requireViewById(R.id.media_cover1_container),
260             cardView.requireViewById(R.id.media_cover2_container),
261             cardView.requireViewById(R.id.media_cover3_container),
262         )
263     }
264 
265     private fun getNumberOfFittedRecommendations(context: Context): Int {
266         val res = context.resources
267         val config = res.configuration
268         val defaultDpWidth = res.getInteger(R.integer.default_qs_media_rec_width_dp)
269         val recCoverWidth =
270             (res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) +
271                 res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2)
272 
273         // On landscape, media controls should take half of the screen width.
274         val displayAvailableDpWidth =
275             if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
276                 config.screenWidthDp / 2
277             } else {
278                 config.screenWidthDp
279             }
280         val fittedNum =
281             if (displayAvailableDpWidth > defaultDpWidth) {
282                 val recCoverDefaultWidth =
283                     res.getDimensionPixelSize(R.dimen.qs_media_rec_default_width)
284                 recCoverDefaultWidth / recCoverWidth
285             } else {
286                 val displayAvailableWidth =
287                     TypedValue.applyDimension(
288                             TypedValue.COMPLEX_UNIT_DIP,
289                             displayAvailableDpWidth.toFloat(),
290                             res.displayMetrics
291                         )
292                         .toInt()
293                 displayAvailableWidth / recCoverWidth
294             }
295         return min(fittedNum.toDouble(), NUM_REQUIRED_RECOMMENDATIONS.toDouble()).toInt()
296     }
297 }
298