diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt index 0ffd08b380a59..35f5a8ca43456 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt @@ -27,6 +27,7 @@ import android.view.View import android.view.ViewConfiguration import android.widget.SeekBar import androidx.annotation.AnyThread +import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import androidx.core.view.GestureDetectorCompat import androidx.lifecycle.LiveData @@ -37,6 +38,7 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.util.concurrency.RepeatableExecutor import javax.inject.Inject +import kotlin.math.abs private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L private const val MIN_FLING_VELOCITY_SCALE_FACTOR = 10 @@ -325,6 +327,10 @@ constructor( return SeekBarChangeListener(this, falsingManager) } + /** first and last motion events of seekbar grab. */ + @VisibleForTesting var firstMotionEvent: MotionEvent? = null + @VisibleForTesting var lastMotionEvent: MotionEvent? = null + /** Attach touch handlers to the seek bar view. */ fun attachTouchHandlers(bar: SeekBar) { bar.setOnSeekBarChangeListener(seekBarListener) @@ -351,6 +357,23 @@ constructor( } } + /** + * This method specifies if user made a bad seekbar grab or not. If the vertical distance from + * first touch on seekbar is more than the horizontal distance, this means that the seekbar grab + * is more vertical and should be rejected. Seekbar accepts horizontal grabs only. + * + * Single tap has the same first and last motion event, it is counted as a valid grab. + * + * @return whether the touch on seekbar is valid. + */ + private fun isValidSeekbarGrab(): Boolean { + if (firstMotionEvent == null || lastMotionEvent == null) { + return true + } + return abs(firstMotionEvent!!.x - lastMotionEvent!!.x) >= + abs(firstMotionEvent!!.y - lastMotionEvent!!.y) + } + /** Listener interface to be notified when the user starts or stops scrubbing. */ interface ScrubbingChangeListener { fun onScrubbingChanged(scrubbing: Boolean) @@ -376,7 +399,7 @@ constructor( } override fun onStopTrackingTouch(bar: SeekBar) { - if (falsingManager.isFalseTouch(MEDIA_SEEKBAR)) { + if (!viewModel.isValidSeekbarGrab() || falsingManager.isFalseTouch(MEDIA_SEEKBAR)) { viewModel.onSeekFalse() } viewModel.onSeek(bar.progress.toLong()) @@ -424,6 +447,8 @@ constructor( return false } detector.onTouchEvent(event) + // Store the last motion event done on seekbar. + viewModel.lastMotionEvent = event.copy() return !shouldGoToSeekBar } @@ -468,6 +493,8 @@ constructor( if (shouldGoToSeekBar) { bar.parent?.requestDisallowInterceptTouchEvent(true) } + // Store the first motion event done on seekbar. + viewModel.firstMotionEvent = event.copy() return shouldGoToSeekBar } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt index 56c91bc4525d6..e3c8b052327cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt @@ -22,6 +22,7 @@ import android.media.session.MediaSession import android.media.session.PlaybackState import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.view.MotionEvent import android.widget.SeekBar import androidx.arch.core.executor.ArchTaskExecutor import androidx.arch.core.executor.TaskExecutor @@ -466,20 +467,62 @@ public class SeekBarViewModelTest : SysuiTestCase() { whenever(mockController.getTransportControls()).thenReturn(mockTransport) whenever(falsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).thenReturn(true) whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) - viewModel.updateController(mockController) - val pos = 169 - viewModel.attachTouchHandlers(mockBar) + viewModel.updateController(mockController) + val pos = 40 + val bar = SeekBar(context).apply { progress = pos } with(viewModel.seekBarListener) { - onStartTrackingTouch(mockBar) - onProgressChanged(mockBar, pos, true) - onStopTrackingTouch(mockBar) + onStartTrackingTouch(bar) + onStopTrackingTouch(bar) } + fakeExecutor.runAllReady() // THEN transport controls should not be used verify(mockTransport, never()).seekTo(pos.toLong()) } + @Test + fun onSeekbarGrabInvalidTouch() { + whenever(mockController.getTransportControls()).thenReturn(mockTransport) + viewModel.firstMotionEvent = + MotionEvent.obtain(12L, 13L, MotionEvent.ACTION_DOWN, 76F, 0F, 0) + viewModel.lastMotionEvent = MotionEvent.obtain(12L, 14L, MotionEvent.ACTION_UP, 78F, 4F, 0) + val pos = 78 + + viewModel.updateController(mockController) + // WHEN user ends drag + val bar = SeekBar(context).apply { progress = pos } + with(viewModel.seekBarListener) { + onStartTrackingTouch(bar) + onStopTrackingTouch(bar) + } + fakeExecutor.runAllReady() + + // THEN transport controls should not be used + verify(mockTransport, never()).seekTo(pos.toLong()) + } + + @Test + fun onSeekbarGrabValidTouch() { + whenever(mockController.transportControls).thenReturn(mockTransport) + viewModel.firstMotionEvent = + MotionEvent.obtain(12L, 13L, MotionEvent.ACTION_DOWN, 36F, 0F, 0) + viewModel.lastMotionEvent = MotionEvent.obtain(12L, 14L, MotionEvent.ACTION_UP, 40F, 1F, 0) + val pos = 40 + + viewModel.updateController(mockController) + // WHEN user ends drag + val bar = SeekBar(context).apply { progress = pos } + with(viewModel.seekBarListener) { + onStartTrackingTouch(bar) + onStopTrackingTouch(bar) + } + fakeExecutor.runAllReady() + + // THEN transport controls should be used + verify(mockTransport).seekTo(pos.toLong()) + } + @Test fun queuePollTaskWhenPlaying() { // GIVEN that the track is playing