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