Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 68c98a6e authored by Michael Mikhail's avatar Michael Mikhail Committed by Android (Google) Code Review
Browse files

Merge "Add media recommendations view binder" into main

parents 1eb86347 a0311ef3
Loading
Loading
Loading
Loading
+297 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.media.controls.ui.binder

import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Matrix
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.animation.Expandable
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.media.controls.shared.model.NUM_REQUIRED_RECOMMENDATIONS
import com.android.systemui.media.controls.ui.controller.MediaViewController
import com.android.systemui.media.controls.ui.view.RecommendationViewHolder
import com.android.systemui.media.controls.ui.viewmodel.MediaRecViewModel
import com.android.systemui.media.controls.ui.viewmodel.MediaRecommendationsViewModel
import com.android.systemui.media.controls.ui.viewmodel.MediaRecsCardViewModel
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.util.animation.TransitionLayout
import kotlin.math.min
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

object MediaRecommendationsViewBinder {

    /** Binds recommendations view holder to the given view-model */
    fun bind(
        viewHolder: RecommendationViewHolder,
        viewModel: MediaRecommendationsViewModel,
        mediaViewController: MediaViewController,
        falsingManager: FalsingManager,
    ) {
        mediaViewController.recsConfigurationChangeListener = this::updateRecommendationsVisibility
        val cardView = viewHolder.recommendations
        cardView.repeatWhenAttached {
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    launch {
                        viewModel.mediaRecsCard.collectLatest { viewModel ->
                            viewModel?.let {
                                bindRecsCard(viewHolder, it, mediaViewController, falsingManager)
                            }
                        }
                    }
                }
            }
        }
    }

    private fun bindRecsCard(
        viewHolder: RecommendationViewHolder,
        viewModel: MediaRecsCardViewModel,
        mediaViewController: MediaViewController,
        falsingManager: FalsingManager,
    ) {
        // Bind main card.
        viewHolder.cardTitle.setTextColor(viewModel.cardTitleColor)
        viewHolder.recommendations.backgroundTintList = ColorStateList.valueOf(viewModel.cardColor)
        viewHolder.recommendations.contentDescription =
            viewModel.contentDescription.invoke(mediaViewController.isGutsVisible)

        viewHolder.recommendations.setOnClickListener {
            if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@setOnClickListener
            viewModel.onClicked(Expandable.fromView(it))
        }

        viewHolder.recommendations.setOnLongClickListener {
            if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY))
                return@setOnLongClickListener true
            if (!mediaViewController.isGutsVisible) {
                openGuts(viewHolder, viewModel, mediaViewController)
            } else {
                closeGuts(viewHolder, viewModel, mediaViewController)
            }
            return@setOnLongClickListener true
        }

        // Bind all recommendations.
        bindRecommendationsList(viewHolder, viewModel.mediaRecs, falsingManager)
        updateRecommendationsVisibility(mediaViewController, viewHolder.recommendations)

        // Set visibility of recommendations.
        val expandedSet: ConstraintSet = mediaViewController.expandedLayout
        val collapsedSet: ConstraintSet = mediaViewController.collapsedLayout
        viewHolder.mediaTitles.forEach {
            setVisibleAndAlpha(expandedSet, it.id, viewModel.areTitlesVisible)
            setVisibleAndAlpha(collapsedSet, it.id, viewModel.areTitlesVisible)
        }
        viewHolder.mediaSubtitles.forEach {
            setVisibleAndAlpha(expandedSet, it.id, viewModel.areSubtitlesVisible)
            setVisibleAndAlpha(collapsedSet, it.id, viewModel.areSubtitlesVisible)
        }

        bindRecommendationsGuts(viewHolder, viewModel, mediaViewController, falsingManager)

        mediaViewController.refreshState()
    }

    private fun bindRecommendationsGuts(
        viewHolder: RecommendationViewHolder,
        viewModel: MediaRecsCardViewModel,
        mediaViewController: MediaViewController,
        falsingManager: FalsingManager,
    ) {
        val gutsViewHolder = viewHolder.gutsViewHolder
        val gutsViewModel = viewModel.gutsMenu

        gutsViewHolder.gutsText.text = gutsViewModel.gutsText
        gutsViewHolder.dismissText.visibility = View.VISIBLE
        gutsViewHolder.dismiss.isEnabled = true
        gutsViewHolder.dismiss.setOnClickListener {
            if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@setOnClickListener
            closeGuts(viewHolder, viewModel, mediaViewController)
            gutsViewModel.onDismissClicked.invoke()
        }

        gutsViewHolder.cancelText.background = gutsViewModel.cancelTextBackground
        gutsViewHolder.cancel.setOnClickListener {
            if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
                closeGuts(viewHolder, viewModel, mediaViewController)
            }
        }

        gutsViewHolder.settings.setOnClickListener {
            if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
                gutsViewModel.onSettingsClicked.invoke()
            }
        }

        gutsViewHolder.setDismissible(gutsViewModel.isDismissEnabled)
        gutsViewHolder.setTextPrimaryColor(gutsViewModel.textPrimaryColor)
        gutsViewHolder.setAccentPrimaryColor(gutsViewModel.accentPrimaryColor)
        gutsViewHolder.setSurfaceColor(gutsViewModel.surfaceColor)
    }

    private fun bindRecommendationsList(
        viewHolder: RecommendationViewHolder,
        mediaRecs: List<MediaRecViewModel>,
        falsingManager: FalsingManager
    ) {
        mediaRecs.forEachIndexed { index, mediaRecViewModel ->
            if (index >= NUM_REQUIRED_RECOMMENDATIONS) return@forEachIndexed

            val appIconView = viewHolder.mediaAppIcons[index]
            appIconView.clearColorFilter()
            if (mediaRecViewModel.appIcon != null) {
                appIconView.setImageDrawable(mediaRecViewModel.appIcon)
            } else {
                appIconView.setImageResource(R.drawable.ic_music_note)
            }

            val mediaCoverContainer = viewHolder.mediaCoverContainers[index]
            mediaCoverContainer.setOnClickListener {
                if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@setOnClickListener
                mediaRecViewModel.onClicked.invoke(Expandable.fromView(it), index)
            }
            mediaCoverContainer.setOnLongClickListener {
                if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY))
                    return@setOnLongClickListener true
                (it.parent as View).performLongClick()
                return@setOnLongClickListener true
            }

            val mediaCover = viewHolder.mediaCoverItems[index]
            val width: Int =
                mediaCover.context.resources.getDimensionPixelSize(R.dimen.qs_media_rec_album_width)
            val height: Int =
                mediaCover.context.resources.getDimensionPixelSize(
                    R.dimen.qs_media_rec_album_height_expanded
                )
            val coverMatrix = Matrix(mediaCover.imageMatrix)
            coverMatrix.postScale(1.25f, 1.25f, 0.5f * width, 0.5f * height)
            mediaCover.imageMatrix = coverMatrix
            mediaCover.setImageDrawable(mediaRecViewModel.albumIcon)
            mediaCover.contentDescription = mediaRecViewModel.contentDescription

            val title = viewHolder.mediaTitles[index]
            title.text = mediaRecViewModel.title
            title.setTextColor(ColorStateList.valueOf(mediaRecViewModel.titleColor))

            val subtitle = viewHolder.mediaSubtitles[index]
            subtitle.text = mediaRecViewModel.subtitle
            subtitle.setTextColor(ColorStateList.valueOf(mediaRecViewModel.subtitleColor))

            val progressBar = viewHolder.mediaProgressBars[index]
            progressBar.progress = mediaRecViewModel.progress
            progressBar.progressTintList = ColorStateList.valueOf(mediaRecViewModel.progressColor)
            if (mediaRecViewModel.progress == 0) {
                progressBar.visibility = View.GONE
            }
        }
    }

    private fun openGuts(
        viewHolder: RecommendationViewHolder,
        viewModel: MediaRecsCardViewModel,
        mediaViewController: MediaViewController,
    ) {
        viewHolder.marquee(true, MediaViewController.GUTS_ANIMATION_DURATION)
        mediaViewController.openGuts()
        viewHolder.recommendations.contentDescription = viewModel.contentDescription.invoke(true)
        viewModel.onLongClicked.invoke()
    }

    private fun closeGuts(
        viewHolder: RecommendationViewHolder,
        mediaRecsCardViewModel: MediaRecsCardViewModel,
        mediaViewController: MediaViewController,
    ) {
        viewHolder.marquee(false, MediaViewController.GUTS_ANIMATION_DURATION)
        mediaViewController.closeGuts(false)
        viewHolder.recommendations.contentDescription =
            mediaRecsCardViewModel.contentDescription.invoke(false)
    }

    private fun setVisibleAndAlpha(set: ConstraintSet, resId: Int, visible: Boolean) {
        set.setVisibility(resId, if (visible) ConstraintSet.VISIBLE else ConstraintSet.GONE)
        set.setAlpha(resId, if (visible) 1.0f else 0.0f)
    }

    private fun updateRecommendationsVisibility(
        mediaViewController: MediaViewController,
        cardView: TransitionLayout,
    ) {
        val fittedRecsNum = getNumberOfFittedRecommendations(cardView.context)
        val expandedSet = mediaViewController.expandedLayout
        val collapsedSet = mediaViewController.collapsedLayout
        val mediaCoverContainers = getMediaCoverContainers(cardView)
        // Hide media cover that cannot fit in the recommendation card.
        mediaCoverContainers.forEachIndexed { index, container ->
            setVisibleAndAlpha(expandedSet, container.id, index < fittedRecsNum)
            setVisibleAndAlpha(collapsedSet, container.id, index < fittedRecsNum)
        }
    }

    private fun getMediaCoverContainers(cardView: TransitionLayout): List<ViewGroup> {
        return listOf<ViewGroup>(
            cardView.requireViewById(R.id.media_cover1_container),
            cardView.requireViewById(R.id.media_cover2_container),
            cardView.requireViewById(R.id.media_cover3_container),
        )
    }

    private fun getNumberOfFittedRecommendations(context: Context): Int {
        val res = context.resources
        val config = res.configuration
        val defaultDpWidth = res.getInteger(R.integer.default_qs_media_rec_width_dp)
        val recCoverWidth =
            (res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) +
                res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2)

        // On landscape, media controls should take half of the screen width.
        val displayAvailableDpWidth =
            if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
                config.screenWidthDp / 2
            } else {
                config.screenWidthDp
            }
        val fittedNum =
            if (displayAvailableDpWidth > defaultDpWidth) {
                val recCoverDefaultWidth =
                    res.getDimensionPixelSize(R.dimen.qs_media_rec_default_width)
                recCoverDefaultWidth / recCoverWidth
            } else {
                val displayAvailableWidth =
                    TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP,
                            displayAvailableDpWidth.toFloat(),
                            res.displayMetrics
                        )
                        .toInt()
                displayAvailableWidth / recCoverWidth
            }
        return min(fittedNum.toDouble(), NUM_REQUIRED_RECOMMENDATIONS.toDouble()).toInt()
    }
}
+12 −1
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ constructor(
    /** A listener when the current dimensions of the player change */
    lateinit var sizeChangedListener: () -> Unit
    lateinit var configurationChangeListener: () -> Unit
    lateinit var recsConfigurationChangeListener: (MediaViewController, TransitionLayout) -> Unit
    private var firstRefresh: Boolean = true
    @VisibleForTesting private var transitionLayout: TransitionLayout? = null
    private val layoutController = TransitionLayoutController()
@@ -160,7 +161,17 @@ constructor(
                            )
                        )
                    }
                    if (this@MediaViewController::configurationChangeListener.isInitialized) {
                    if (mediaFlags.isMediaControlsRefactorEnabled()) {
                        if (
                            this@MediaViewController::recsConfigurationChangeListener.isInitialized
                        ) {
                            transitionLayout?.let {
                                recsConfigurationChangeListener.invoke(this@MediaViewController, it)
                            }
                        }
                    } else if (
                        this@MediaViewController::configurationChangeListener.isInitialized
                    ) {
                        configurationChangeListener.invoke()
                        refreshState()
                    }
+3 −3
Original line number Diff line number Diff line
@@ -22,9 +22,9 @@ import android.graphics.drawable.Drawable
/** Models UI state for media guts menu */
data class GutsViewModel(
    val gutsText: CharSequence,
    @ColorInt val textColor: Int,
    @ColorInt val buttonBackgroundColor: Int,
    @ColorInt val buttonTextColor: Int,
    @ColorInt val textPrimaryColor: Int,
    @ColorInt val accentPrimaryColor: Int,
    @ColorInt val surfaceColor: Int,
    val isDismissEnabled: Boolean = true,
    val onDismissClicked: () -> Unit,
    val cancelTextBackground: Drawable?,
+3 −3
Original line number Diff line number Diff line
@@ -235,9 +235,9 @@ class MediaControlViewModel(
                } else {
                    applicationContext.getString(R.string.controls_media_active_session)
                },
            textColor = textPrimaryFromScheme(scheme),
            buttonBackgroundColor = accentPrimaryFromScheme(scheme),
            buttonTextColor = surfaceFromScheme(scheme),
            textPrimaryColor = textPrimaryFromScheme(scheme),
            accentPrimaryColor = accentPrimaryFromScheme(scheme),
            surfaceColor = surfaceFromScheme(scheme),
            isDismissEnabled = model.isDismissible,
            onDismissClicked = {
                onDismissMediaData(model.token, model.uid, model.packageName, model.instanceId)
+3 −3
Original line number Diff line number Diff line
@@ -213,9 +213,9 @@ constructor(
        return GutsViewModel(
            gutsText =
                applicationContext.getString(R.string.controls_media_close_session, model.appName),
            textColor = textPrimaryFromScheme(scheme),
            buttonBackgroundColor = accentPrimaryFromScheme(scheme),
            buttonTextColor = surfaceFromScheme(scheme),
            textPrimaryColor = textPrimaryFromScheme(scheme),
            accentPrimaryColor = accentPrimaryFromScheme(scheme),
            surfaceColor = surfaceFromScheme(scheme),
            onDismissClicked = {
                onMediaRecommendationsDismissed(
                    model.key,