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

Commit a57c5752 authored by Anton Potapov's avatar Anton Potapov Committed by Android (Google) Code Review
Browse files

Merge "Add support for Material 3 Slider to SliderHapticPlugin" into main

parents 9441b51f 4c7ed59f
Loading
Loading
Loading
Loading
+11 −5
Original line number Diff line number Diff line
@@ -45,14 +45,14 @@ import org.mockito.junit.MockitoRule
@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class SeekbarHapticPluginTest : SysuiTestCase() {
class HapticSliderPluginTest : SysuiTestCase() {

    private val kosmos = Kosmos()

    @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule()
    @Mock private lateinit var vibratorHelper: VibratorHelper
    private val seekBar = SeekBar(mContext)
    private lateinit var plugin: SeekbarHapticPlugin
    private lateinit var plugin: HapticSliderPlugin

    @Before
    fun setup() {
@@ -95,7 +95,7 @@ class SeekbarHapticPluginTest : SysuiTestCase() {
        // GIVEN an onKeyDown that starts the wait and a program progress change that advances the
        // slider state to ARROW_HANDLE_MOVED_ONCE
        plugin.onKeyDown()
        plugin.onProgressChanged(seekBar, 50, false)
        plugin.onProgressChanged(50, false)
        testScheduler.runCurrent()
        assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)

@@ -112,7 +112,7 @@ class SeekbarHapticPluginTest : SysuiTestCase() {
        // GIVEN an onKeyDown that starts the wait and a program progress change that advances the
        // slider state to ARROW_HANDLE_MOVED_ONCE
        plugin.onKeyDown()
        plugin.onProgressChanged(seekBar, 50, false)
        plugin.onProgressChanged(50, false)
        testScheduler.runCurrent()
        assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)

@@ -142,7 +142,13 @@ class SeekbarHapticPluginTest : SysuiTestCase() {
        }

    private fun createPlugin() {
        plugin = SeekbarHapticPlugin(vibratorHelper, kosmos.msdlPlayer, kosmos.fakeSystemClock)
        plugin =
            HapticSliderPlugin(
                vibratorHelper,
                kosmos.msdlPlayer,
                kosmos.fakeSystemClock,
                HapticSlider.SeekBar(seekBar),
            )
    }

    companion object {
+8 −2
Original line number Diff line number Diff line
@@ -24,7 +24,8 @@ import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.settingslib.RestrictedLockUtils
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.haptics.slider.SeekbarHapticPlugin
import com.android.systemui.haptics.slider.HapticSlider
import com.android.systemui.haptics.slider.HapticSliderPlugin
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.BrightnessMirrorController
@@ -86,7 +87,12 @@ class BrightnessSliderControllerTest : SysuiTestCase() {
                brightnessSliderView,
                mFalsingManager,
                uiEventLogger,
                SeekbarHapticPlugin(vibratorHelper, msdlPlayer, systemClock),
                HapticSliderPlugin(
                    vibratorHelper,
                    msdlPlayer,
                    systemClock,
                    HapticSlider.SeekBar(seekBar),
                ),
                activityStarter,
            )
        mController.init()
+41 −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.haptics.slider

sealed interface HapticSlider {

    val min: Float
    val max: Float

    class SeekBar(val seekBar: android.widget.SeekBar) : HapticSlider {

        override val min: Float
            get() = seekBar.min.toFloat()

        override val max: Float
            get() = seekBar.max.toFloat()
    }

    class Slider(val slider: com.google.android.material.slider.Slider) : HapticSlider {

        override val min: Float
            get() = slider.valueFrom

        override val max: Float
            get() = slider.valueTo
    }
}
+18 −22
Original line number Diff line number Diff line
@@ -18,30 +18,30 @@ package com.android.systemui.haptics.slider

import android.view.MotionEvent
import android.view.VelocityTracker
import android.widget.SeekBar
import androidx.annotation.VisibleForTesting
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.time.SystemClock
import com.google.android.msdl.domain.MSDLPlayer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import com.android.app.tracing.coroutines.launchTraced as launch

/**
 * A plugin added to a manager of a [android.widget.SeekBar] that adds dynamic haptic feedback.
 * A plugin added to a manager of a [HapticSlider] that adds dynamic haptic feedback.
 *
 * A [SliderStateProducer] is used as the producer of slider events, a
 * [SliderHapticFeedbackProvider] is used as the listener of slider states to play haptic feedback
 * depending on the state, and a [SliderStateTracker] is used as the state machine handler that
 * tracks and manipulates the slider state.
 */
class SeekbarHapticPlugin
class HapticSliderPlugin
@JvmOverloads
constructor(
    vibratorHelper: VibratorHelper,
    msdlPlayer: MSDLPlayer,
    systemClock: SystemClock,
    private val slider: HapticSlider,
    sliderHapticFeedbackConfig: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(),
    private val sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
) {
@@ -128,46 +128,42 @@ constructor(
        }
    }

    /** onStartTrackingTouch event from the slider's [android.widget.SeekBar] */
    fun onStartTrackingTouch(seekBar: SeekBar) {
    /** onStartTrackingTouch event from the slider. */
    fun onStartTrackingTouch() {
        if (isTracking) {
            sliderEventProducer.onStartTracking(true)
        }
    }

    /** onProgressChanged event from the slider's [android.widget.SeekBar] */
    fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
    /** onProgressChanged event from the slider's. */
    fun onProgressChanged(progress: Int, fromUser: Boolean) {
        if (isTracking) {
            if (sliderTracker?.currentState == SliderState.IDLE && !fromUser) {
                // This case translates to the slider starting to track program changes
                sliderEventProducer.resetWithProgress(normalizeProgress(seekBar, progress))
                sliderEventProducer.resetWithProgress(normalizeProgress(slider, progress))
                sliderEventProducer.onStartTracking(false)
            } else {
                sliderEventProducer.onProgressChanged(
                    fromUser,
                    normalizeProgress(seekBar, progress),
                )
                sliderEventProducer.onProgressChanged(fromUser, normalizeProgress(slider, progress))
            }
        }
    }

    /**
     * Normalize the integer progress of a SeekBar to the range from 0F to 1F.
     * Normalize the integer progress of a HapticSlider to the range from 0F to 1F.
     *
     * @param[seekBar] The SeekBar that reports a progress.
     * @param[progress] The integer progress of the SeekBar within its min and max values.
     * @param[slider] The HapticSlider that reports a progress.
     * @param[progress] The integer progress of the HapticSlider within its min and max values.
     * @return The progress in the range from 0F to 1F.
     */
    private fun normalizeProgress(seekBar: SeekBar, progress: Int): Float {
        if (seekBar.max == seekBar.min) {
    private fun normalizeProgress(slider: HapticSlider, progress: Int): Float {
        if (slider.max == slider.min) {
            return 1.0f
        }
        val range = seekBar.max - seekBar.min
        return (progress - seekBar.min) / range.toFloat()
        return (progress - slider.min) / (slider.max - slider.min)
    }

    /** onStopTrackingTouch event from the slider's [android.widget.SeekBar] */
    fun onStopTrackingTouch(seekBar: SeekBar) {
    /** onStopTrackingTouch event from the slider. */
    fun onStopTrackingTouch() {
        if (isTracking) {
            sliderEventProducer.onStopTracking(true)
        }
+2 −2
Original line number Diff line number Diff line
@@ -23,11 +23,11 @@ import kotlinx.coroutines.awaitCancellation

object HapticSliderViewBinder {
    /**
     * Binds a [SeekbarHapticPlugin] to a [View]. The binded view should be a
     * Binds a [HapticSliderPlugin] to a [View]. The binded view should be a
     * [android.widget.SeekBar] or a container of a [android.widget.SeekBar]
     */
    @JvmStatic
    fun bind(view: View?, plugin: SeekbarHapticPlugin) {
    fun bind(view: View?, plugin: HapticSliderPlugin) {
        view?.repeatWhenAttached {
            plugin.startInScope(lifecycleScope)
            try {
Loading