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

Commit ee67c84b authored by Michael Mikhail's avatar Michael Mikhail
Browse files

Setting up environment for media control view-binder

Adds seek bar view model work, and modify some fields in view-models.

Flag: ACONFIG media_controls_refactor DISABLED
Bug: 328207006
Test: atest SystemUiRoboTests:MediaControlViewModelTest
Test: build.
Change-Id: I62e5caee4958b1f27c5a0337c53aea4f691e4f16
parent 829ab8b5
Loading
Loading
Loading
Loading
+65 −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.widget.ImageButton
import androidx.constraintlayout.widget.ConstraintSet
import com.android.systemui.res.R

object MediaControlViewBinder {

    fun setVisibleAndAlpha(set: ConstraintSet, resId: Int, visible: Boolean) {
        setVisibleAndAlpha(set, resId, visible, ConstraintSet.GONE)
    }

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

    fun updateSeekBarVisibility(constraintSet: ConstraintSet, isSeekBarEnabled: Boolean) {
        if (isSeekBarEnabled) {
            constraintSet.setVisibility(R.id.media_progress_bar, ConstraintSet.VISIBLE)
            constraintSet.setAlpha(R.id.media_progress_bar, 1.0f)
        } else {
            constraintSet.setVisibility(R.id.media_progress_bar, ConstraintSet.INVISIBLE)
            constraintSet.setAlpha(R.id.media_progress_bar, 0.0f)
        }
    }

    fun setSemanticButtonVisibleAndAlpha(
        button: ImageButton,
        expandedSet: ConstraintSet,
        collapsedSet: ConstraintSet,
        visible: Boolean,
        notVisibleValue: Int,
        showInCollapsed: Boolean
    ) {
        if (notVisibleValue == ConstraintSet.INVISIBLE) {
            // Since time views should appear instead of buttons.
            button.isFocusable = visible
            button.isClickable = visible
        }
        setVisibleAndAlpha(expandedSet, button.id, visible, notVisibleValue)
        setVisibleAndAlpha(collapsedSet, button.id, visible = visible && showInCollapsed)
    }
}
+348 −1
Original line number Diff line number Diff line
@@ -16,41 +16,73 @@

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

import android.animation.Animator
import android.animation.AnimatorInflater
import android.animation.AnimatorSet
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.provider.Settings
import android.view.View
import android.view.animation.Interpolator
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT
import com.android.app.animation.Interpolators
import com.android.app.tracing.traceSection
import com.android.systemui.Flags
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.ui.animation.ColorSchemeTransition
import com.android.systemui.media.controls.ui.animation.MetadataAnimationHandler
import com.android.systemui.media.controls.ui.binder.MediaControlViewBinder
import com.android.systemui.media.controls.ui.binder.SeekBarObserver
import com.android.systemui.media.controls.ui.controller.MediaCarouselController.Companion.calculateAlpha
import com.android.systemui.media.controls.ui.view.GutsViewHolder
import com.android.systemui.media.controls.ui.view.MediaHostState
import com.android.systemui.media.controls.ui.view.MediaViewHolder
import com.android.systemui.media.controls.ui.view.RecommendationViewHolder
import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel
import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.surfaceeffects.PaintDrawCallback
import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect
import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView
import com.android.systemui.surfaceeffects.ripple.MultiRippleController
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
import com.android.systemui.util.animation.MeasurementInput
import com.android.systemui.util.animation.MeasurementOutput
import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.animation.TransitionLayoutController
import com.android.systemui.util.animation.TransitionViewState
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.settings.GlobalSettings
import java.lang.Float.max
import java.lang.Float.min
import java.util.Random
import javax.inject.Inject

/**
 * A class responsible for controlling a single instance of a media player handling interactions
 * with the view instance and keeping the media view states up to date.
 */
class MediaViewController
open class MediaViewController
@Inject
constructor(
    private val context: Context,
    private val configurationController: ConfigurationController,
    private val mediaHostStatesManager: MediaHostStatesManager,
    private val logger: MediaViewLogger,
    private val seekBarViewModel: SeekBarViewModel,
    @Main private val mainExecutor: DelayableExecutor,
    private val mediaFlags: MediaFlags,
    private val globalSettings: GlobalSettings,
) {

    /**
@@ -130,6 +162,72 @@ constructor(
            return transitionLayout?.translationY ?: 0.0f
        }

    /** Whether artwork is bound. */
    var isArtworkBound: Boolean = false

    /** previous background artwork */
    var prevArtwork: Drawable? = null

    /** Whether scrubbing time can show */
    var canShowScrubbingTime: Boolean = false

    /** Whether user is touching the seek bar to change the position */
    var isScrubbing: Boolean = false

    var isSeekBarEnabled: Boolean = false

    /** Not visible value for previous button when scrubbing */
    private var prevNotVisibleValue = ConstraintSet.GONE
    private var isPrevButtonAvailable = false

    /** Not visible value for next button when scrubbing */
    private var nextNotVisibleValue = ConstraintSet.GONE
    private var isNextButtonAvailable = false

    private lateinit var mediaViewHolder: MediaViewHolder
    private lateinit var seekBarObserver: SeekBarObserver
    private lateinit var turbulenceNoiseController: TurbulenceNoiseController
    private lateinit var loadingEffect: LoadingEffect
    private lateinit var turbulenceNoiseAnimationConfig: TurbulenceNoiseAnimationConfig
    private lateinit var noiseDrawCallback: PaintDrawCallback
    private lateinit var stateChangedCallback: LoadingEffect.AnimationStateChangedCallback
    internal lateinit var metadataAnimationHandler: MetadataAnimationHandler
    internal lateinit var colorSchemeTransition: ColorSchemeTransition
    internal lateinit var multiRippleController: MultiRippleController

    private val scrubbingChangeListener =
        object : SeekBarViewModel.ScrubbingChangeListener {
            override fun onScrubbingChanged(scrubbing: Boolean) {
                if (!mediaFlags.isMediaControlsRefactorEnabled()) return
                if (isScrubbing == scrubbing) return
                isScrubbing = scrubbing
                updateDisplayForScrubbingChange()
            }
        }

    private val enabledChangeListener =
        object : SeekBarViewModel.EnabledChangeListener {
            override fun onEnabledChanged(enabled: Boolean) {
                if (!mediaFlags.isMediaControlsRefactorEnabled()) return
                if (isSeekBarEnabled == enabled) return
                isSeekBarEnabled = enabled
                MediaControlViewBinder.updateSeekBarVisibility(expandedLayout, isSeekBarEnabled)
            }
        }

    /**
     * Sets the listening state of the player.
     *
     * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
     * unnecessary work when the QS panel is closed.
     *
     * @param listening True when player should be active. Otherwise, false.
     */
    fun setListening(listening: Boolean) {
        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
        seekBarViewModel.listening = listening
    }

    /** A callback for config changes */
    private val configurationListener =
        object : ConfigurationController.ConfigurationListener {
@@ -221,6 +319,14 @@ constructor(
     * Notify this controller that the view has been removed and all listeners should be destroyed
     */
    fun onDestroy() {
        if (mediaFlags.isMediaControlsRefactorEnabled()) {
            if (this::seekBarObserver.isInitialized) {
                seekBarViewModel.progress.removeObserver(seekBarObserver)
            }
            seekBarViewModel.removeScrubbingChangeListener(scrubbingChangeListener)
            seekBarViewModel.removeEnabledChangeListener(enabledChangeListener)
            seekBarViewModel.onDestroy()
        }
        mediaHostStatesManager.removeController(this)
        configurationController.removeCallback(configurationListener)
    }
@@ -535,6 +641,178 @@ constructor(
            )
        }

    fun attachPlayer(mediaViewHolder: MediaViewHolder) {
        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
        this.mediaViewHolder = mediaViewHolder

        // Setting up seek bar.
        seekBarObserver = SeekBarObserver(mediaViewHolder)
        seekBarViewModel.progress.observeForever(seekBarObserver)
        seekBarViewModel.attachTouchHandlers(mediaViewHolder.seekBar)
        seekBarViewModel.setScrubbingChangeListener(scrubbingChangeListener)
        seekBarViewModel.setEnabledChangeListener(enabledChangeListener)

        val mediaCard = mediaViewHolder.player
        attach(mediaViewHolder.player, TYPE.PLAYER)

        val turbulenceNoiseView = mediaViewHolder.turbulenceNoiseView
        turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView)

        multiRippleController = MultiRippleController(mediaViewHolder.multiRippleView)

        // Metadata Animation
        val titleText = mediaViewHolder.titleText
        val artistText = mediaViewHolder.artistText
        val explicitIndicator = mediaViewHolder.explicitIndicator
        val enter =
            loadAnimator(
                mediaCard.context,
                R.anim.media_metadata_enter,
                Interpolators.EMPHASIZED_DECELERATE,
                titleText,
                artistText,
                explicitIndicator
            )
        val exit =
            loadAnimator(
                mediaCard.context,
                R.anim.media_metadata_exit,
                Interpolators.EMPHASIZED_ACCELERATE,
                titleText,
                artistText,
                explicitIndicator
            )
        metadataAnimationHandler = MetadataAnimationHandler(exit, enter)

        colorSchemeTransition =
            ColorSchemeTransition(
                mediaCard.context,
                mediaViewHolder,
                multiRippleController,
                turbulenceNoiseController
            )

        // For Turbulence noise.
        val loadingEffectView = mediaViewHolder.loadingEffectView
        turbulenceNoiseAnimationConfig =
            createTurbulenceNoiseConfig(
                loadingEffectView,
                turbulenceNoiseView,
                colorSchemeTransition
            )
        noiseDrawCallback =
            object : PaintDrawCallback {
                override fun onDraw(paint: Paint) {
                    loadingEffectView.draw(paint)
                }
            }
        stateChangedCallback =
            object : LoadingEffect.AnimationStateChangedCallback {
                override fun onStateChanged(
                    oldState: LoadingEffect.AnimationState,
                    newState: LoadingEffect.AnimationState
                ) {
                    if (newState === LoadingEffect.AnimationState.NOT_PLAYING) {
                        loadingEffectView.visibility = View.INVISIBLE
                    } else {
                        loadingEffectView.visibility = View.VISIBLE
                    }
                }
            }
    }

    fun updateAnimatorDurationScale() {
        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
        if (this::seekBarObserver.isInitialized) {
            seekBarObserver.animationEnabled =
                globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f
        }
    }

    /** update view with the needed UI changes when user touches seekbar. */
    private fun updateDisplayForScrubbingChange() {
        mainExecutor.execute {
            val isTimeVisible = canShowScrubbingTime && isScrubbing
            MediaControlViewBinder.setVisibleAndAlpha(
                expandedLayout,
                mediaViewHolder.scrubbingTotalTimeView.id,
                isTimeVisible
            )
            MediaControlViewBinder.setVisibleAndAlpha(
                expandedLayout,
                mediaViewHolder.scrubbingElapsedTimeView.id,
                isTimeVisible
            )

            MediaControlViewModel.SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.forEach { id ->
                val isButtonVisible: Boolean
                val notVisibleValue: Int
                when (id) {
                    R.id.actionPrev -> {
                        isButtonVisible = isPrevButtonAvailable && !isTimeVisible
                        notVisibleValue = prevNotVisibleValue
                    }
                    R.id.actionNext -> {
                        isButtonVisible = isNextButtonAvailable && !isTimeVisible
                        notVisibleValue = nextNotVisibleValue
                    }
                    else -> {
                        isButtonVisible = !isTimeVisible
                        notVisibleValue = ConstraintSet.GONE
                    }
                }
                MediaControlViewBinder.setSemanticButtonVisibleAndAlpha(
                    mediaViewHolder.getAction(id),
                    expandedLayout,
                    collapsedLayout,
                    isButtonVisible,
                    notVisibleValue,
                    showInCollapsed = true
                )
            }

            if (!metadataAnimationHandler.isRunning) {
                refreshState()
            }
        }
    }

    fun bindSeekBar(onSeek: () -> Unit, onBindSeekBar: (SeekBarViewModel) -> Unit) {
        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
        seekBarViewModel.logSeek = onSeek
        onBindSeekBar.invoke(seekBarViewModel)
    }

    fun setUpTurbulenceNoise() {
        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
        if (Flags.shaderlibLoadingEffectRefactor()) {
            if (!this::loadingEffect.isInitialized) {
                loadingEffect =
                    LoadingEffect(
                        TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
                        turbulenceNoiseAnimationConfig,
                        noiseDrawCallback,
                        stateChangedCallback
                    )
            }
            colorSchemeTransition.loadingEffect = loadingEffect
            loadingEffect.play()
            mainExecutor.executeDelayed(
                loadingEffect::finish,
                MediaControlViewModel.TURBULENCE_NOISE_PLAY_MS_DURATION
            )
        } else {
            turbulenceNoiseController.play(
                TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
                turbulenceNoiseAnimationConfig
            )
            mainExecutor.executeDelayed(
                turbulenceNoiseController::finish,
                MediaControlViewModel.TURBULENCE_NOISE_PLAY_MS_DURATION
            )
        }
    }

    /**
     * Obtain a measurement for a given location. This makes sure that the state is up to date and
     * all widgets know their location. Calling this method may create a measurement if we don't
@@ -790,6 +1068,75 @@ constructor(
                applyImmediately = true
            )
        }

    @VisibleForTesting
    protected open fun loadAnimator(
        context: Context,
        animId: Int,
        motionInterpolator: Interpolator?,
        vararg targets: View?
    ): AnimatorSet {
        val animators = ArrayList<Animator>()
        for (target in targets) {
            val animator = AnimatorInflater.loadAnimator(context, animId) as AnimatorSet
            animator.childAnimations[0].interpolator = motionInterpolator
            animator.setTarget(target)
            animators.add(animator)
        }
        val result = AnimatorSet()
        result.playTogether(animators)
        return result
    }

    private fun createTurbulenceNoiseConfig(
        loadingEffectView: LoadingEffectView,
        turbulenceNoiseView: TurbulenceNoiseView,
        colorSchemeTransition: ColorSchemeTransition
    ): TurbulenceNoiseAnimationConfig {
        val targetView: View =
            if (Flags.shaderlibLoadingEffectRefactor()) {
                loadingEffectView
            } else {
                turbulenceNoiseView
            }
        val width = targetView.width
        val height = targetView.height
        val random = Random()
        return TurbulenceNoiseAnimationConfig(
            gridCount = 2.14f,
            TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER,
            random.nextFloat(),
            random.nextFloat(),
            random.nextFloat(),
            noiseMoveSpeedX = 0.42f,
            noiseMoveSpeedY = 0f,
            TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z,
            // Color will be correctly updated in ColorSchemeTransition.
            colorSchemeTransition.accentPrimary.currentColor,
            screenColor = Color.BLACK,
            width.toFloat(),
            height.toFloat(),
            TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS,
            easeInDuration = 1350f,
            easeOutDuration = 1350f,
            targetView.context.resources.displayMetrics.density,
            lumaMatteBlendFactor = 0.26f,
            lumaMatteOverallBrightness = 0.09f,
            shouldInverseNoiseLuminosity = false
        )
    }

    fun setUpPrevButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) {
        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
        isPrevButtonAvailable = isAvailable
        prevNotVisibleValue = notVisibleValue
    }

    fun setUpNextButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) {
        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
        isNextButtonAvailable = isAvailable
        nextNotVisibleValue = notVisibleValue
    }
}

/** An internal key for the cache of mediaViewStates. This is a subset of the full host state. */
+2 −1
Original line number Diff line number Diff line
@@ -24,7 +24,8 @@ data class MediaActionViewModel(
    val icon: Drawable?,
    val contentDescription: CharSequence?,
    val background: Drawable?,
    val isVisible: Boolean = true,
    /** whether action is visible if user is touching seekbar to change position. */
    val isVisibleWhenScrubbing: Boolean = true,
    val notVisibleValue: Int = ConstraintSet.GONE,
    val showInCollapsed: Boolean,
    val rebindId: Int? = null,
+32 −21

File changed.

Preview size limit exceeded, changes collapsed.

+6 −2
Original line number Diff line number Diff line
@@ -24,13 +24,15 @@ import com.android.systemui.monet.ColorScheme
data class MediaPlayerViewModel(
    val contentDescription: (Boolean) -> CharSequence,
    val backgroundCover: android.graphics.drawable.Icon?,
    val appIcon: Icon,
    val appIcon: android.graphics.drawable.Icon?,
    val launcherIcon: Icon,
    val useGrayColorFilter: Boolean,
    val artistName: CharSequence,
    val titleName: CharSequence,
    val isExplicitVisible: Boolean,
    val shouldAddGradient: Boolean,
    val colorScheme: ColorScheme,
    val isTimeVisible: Boolean,
    val canShowTime: Boolean,
    val playTurbulenceNoise: Boolean,
    val useSemanticActions: Boolean,
    val actionButtons: List<MediaActionViewModel?>,
@@ -38,4 +40,6 @@ data class MediaPlayerViewModel(
    val gutsMenu: GutsViewModel,
    val onClicked: (Expandable) -> Unit,
    val onLongClicked: () -> Unit,
    val onSeek: () -> Unit,
    val onBindSeekbar: (SeekBarViewModel) -> Unit,
)
Loading