summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt57
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/slider/SliderState.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt188
4 files changed, 251 insertions, 4 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
index d89cf63cd483..10098faaa05e 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
@@ -58,7 +58,7 @@ class SeekableSliderTracker(
override suspend fun iterateState(event: SliderEvent) {
when (currentState) {
- SliderState.IDLE -> handleIdle(event.type)
+ SliderState.IDLE -> handleIdle(event.type, event.currentProgress)
SliderState.WAIT -> handleWait(event.type, event.currentProgress)
SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH -> handleAcquired(event.type)
SliderState.DRAG_HANDLE_DRAGGING -> handleDragging(event.type, event.currentProgress)
@@ -67,17 +67,26 @@ class SeekableSliderTracker(
SliderState.DRAG_HANDLE_RELEASED_FROM_TOUCH -> setState(SliderState.IDLE)
SliderState.JUMP_TRACK_LOCATION_SELECTED -> handleJumpToTrack(event.type)
SliderState.JUMP_BOOKEND_SELECTED -> handleJumpToBookend(event.type)
+ SliderState.ARROW_HANDLE_MOVED_ONCE -> handleArrowOnce(event.type)
+ SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY ->
+ handleArrowContinuous(event.type, event.currentProgress)
+ SliderState.ARROW_HANDLE_REACHED_BOOKEND -> handleArrowBookend()
}
latestProgress = event.currentProgress
}
- private fun handleIdle(newEventType: SliderEventType) {
+ private fun handleIdle(newEventType: SliderEventType, currentProgress: Float) {
if (newEventType == SliderEventType.STARTED_TRACKING_TOUCH) {
timerJob = launchTimer()
// The WAIT state will wait for the timer to complete or a slider progress to occur.
// This will disambiguate between an imprecise touch that acquires the slider handle,
// and a select and jump operation in the slider track.
setState(SliderState.WAIT)
+ } else if (newEventType == SliderEventType.PROGRESS_CHANGE_BY_PROGRAM) {
+ val state =
+ if (bookendReached(currentProgress)) SliderState.ARROW_HANDLE_REACHED_BOOKEND
+ else SliderState.ARROW_HANDLE_MOVED_ONCE
+ setState(state)
}
}
@@ -176,6 +185,13 @@ class SeekableSliderTracker(
SliderState.DRAG_HANDLE_REACHED_BOOKEND -> executeOnBookend()
SliderState.JUMP_TRACK_LOCATION_SELECTED ->
sliderListener.onProgressJump(latestProgress)
+ SliderState.ARROW_HANDLE_MOVED_ONCE -> sliderListener.onSelectAndArrow(latestProgress)
+ SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY -> sliderListener.onProgress(latestProgress)
+ SliderState.ARROW_HANDLE_REACHED_BOOKEND -> {
+ executeOnBookend()
+ // This transitory execution must also reset the state
+ resetState()
+ }
else -> {}
}
}
@@ -204,6 +220,43 @@ class SeekableSliderTracker(
currentProgress <= config.lowerBookendThreshold
}
+ private fun handleArrowOnce(newEventType: SliderEventType) {
+ val nextState =
+ when (newEventType) {
+ SliderEventType.STARTED_TRACKING_TOUCH -> {
+ // Launching the timer and going to WAIT
+ timerJob = launchTimer()
+ SliderState.WAIT
+ }
+ SliderEventType.PROGRESS_CHANGE_BY_PROGRAM ->
+ SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY
+ SliderEventType.ARROW_UP -> SliderState.IDLE
+ else -> SliderState.ARROW_HANDLE_MOVED_ONCE
+ }
+ setState(nextState)
+ }
+
+ private fun handleArrowContinuous(newEventType: SliderEventType, currentProgress: Float) {
+ val reachedBookend = bookendReached(currentProgress)
+ val nextState =
+ when (newEventType) {
+ SliderEventType.ARROW_UP -> SliderState.IDLE
+ SliderEventType.STARTED_TRACKING_TOUCH -> {
+ // Launching the timer and going to WAIT
+ timerJob = launchTimer()
+ SliderState.WAIT
+ }
+ SliderEventType.PROGRESS_CHANGE_BY_PROGRAM -> {
+ if (reachedBookend) SliderState.ARROW_HANDLE_REACHED_BOOKEND
+ else SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY
+ }
+ else -> SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY
+ }
+ setState(nextState)
+ }
+
+ private fun handleArrowBookend() = setState(SliderState.IDLE)
+
@VisibleForTesting
fun setState(state: SliderState) {
currentState = state
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderState.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderState.kt
index fe092e67036b..de6ddd7168e5 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderState.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderState.kt
@@ -32,6 +32,12 @@ enum class SliderState {
DRAG_HANDLE_REACHED_BOOKEND,
/* A location in the slider track has been selected. */
JUMP_TRACK_LOCATION_SELECTED,
- /* The slider handled moved to a bookend after it was selected. */
+ /* The slider handle moved to a bookend after it was selected. */
JUMP_BOOKEND_SELECTED,
+ /** The slider handle moved due to single select-and-arrow operation */
+ ARROW_HANDLE_MOVED_ONCE,
+ /** The slider handle moves continuously due to constant select-and-arrow operations */
+ ARROW_HANDLE_MOVES_CONTINUOUSLY,
+ /** The slider handle reached a bookend due to a select-and-arrow operation */
+ ARROW_HANDLE_REACHED_BOOKEND,
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
index bc5090f14d23..be1fa2bcadf9 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
@@ -227,7 +227,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
mListener.onChanged(mTracking, progress, false);
SeekableSliderEventProducer eventProducer =
mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer();
- if (eventProducer != null) {
+ if (eventProducer != null && fromUser) {
eventProducer.onProgressChanged(seekBar, progress, fromUser);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
index 8d12e491ad11..db0496227a38 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
@@ -528,6 +528,194 @@ class SeekableSliderTrackerTest : SysuiTestCase() {
verifyNoMoreInteractions(sliderStateListener)
}
+ @Test
+ fun onProgressChangeByProgram_atTheMiddle_onIdle_movesToArrowHandleMovedOnce() = runTest {
+ // GIVEN an initialized tracker in the IDLE state
+ initTracker(testScheduler)
+
+ // GIVEN a progress due to an external source that lands at the middle of the slider
+ val progress = 0.5f
+ sliderEventProducer.sendEvent(
+ SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+ )
+
+ // THEN the state moves to ARROW_HANDLE_MOVED_ONCE and the listener is called to play
+ // haptics
+ assertThat(mSeekableSliderTracker.currentState)
+ .isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)
+ verify(sliderStateListener).onSelectAndArrow(progress)
+ }
+
+ @Test
+ fun onProgressChangeByProgram_atUpperBookend_onIdle_movesToIdle() = runTest {
+ // GIVEN an initialized tracker in the IDLE state
+ val config = SeekableSliderTrackerConfig()
+ initTracker(testScheduler, config)
+
+ // GIVEN a progress due to an external source that lands at the upper bookend
+ val progress = config.upperBookendThreshold + 0.01f
+ sliderEventProducer.sendEvent(
+ SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+ )
+
+ // THEN the tracker executes upper bookend haptics before moving back to IDLE
+ verify(sliderStateListener).onUpperBookend()
+ assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ }
+
+ @Test
+ fun onProgressChangeByProgram_atLowerBookend_onIdle_movesToIdle() = runTest {
+ // GIVEN an initialized tracker in the IDLE state
+ val config = SeekableSliderTrackerConfig()
+ initTracker(testScheduler, config)
+
+ // WHEN a progress is recorded due to an external source that lands at the lower bookend
+ val progress = config.lowerBookendThreshold - 0.01f
+ sliderEventProducer.sendEvent(
+ SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+ )
+
+ // THEN the tracker executes lower bookend haptics before moving to IDLE
+ verify(sliderStateListener).onLowerBookend()
+ assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ }
+
+ @Test
+ fun onArrowUp_onArrowMovedOnce_movesToIdle() = runTest {
+ // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
+ initTracker(testScheduler)
+ mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
+
+ // WHEN the external stimulus is released
+ val progress = 0.5f
+ sliderEventProducer.sendEvent(SliderEvent(SliderEventType.ARROW_UP, progress))
+
+ // THEN the tracker moves back to IDLE and there are no haptics
+ assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ verifyZeroInteractions(sliderStateListener)
+ }
+
+ @Test
+ fun onStartTrackingTouch_onArrowMovedOnce_movesToWait() = runTest {
+ // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
+ initTracker(testScheduler)
+ mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
+
+ // WHEN the slider starts tracking touch
+ val progress = 0.5f
+ sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress))
+
+ // THEN the tracker moves back to WAIT and starts the waiting job. Also, there are no
+ // haptics
+ assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.WAIT)
+ assertThat(mSeekableSliderTracker.isWaiting).isTrue()
+ verifyZeroInteractions(sliderStateListener)
+ }
+
+ @Test
+ fun onProgressChangeByProgram_onArrowMovedOnce_movesToArrowMovesContinuously() = runTest {
+ // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
+ initTracker(testScheduler)
+ mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
+
+ // WHEN the slider gets an external progress change
+ val progress = 0.5f
+ sliderEventProducer.sendEvent(
+ SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+ )
+
+ // THEN the tracker moves to ARROW_HANDLE_MOVES_CONTINUOUSLY and calls the appropriate
+ // haptics
+ assertThat(mSeekableSliderTracker.currentState)
+ .isEqualTo(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+ verify(sliderStateListener).onProgress(progress)
+ }
+
+ @Test
+ fun onArrowUp_onArrowMovesContinuously_movesToIdle() = runTest {
+ // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
+ initTracker(testScheduler)
+ mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+
+ // WHEN the external stimulus is released
+ val progress = 0.5f
+ sliderEventProducer.sendEvent(SliderEvent(SliderEventType.ARROW_UP, progress))
+
+ // THEN the tracker moves to IDLE and no haptics are played
+ assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ verifyZeroInteractions(sliderStateListener)
+ }
+
+ @Test
+ fun onStartTrackingTouch_onArrowMovesContinuously_movesToWait() = runTest {
+ // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
+ initTracker(testScheduler)
+ mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+
+ // WHEN the slider starts tracking touch
+ val progress = 0.5f
+ sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress))
+
+ // THEN the tracker moves to WAIT and the wait job starts.
+ assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.WAIT)
+ assertThat(mSeekableSliderTracker.isWaiting).isTrue()
+ verifyZeroInteractions(sliderStateListener)
+ }
+
+ @Test
+ fun onProgressChangeByProgram_onArrowMovesContinuously_preservesState() = runTest {
+ // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
+ initTracker(testScheduler)
+ mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+
+ // WHEN the slider changes progress programmatically at the middle
+ val progress = 0.5f
+ sliderEventProducer.sendEvent(
+ SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+ )
+
+ // THEN the tracker stays in the same state and haptics are delivered appropriately
+ assertThat(mSeekableSliderTracker.currentState)
+ .isEqualTo(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+ verify(sliderStateListener).onProgress(progress)
+ }
+
+ @Test
+ fun onProgramProgress_atLowerBookend_onArrowMovesContinuously_movesToIdle() = runTest {
+ // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
+ val config = SeekableSliderTrackerConfig()
+ initTracker(testScheduler, config)
+ mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+
+ // WHEN the slider reaches the lower bookend programmatically
+ val progress = config.lowerBookendThreshold - 0.01f
+ sliderEventProducer.sendEvent(
+ SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+ )
+
+ // THEN the tracker executes lower bookend haptics before moving to IDLE
+ verify(sliderStateListener).onLowerBookend()
+ assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ }
+
+ @Test
+ fun onProgramProgress_atUpperBookend_onArrowMovesContinuously_movesToIdle() = runTest {
+ // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
+ val config = SeekableSliderTrackerConfig()
+ initTracker(testScheduler, config)
+ mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+
+ // WHEN the slider reaches the lower bookend programmatically
+ val progress = config.upperBookendThreshold + 0.01f
+ sliderEventProducer.sendEvent(
+ SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+ )
+
+ // THEN the tracker executes upper bookend haptics before moving to IDLE
+ verify(sliderStateListener).onUpperBookend()
+ assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ }
+
@OptIn(ExperimentalCoroutinesApi::class)
private fun initTracker(
scheduler: TestCoroutineScheduler,