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

Commit 74b970b3 authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez Committed by Android (Google) Code Review
Browse files

Merge changes Ib409146c,I935370a1 into main

* changes:
  Adding slider haptics to volume sliders in the volume panel.
  Adding support for discrete slider haptics.
parents f0748d67 fb9b9295
Loading
Loading
Loading
Loading
+6 −8
Original line number Diff line number Diff line
@@ -78,9 +78,7 @@ fun ColumnVolumeSliders(
) {
    require(viewModels.isNotEmpty())
    Column(modifier = modifier) {
        Box(
            modifier = Modifier.fillMaxWidth(),
        ) {
        Box(modifier = Modifier.fillMaxWidth()) {
            val sliderViewModel: SliderViewModel = viewModels.first()
            val sliderState by viewModels.first().slider.collectAsStateWithLifecycle()
            val sliderPadding by topSliderPadding(isExpandable)
@@ -94,6 +92,7 @@ fun ColumnVolumeSliders(
                onValueChangeFinished = { sliderViewModel.onValueChangeFinished() },
                onIconTapped = { sliderViewModel.toggleMuted(sliderState) },
                sliderColors = sliderColors,
                hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory,
            )

            ExpandButton(
@@ -143,6 +142,7 @@ fun ColumnVolumeSliders(
                            onValueChangeFinished = { sliderViewModel.onValueChangeFinished() },
                            onIconTapped = { sliderViewModel.toggleMuted(sliderState) },
                            sliderColors = sliderColors,
                            hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory,
                        )
                    }
                }
@@ -181,7 +181,7 @@ private fun ExpandButton(
            colors =
                IconButtonDefaults.filledIconButtonColors(
                    containerColor = sliderColors.indicatorColor,
                    contentColor = sliderColors.iconColor
                    contentColor = sliderColors.iconColor,
                ),
        ) {
            Icon(
@@ -211,9 +211,7 @@ private fun enterTransition(index: Int, totalCount: Int): EnterTransition {
            animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay),
            clip = false,
        ) +
        fadeIn(
            animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay),
        )
        fadeIn(animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay))
}

private fun exitTransition(index: Int, totalCount: Int): ExitTransition {
@@ -286,6 +284,6 @@ private fun topSliderPadding(isExpandable: Boolean): State<Dp> {
                0.dp
            },
        animationSpec = animationSpec,
        label = "TopVolumeSliderPadding"
        label = "TopVolumeSliderPadding",
    )
}
+1 −0
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ fun GridVolumeSliders(
                onValueChangeFinished = { sliderViewModel.onValueChangeFinished() },
                onIconTapped = { sliderViewModel.toggleMuted(sliderState) },
                sliderColors = sliderColors,
                hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory,
            )
        }
    }
+53 −11
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
@@ -46,9 +48,14 @@ import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.dp
import com.android.compose.PlatformSlider
import com.android.compose.PlatformSliderColors
import com.android.systemui.Flags
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig
import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState

@Composable
@@ -59,8 +66,40 @@ fun VolumeSlider(
    onIconTapped: () -> Unit,
    modifier: Modifier = Modifier,
    sliderColors: PlatformSliderColors,
    hapticsViewModelFactory: SliderHapticsViewModel.Factory,
) {
    val value by valueState(state)
    val interactionSource = remember { MutableInteractionSource() }
    val sliderStepSize = 1f / (state.valueRange.endInclusive - state.valueRange.start)
    val hapticsViewModel: SliderHapticsViewModel? =
        if (Flags.hapticsForComposeSliders()) {
            rememberViewModel(traceName = "SliderHapticsViewModel") {
                hapticsViewModelFactory.create(
                    interactionSource,
                    state.valueRange,
                    Orientation.Horizontal,
                    SliderHapticFeedbackConfig(
                        lowerBookendScale = 0.2f,
                        progressBasedDragMinScale = 0.2f,
                        progressBasedDragMaxScale = 0.5f,
                        deltaProgressForDragThreshold = 0f,
                        additionalVelocityMaxBump = 0.2f,
                        maxVelocityToScale = 0.1f, /* slider progress(from 0 to 1) per sec */
                        sliderStepSize = sliderStepSize,
                    ),
                    SeekableSliderTrackerConfig(
                        lowerBookendThreshold = 0f,
                        upperBookendThreshold = 1f,
                    ),
                )
            }
        } else {
            null
        }

    // Perform haptics due to UI composition
    hapticsViewModel?.onValueChange(value)

    PlatformSlider(
        modifier =
            modifier.sysuiResTag(state.label).clearAndSetSemantics {
@@ -94,7 +133,7 @@ fun VolumeSlider(
                    val newValue =
                        (value + targetDirection * state.a11yStep).coerceIn(
                            state.valueRange.start,
                            state.valueRange.endInclusive
                            state.valueRange.endInclusive,
                        )
                    onValueChange(newValue)
                    true
@@ -102,16 +141,18 @@ fun VolumeSlider(
            },
        value = value,
        valueRange = state.valueRange,
        onValueChange = onValueChange,
        onValueChangeFinished = onValueChangeFinished,
        onValueChange = { newValue ->
            hapticsViewModel?.addVelocityDataPoint(newValue)
            onValueChange(newValue)
        },
        onValueChangeFinished = {
            hapticsViewModel?.onValueChangeEnded()
            onValueChangeFinished?.invoke()
        },
        enabled = state.isEnabled,
        icon = {
            state.icon?.let {
                SliderIcon(
                    icon = it,
                    onIconTapped = onIconTapped,
                    isTappable = state.isMutable,
                )
                SliderIcon(icon = it, onIconTapped = onIconTapped, isTappable = state.isMutable)
            }
        },
        colors = sliderColors,
@@ -128,7 +169,8 @@ fun VolumeSlider(
                    disabledMessage = state.disabledMessage,
                )
            }
        }
        },
        interactionSource = interactionSource,
    )
}

@@ -150,14 +192,14 @@ private fun SliderIcon(
    icon: Icon,
    onIconTapped: () -> Unit,
    isTappable: Boolean,
    modifier: Modifier = Modifier
    modifier: Modifier = Modifier,
) {
    val boxModifier =
        if (isTappable) {
                modifier.clickable(
                    onClick = onIconTapped,
                    interactionSource = null,
                    indication = null
                    indication = null,
                )
            } else {
                modifier
+67 −1
Original line number Diff line number Diff line
@@ -47,7 +47,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private val config = SliderHapticFeedbackConfig()
    private var config = SliderHapticFeedbackConfig()

    private val dragVelocityProvider = SliderDragVelocityProvider { config.maxVelocityToScale }

@@ -226,6 +226,72 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
            assertEquals(/* expected= */ 1, vibratorHelper.timesVibratedWithEffect(ticks.compose()))
        }

    @Test
    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun playHapticAtProgress_forDiscreteSlider_playsTick() =
        with(kosmos) {
            config = SliderHapticFeedbackConfig(sliderStepSize = 0.2f)
            sliderHapticFeedbackProvider =
                SliderHapticFeedbackProvider(
                    vibratorHelper,
                    msdlPlayer,
                    dragVelocityProvider,
                    config,
                    kosmos.fakeSystemClock,
                )

            // GIVEN max velocity and slider progress
            val progress = 1f
            val expectedScale =
                sliderHapticFeedbackProvider.scaleOnDragTexture(config.maxVelocityToScale, progress)
            val tick =
                VibrationEffect.startComposition()
                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, expectedScale)
                    .compose()

            // GIVEN system running for 1s
            fakeSystemClock.advanceTime(1000)

            // WHEN called to play haptics
            sliderHapticFeedbackProvider.onProgress(progress)

            // THEN the correct composition only plays once
            assertEquals(expected = 1, vibratorHelper.timesVibratedWithEffect(tick))
        }

    @Test
    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun playHapticAtProgress_forDiscreteSlider_playsDiscreteSliderToken() =
        with(kosmos) {
            config = SliderHapticFeedbackConfig(sliderStepSize = 0.2f)
            sliderHapticFeedbackProvider =
                SliderHapticFeedbackProvider(
                    vibratorHelper,
                    msdlPlayer,
                    dragVelocityProvider,
                    config,
                    kosmos.fakeSystemClock,
                )

            // GIVEN max velocity and slider progress
            val progress = 1f
            val expectedScale =
                sliderHapticFeedbackProvider.scaleOnDragTexture(config.maxVelocityToScale, progress)
            val expectedProperties =
                InteractionProperties.DynamicVibrationScale(expectedScale, pipeliningAttributes)

            // GIVEN system running for 1s
            fakeSystemClock.advanceTime(1000)

            // WHEN called to play haptics
            sliderHapticFeedbackProvider.onProgress(progress)

            // THEN the correct token plays once
            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR_DISCRETE)
            assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(expectedProperties)
            assertThat(msdlPlayer.getHistory().size).isEqualTo(1)
        }

    @Test
    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun playHapticAtProgress_onQuickSuccession_playsContinuousDragTokenOnce() =
+2 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
@@ -73,6 +74,7 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() {
            kosmos.zenModeInteractor,
            kosmos.uiEventLogger,
            kosmos.volumePanelLogger,
            kosmos.sliderHapticsViewModelFactory,
        )
    }

Loading