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

Commit ec989c70 authored by Anton Potapov's avatar Anton Potapov
Browse files

Hide expand button when there is no active media playing

Flag: aconfig new_volume_panel TRUNKFOOD
Test: manual on the phone with a compatible headset to change the music
while the panel is opened
Fixes: 335113175

Change-Id: I4cf0314e54f1b4d76d8a18c44757e55710970a1d
parent c6a68fea
Loading
Loading
Loading
Loading
+116 −38
Original line number Original line Diff line number Diff line
@@ -20,6 +20,8 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.expandVertically
import androidx.compose.animation.expandVertically
@@ -28,10 +30,8 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.size
@@ -39,6 +39,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment
@@ -49,15 +50,21 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.dp
import com.android.compose.PlatformSliderColors
import com.android.compose.PlatformSliderColors
import com.android.compose.modifiers.padding
import com.android.systemui.res.R
import com.android.systemui.res.R
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel


private const val EXPAND_DURATION_MILLIS = 500
private const val EXPAND_DURATION_MILLIS = 500
private const val COLLAPSE_EXPAND_BUTTON_DELAY_MILLIS = 350
private const val COLLAPSE_DURATION_MILLIS = 300
private const val COLLAPSE_DURATION_MILLIS = 300
private const val EXPAND_BUTTON_ANIMATION_DURATION_MILLIS = 350
private const val TOP_SLIDER_ANIMATION_DURATION_MILLIS = 400
private const val SHRINK_FRACTION = 0.55f
private const val SHRINK_FRACTION = 0.55f
private const val SCALE_FRACTION = 0.9f
private const val SCALE_FRACTION = 0.9f
private const val EXPAND_BUTTON_SCALE = 0.8f


/** Volume sliders laid out in a collapsable column */
/** Volume sliders laid out in a collapsable column */
@OptIn(ExperimentalAnimationApi::class)
@OptIn(ExperimentalAnimationApi::class)
@@ -73,14 +80,15 @@ fun ColumnVolumeSliders(
    require(viewModels.isNotEmpty())
    require(viewModels.isNotEmpty())
    val transition = updateTransition(isExpanded, label = "CollapsableSliders")
    val transition = updateTransition(isExpanded, label = "CollapsableSliders")
    Column(modifier = modifier) {
    Column(modifier = modifier) {
        Row(
        Box(
            modifier = Modifier.fillMaxWidth(),
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
        ) {
        ) {
            val sliderViewModel: SliderViewModel = viewModels.first()
            val sliderViewModel: SliderViewModel = viewModels.first()
            val sliderState by viewModels.first().slider.collectAsState()
            val sliderState by viewModels.first().slider.collectAsState()
            val sliderPadding by topSliderPadding(isExpandable)

            VolumeSlider(
            VolumeSlider(
                modifier = Modifier.weight(1f),
                modifier = Modifier.padding(end = { sliderPadding.roundToPx() }).fillMaxWidth(),
                state = sliderState,
                state = sliderState,
                onValueChange = { newValue: Float ->
                onValueChange = { newValue: Float ->
                    sliderViewModel.onValueChanged(sliderState, newValue)
                    sliderViewModel.onValueChanged(sliderState, newValue)
@@ -90,22 +98,14 @@ fun ColumnVolumeSliders(
                sliderColors = sliderColors,
                sliderColors = sliderColors,
            )
            )


            val expandButtonStateDescription =
                if (isExpanded) stringResource(R.string.volume_panel_expanded_sliders)
                else stringResource(R.string.volume_panel_collapsed_sliders)
            if (isExpandable) {
            ExpandButton(
            ExpandButton(
                    modifier =
                modifier = Modifier.align(Alignment.CenterEnd),
                        Modifier.semantics {
                            role = Role.Switch
                            stateDescription = expandButtonStateDescription
                        },
                isExpanded = isExpanded,
                isExpanded = isExpanded,
                isExpandable = isExpandable,
                onExpandedChanged = onExpandedChanged,
                onExpandedChanged = onExpandedChanged,
                sliderColors = sliderColors,
                sliderColors = sliderColors,
            )
            )
        }
        }
        }
        transition.AnimatedVisibility(
        transition.AnimatedVisibility(
            visible = { it || !isExpandable },
            visible = { it || !isExpandable },
            enter =
            enter =
@@ -147,12 +147,29 @@ fun ColumnVolumeSliders(
@Composable
@Composable
private fun ExpandButton(
private fun ExpandButton(
    isExpanded: Boolean,
    isExpanded: Boolean,
    isExpandable: Boolean,
    onExpandedChanged: (Boolean) -> Unit,
    onExpandedChanged: (Boolean) -> Unit,
    sliderColors: PlatformSliderColors,
    sliderColors: PlatformSliderColors,
    modifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
) {
    val expandButtonStateDescription =
        if (isExpanded) {
            stringResource(R.string.volume_panel_expanded_sliders)
        } else {
            stringResource(R.string.volume_panel_collapsed_sliders)
        }
    AnimatedVisibility(
        modifier = modifier,
        visible = isExpandable,
        enter = expandButtonEnterTransition(),
        exit = expandButtonExitTransition(),
    ) {
    ) {
        IconButton(
        IconButton(
        modifier = modifier.size(64.dp),
            modifier =
                Modifier.size(64.dp).semantics {
                    role = Role.Switch
                    stateDescription = expandButtonStateDescription
                },
            onClick = { onExpandedChanged(!isExpanded) },
            onClick = { onExpandedChanged(!isExpanded) },
            colors =
            colors =
                IconButtonDefaults.filledIconButtonColors(
                IconButtonDefaults.filledIconButtonColors(
@@ -173,6 +190,7 @@ private fun ExpandButton(
            )
            )
        }
        }
    }
    }
}


private fun enterTransition(index: Int, totalCount: Int): EnterTransition {
private fun enterTransition(index: Int, totalCount: Int): EnterTransition {
    val enterDelay = ((totalCount - index + 1) * 10).coerceAtLeast(0)
    val enterDelay = ((totalCount - index + 1) * 10).coerceAtLeast(0)
@@ -204,3 +222,63 @@ private fun exitTransition(index: Int, totalCount: Int): ExitTransition {
        ) +
        ) +
        fadeOut(animationSpec = tween(durationMillis = exitDuration))
        fadeOut(animationSpec = tween(durationMillis = exitDuration))
}
}

private fun expandButtonEnterTransition(): EnterTransition {
    return fadeIn(
        tween(
            delayMillis = COLLAPSE_EXPAND_BUTTON_DELAY_MILLIS,
            durationMillis = EXPAND_BUTTON_ANIMATION_DURATION_MILLIS,
        )
    ) +
        scaleIn(
            animationSpec =
                tween(
                    delayMillis = COLLAPSE_EXPAND_BUTTON_DELAY_MILLIS,
                    durationMillis = EXPAND_BUTTON_ANIMATION_DURATION_MILLIS,
                ),
            initialScale = EXPAND_BUTTON_SCALE,
        )
}

private fun expandButtonExitTransition(): ExitTransition {
    return fadeOut(
        tween(
            delayMillis = EXPAND_DURATION_MILLIS,
            durationMillis = EXPAND_BUTTON_ANIMATION_DURATION_MILLIS,
        )
    ) +
        scaleOut(
            animationSpec =
                tween(
                    delayMillis = EXPAND_DURATION_MILLIS,
                    durationMillis = EXPAND_BUTTON_ANIMATION_DURATION_MILLIS,
                ),
            targetScale = EXPAND_BUTTON_SCALE,
        )
}

@Composable
private fun topSliderPadding(isExpandable: Boolean): State<Dp> {
    val animationSpec: AnimationSpec<Dp> =
        if (isExpandable) {
            tween(
                delayMillis = COLLAPSE_DURATION_MILLIS,
                durationMillis = TOP_SLIDER_ANIMATION_DURATION_MILLIS,
            )
        } else {
            tween(
                delayMillis = EXPAND_DURATION_MILLIS,
                durationMillis = TOP_SLIDER_ANIMATION_DURATION_MILLIS,
            )
        }
    return animateDpAsState(
        targetValue =
            if (isExpandable) {
                72.dp
            } else {
                0.dp
            },
        animationSpec = animationSpec,
        label = "TopVolumeSliderPadding"
    )
}
+11 −2
Original line number Original line Diff line number Diff line
@@ -24,6 +24,7 @@ import androidx.compose.ui.Modifier
import com.android.compose.PlatformSliderDefaults
import com.android.compose.PlatformSliderDefaults
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
import com.android.systemui.volume.panel.component.volume.ui.viewmodel.AudioVolumeComponentViewModel
import com.android.systemui.volume.panel.component.volume.ui.viewmodel.AudioVolumeComponentViewModel
import com.android.systemui.volume.panel.component.volume.ui.viewmodel.SlidersExpandableViewModel
import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent
import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent
import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope
import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope
import com.android.systemui.volume.panel.ui.composable.isPortrait
import com.android.systemui.volume.panel.ui.composable.isPortrait
@@ -48,13 +49,21 @@ constructor(
                modifier = modifier.fillMaxWidth(),
                modifier = modifier.fillMaxWidth(),
            )
            )
        } else {
        } else {
            val isExpanded by viewModel.isExpanded.collectAsState()
            val expandableViewModel: SlidersExpandableViewModel by
                viewModel
                    .isExpandable(isPortrait)
                    .collectAsState(SlidersExpandableViewModel.Unavailable)
            if (expandableViewModel is SlidersExpandableViewModel.Unavailable) {
                return
            }
            val isExpanded =
                (expandableViewModel as? SlidersExpandableViewModel.Expandable)?.isExpanded ?: true
            ColumnVolumeSliders(
            ColumnVolumeSliders(
                viewModels = sliderViewModels,
                viewModels = sliderViewModels,
                isExpanded = isExpanded,
                isExpanded = isExpanded,
                onExpandedChanged = viewModel::onExpandedChanged,
                onExpandedChanged = viewModel::onExpandedChanged,
                sliderColors = PlatformSliderDefaults.defaultPlatformSliderColors(),
                sliderColors = PlatformSliderDefaults.defaultPlatformSliderColors(),
                isExpandable = isPortrait,
                isExpandable = expandableViewModel is SlidersExpandableViewModel.Expandable,
                modifier = modifier.fillMaxWidth(),
                modifier = modifier.fillMaxWidth(),
            )
            )
        }
        }
+40 −19
Original line number Original line Diff line number Diff line
@@ -31,13 +31,15 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch
@@ -53,12 +55,39 @@ class AudioVolumeComponentViewModel
constructor(
constructor(
    @VolumePanelScope private val scope: CoroutineScope,
    @VolumePanelScope private val scope: CoroutineScope,
    mediaOutputInteractor: MediaOutputInteractor,
    mediaOutputInteractor: MediaOutputInteractor,
    private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
    mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
    private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory,
    private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory,
    private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory,
    private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory,
    streamsInteractor: AudioSlidersInteractor,
    streamsInteractor: AudioSlidersInteractor,
) {
) {


    private val mutableIsExpanded = MutableStateFlow<Boolean?>(null)
    private val isPlaybackActive: Flow<Boolean?> =
        mediaOutputInteractor.defaultActiveMediaSession
            .filterData()
            .flatMapLatest { session ->
                if (session == null) {
                    flowOf(false)
                } else {
                    mediaDeviceSessionInteractor.playbackState(session).map { it?.isActive == true }
                }
            }
            .onEach { isPlaybackActive -> mutableIsExpanded.value = !isPlaybackActive }
            .stateIn(scope, SharingStarted.Eagerly, null)
    private val portraitExpandable: Flow<SlidersExpandableViewModel> =
        isPlaybackActive
            .filterNotNull()
            .flatMapLatest { isActive ->
                if (isActive) {
                    mutableIsExpanded.filterNotNull().map { isExpanded ->
                        SlidersExpandableViewModel.Expandable(isExpanded)
                    }
                } else {
                    flowOf(SlidersExpandableViewModel.Fixed)
                }
            }
            .stateIn(scope, SharingStarted.Eagerly, SlidersExpandableViewModel.Unavailable)

    val sliderViewModels: StateFlow<List<SliderViewModel>> =
    val sliderViewModels: StateFlow<List<SliderViewModel>> =
        streamsInteractor.volumePanelSliders
        streamsInteractor.volumePanelSliders
            .transformLatest { sliderTypes ->
            .transformLatest { sliderTypes ->
@@ -76,24 +105,16 @@ constructor(
            }
            }
            .stateIn(scope, SharingStarted.Eagerly, emptyList())
            .stateIn(scope, SharingStarted.Eagerly, emptyList())


    private val mutableIsExpanded = MutableSharedFlow<Boolean>()
    fun isExpandable(isPortrait: Boolean): Flow<SlidersExpandableViewModel> {

        return if (isPortrait) {
    val isExpanded: StateFlow<Boolean> =
            portraitExpandable
        merge(
        } else {
                mutableIsExpanded,
            flowOf(SlidersExpandableViewModel.Fixed)
                mediaOutputInteractor.defaultActiveMediaSession.filterData().flatMapLatest { session
        }
                    ->
                    if (session == null) flowOf(true)
                    else
                        mediaDeviceSessionInteractor.playbackState(session).map {
                            it?.isActive != true
    }
    }
                },
            )
            .stateIn(scope, SharingStarted.Eagerly, false)


    fun onExpandedChanged(isExpanded: Boolean) {
    fun onExpandedChanged(isExpanded: Boolean) {
        scope.launch { mutableIsExpanded.emit(isExpanded) }
        scope.launch { mutableIsExpanded.value = isExpanded }
    }
    }


    private fun CoroutineScope.createSessionViewModel(
    private fun CoroutineScope.createSessionViewModel(
+31 −0
Original line number Original line 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.volume.panel.component.volume.ui.viewmodel

/**
 * Models expandability state of the
 * [com.android.systemui.volume.panel.component.volume.ui.composable.VolumeSlidersComponent].
 */
sealed interface SlidersExpandableViewModel {

    /** [SlidersExpandableViewModel] is not loaded. */
    data object Unavailable : SlidersExpandableViewModel

    data class Expandable(val isExpanded: Boolean) : SlidersExpandableViewModel

    data object Fixed : SlidersExpandableViewModel
}