Loading packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekbarHapticPluginTest.kt→packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/HapticSliderPluginTest.kt +11 −5 Original line number Diff line number Diff line Loading @@ -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() { Loading Loading @@ -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) Loading @@ -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) Loading Loading @@ -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 { Loading packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt +8 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -86,7 +87,12 @@ class BrightnessSliderControllerTest : SysuiTestCase() { brightnessSliderView, mFalsingManager, uiEventLogger, SeekbarHapticPlugin(vibratorHelper, msdlPlayer, systemClock), HapticSliderPlugin( vibratorHelper, msdlPlayer, systemClock, HapticSlider.SeekBar(seekBar), ), activityStarter, ) mController.init() Loading packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSlider.kt 0 → 100644 +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 } } packages/SystemUI/src/com/android/systemui/haptics/slider/SeekbarHapticPlugin.kt→packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderPlugin.kt +18 −22 Original line number Diff line number Diff line Loading @@ -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(), ) { Loading Loading @@ -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) } Loading packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt +2 −2 Original line number Diff line number Diff line Loading @@ -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 Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekbarHapticPluginTest.kt→packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/HapticSliderPluginTest.kt +11 −5 Original line number Diff line number Diff line Loading @@ -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() { Loading Loading @@ -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) Loading @@ -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) Loading Loading @@ -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 { Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt +8 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -86,7 +87,12 @@ class BrightnessSliderControllerTest : SysuiTestCase() { brightnessSliderView, mFalsingManager, uiEventLogger, SeekbarHapticPlugin(vibratorHelper, msdlPlayer, systemClock), HapticSliderPlugin( vibratorHelper, msdlPlayer, systemClock, HapticSlider.SeekBar(seekBar), ), activityStarter, ) mController.init() Loading
packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSlider.kt 0 → 100644 +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 } }
packages/SystemUI/src/com/android/systemui/haptics/slider/SeekbarHapticPlugin.kt→packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderPlugin.kt +18 −22 Original line number Diff line number Diff line Loading @@ -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(), ) { Loading Loading @@ -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) } Loading
packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt +2 −2 Original line number Diff line number Diff line Loading @@ -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