diff options
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, |