diff options
| -rw-r--r-- | core/api/current.txt | 18 | ||||
| -rw-r--r-- | core/java/android/view/HapticScrollFeedbackProvider.java | 75 | ||||
| -rw-r--r-- | core/java/android/view/ScrollFeedbackProvider.java | 96 | ||||
| -rw-r--r-- | core/java/android/view/ViewConfiguration.java | 110 | ||||
| -rw-r--r-- | core/java/android/view/flags/scroll_feedback_flags.aconfig | 8 | ||||
| -rw-r--r-- | core/res/res/values/config.xml | 5 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 1 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java | 454 |
8 files changed, 686 insertions, 81 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 14ddf400e9bb..8de8ab895962 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -50040,6 +50040,13 @@ package android.view { field public static final int VIRTUAL_KEY_RELEASE = 8; // 0x8 } + @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public class HapticScrollFeedbackProvider implements android.view.ScrollFeedbackProvider { + ctor public HapticScrollFeedbackProvider(@NonNull android.view.View); + method public void onScrollLimit(int, int, int, boolean); + method public void onScrollProgress(int, int, int, int); + method public void onSnapToItem(int, int, int); + } + public class InflateException extends java.lang.RuntimeException { ctor public InflateException(); ctor public InflateException(String, Throwable); @@ -51205,6 +51212,15 @@ package android.view { method @UiThread public void updatePositionInWindow(); } + @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public interface ScrollFeedbackProvider { + method public void onScrollLimit(int, int, int, boolean); + method public default void onScrollLimit(@NonNull android.view.MotionEvent, int, boolean); + method public void onScrollProgress(int, int, int, int); + method public default void onScrollProgress(@NonNull android.view.MotionEvent, int, int); + method public void onSnapToItem(int, int, int); + method public default void onSnapToItem(@NonNull android.view.MotionEvent, int); + } + public class SearchEvent { ctor public SearchEvent(android.view.InputDevice); method public android.view.InputDevice getInputDevice(); @@ -52482,6 +52498,7 @@ package android.view { method @Deprecated public static int getEdgeSlop(); method @Deprecated public static int getFadingEdgeLength(); method @Deprecated public static long getGlobalActionKeyTimeout(); + method @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public int getHapticScrollFeedbackTickInterval(int, int, int); method public static int getJumpTapTimeout(); method public static int getKeyRepeatDelay(); method public static int getKeyRepeatTimeout(); @@ -52521,6 +52538,7 @@ package android.view { method @Deprecated public static int getWindowTouchSlop(); method public static long getZoomControlsTimeout(); method public boolean hasPermanentMenuKey(); + method @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public boolean isHapticScrollFeedbackEnabled(int, int, int); method public boolean shouldShowMenuShortcutsWhenKeyboardPresent(); } diff --git a/core/java/android/view/HapticScrollFeedbackProvider.java b/core/java/android/view/HapticScrollFeedbackProvider.java index 7e103a53d4ed..fba23ba8369f 100644 --- a/core/java/android/view/HapticScrollFeedbackProvider.java +++ b/core/java/android/view/HapticScrollFeedbackProvider.java @@ -16,7 +16,9 @@ package android.view; -import static com.android.internal.R.dimen.config_rotaryEncoderAxisScrollTickInterval; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.view.flags.Flags; import com.android.internal.annotations.VisibleForTesting; @@ -25,16 +27,15 @@ import com.android.internal.annotations.VisibleForTesting; * * <p>Each scrolling widget should have its own instance of this class to ensure that scroll state * is isolated. - * - * @hide */ +@FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { private static final String TAG = "HapticScrollFeedbackProvider"; private static final int TICK_INTERVAL_NO_TICK = 0; - private static final int TICK_INTERVAL_UNSET = Integer.MAX_VALUE; private final View mView; + private final ViewConfiguration mViewConfig; // Info about the cause of the latest scroll event. @@ -49,26 +50,35 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { * Cache for tick interval for scroll tick caused by a {@link InputDevice#SOURCE_ROTARY_ENCODER} * on {@link MotionEvent#AXIS_SCROLL}. Set to -1 if the value has not been fetched and cached. */ - private int mRotaryEncoderAxisScrollTickIntervalPixels = TICK_INTERVAL_UNSET; /** The tick interval corresponding to the current InputDevice/source/axis. */ private int mTickIntervalPixels = TICK_INTERVAL_NO_TICK; private int mTotalScrollPixels = 0; private boolean mCanPlayLimitFeedback = true; + private boolean mHapticScrollFeedbackEnabled = false; - public HapticScrollFeedbackProvider(View view) { - this(view, /* rotaryEncoderAxisScrollTickIntervalPixels= */ TICK_INTERVAL_UNSET); + public HapticScrollFeedbackProvider(@NonNull View view) { + this(view, ViewConfiguration.get(view.getContext())); } /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) - public HapticScrollFeedbackProvider(View view, int rotaryEncoderAxisScrollTickIntervalPixels) { + public HapticScrollFeedbackProvider(View view, ViewConfiguration viewConfig) { mView = view; - mRotaryEncoderAxisScrollTickIntervalPixels = rotaryEncoderAxisScrollTickIntervalPixels; + mViewConfig = viewConfig; } @Override - public void onScrollProgress(MotionEvent event, int axis, int deltaInPixels) { - maybeUpdateCurrentConfig(event, axis); + public void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels) { + maybeUpdateCurrentConfig(inputDeviceId, source, axis); + if (!mHapticScrollFeedbackEnabled) { + return; + } + + // Unlock limit feedback regardless of scroll tick being enabled as long as there's a + // non-zero scroll progress. + if (deltaInPixels != 0) { + mCanPlayLimitFeedback = true; + } if (mTickIntervalPixels == TICK_INTERVAL_NO_TICK) { // There's no valid tick interval. Exit early before doing any further computation. @@ -82,13 +92,14 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_TICK); } - - mCanPlayLimitFeedback = true; } @Override - public void onScrollLimit(MotionEvent event, int axis, boolean isStart) { - maybeUpdateCurrentConfig(event, axis); + public void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart) { + maybeUpdateCurrentConfig(inputDeviceId, source, axis); + if (!mHapticScrollFeedbackEnabled) { + return; + } if (!mCanPlayLimitFeedback) { return; @@ -101,41 +112,33 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { } @Override - public void onSnapToItem(MotionEvent event, int axis) { + public void onSnapToItem(int inputDeviceId, int source, int axis) { + maybeUpdateCurrentConfig(inputDeviceId, source, axis); + if (!mHapticScrollFeedbackEnabled) { + return; + } // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_ITEM_FOCUS); mCanPlayLimitFeedback = true; } - private void maybeUpdateCurrentConfig(MotionEvent event, int axis) { - int source = event.getSource(); - int deviceId = event.getDeviceId(); - + private void maybeUpdateCurrentConfig(int deviceId, int source, int axis) { if (mAxis != axis || mSource != source || mDeviceId != deviceId) { mSource = source; mAxis = axis; mDeviceId = deviceId; + mHapticScrollFeedbackEnabled = + mViewConfig.isHapticScrollFeedbackEnabled(deviceId, axis, source); mCanPlayLimitFeedback = true; mTotalScrollPixels = 0; - calculateTickIntervals(source, axis); + updateTickIntervals(deviceId, source, axis); } } - private void calculateTickIntervals(int source, int axis) { - mTickIntervalPixels = TICK_INTERVAL_NO_TICK; - - if (axis == MotionEvent.AXIS_SCROLL && source == InputDevice.SOURCE_ROTARY_ENCODER) { - if (mRotaryEncoderAxisScrollTickIntervalPixels == TICK_INTERVAL_UNSET) { - // Value has not been fetched yet. Fetch and cache it. - mRotaryEncoderAxisScrollTickIntervalPixels = - mView.getContext().getResources().getDimensionPixelSize( - config_rotaryEncoderAxisScrollTickInterval); - if (mRotaryEncoderAxisScrollTickIntervalPixels < 0) { - mRotaryEncoderAxisScrollTickIntervalPixels = TICK_INTERVAL_NO_TICK; - } - } - mTickIntervalPixels = mRotaryEncoderAxisScrollTickIntervalPixels; - } + private void updateTickIntervals(int deviceId, int source, int axis) { + mTickIntervalPixels = mHapticScrollFeedbackEnabled + ? mViewConfig.getHapticScrollFeedbackTickInterval(deviceId, axis, source) + : TICK_INTERVAL_NO_TICK; } } diff --git a/core/java/android/view/ScrollFeedbackProvider.java b/core/java/android/view/ScrollFeedbackProvider.java index 8d3491df5d9e..6f760c56e11a 100644 --- a/core/java/android/view/ScrollFeedbackProvider.java +++ b/core/java/android/view/ScrollFeedbackProvider.java @@ -16,16 +16,45 @@ package android.view; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.view.flags.Flags; + /** * Interface to represent an entity giving consistent feedback for different events surrounding view * scroll. * - * @hide + * <p>When you have access to the {@link MotionEvent}s that triggered the different scroll events, + * use the {@link MotionEvent} based APIs in this class. If you do not have access to the motion + * events, you can use the methods that accept the {@link InputDevice} ID (which can be obtained by + * APIs like {@link MotionEvent#getDeviceId()} and {@link InputDevice#getId()}) and source (which + * can be obtained by APIs like {@link MotionEvent#getSource()}) of the motion that caused the + * scroll events. */ +@FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public interface ScrollFeedbackProvider { /** - * The view has snapped to an item, with a motion from a given {@link MotionEvent} on a given - * {@code axis}. + * Call this when the view has snapped to an item, with a motion generated by an + * {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and on + * a given motion event {@code axis}. + * + * <p>This method has the same purpose as {@link #onSnapToItem(MotionEvent, int)}. When a scroll + * snap happens, call either this method or {@link #onSnapToItem(MotionEvent, int)}, not both. + * This method is useful when you have no direct access to the {@link MotionEvent} that + * caused the snap event. + * + * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion triggering + * the snap. + * @param source the input source of the motion causing the snap. + * @param axis the axis of {@code event} that caused the item to snap. + * + * @see #onSnapToItem(MotionEvent, int) + */ + void onSnapToItem(int inputDeviceId, int source, int axis); + + /** + * Call this when the view has snapped to an item, with a motion from a given + * {@link MotionEvent} on a given {@code axis}. * * <p>The interface is not aware of the internal scroll states of the view for which scroll * feedback is played. As such, the client should call @@ -33,22 +62,69 @@ public interface ScrollFeedbackProvider { * * @param event the {@link MotionEvent} that caused the item to snap. * @param axis the axis of {@code event} that caused the item to snap. + * + * @see #onSnapToItem(int, int, int) */ - void onSnapToItem(MotionEvent event, int axis); + default void onSnapToItem(@NonNull MotionEvent event, int axis) { + onSnapToItem(event.getDeviceId(), event.getSource(), axis); + } /** - * The view has reached the scroll limit when scrolled by the motion from a given + * Call this when the view has reached the scroll limit when scrolled by a motion generated by + * an {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and + * on a given motion event {@code axis}. + * + * <p>This method has the same purpose as {@link #onScrollLimit(MotionEvent, int, boolean)}. + * When a scroll limit happens, call either this method or + * {@link #onScrollLimit(MotionEvent, int, boolean)}, not both. This method is useful when you + * have no direct access to the {@link MotionEvent} that caused the scroll limit. + * + * @param inputDeviceId the ID of the {@link InputDevice} that caused scrolling to hit limit. + * @param source the input source of the motion that caused scrolling to hit the limit. + * @param axis the axis of {@code event} that caused scrolling to hit the limit. + * @param isStart {@code true} if scrolling hit limit at the start of the scrolling list, and + * {@code false} if the scrolling hit limit at the end of the scrolling list. + * + * @see #onScrollLimit(MotionEvent, int, boolean) + */ + void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart); + + /** + * Call this when the view has reached the scroll limit when scrolled by the motion from a given * {@link MotionEvent} on a given {@code axis}. * * @param event the {@link MotionEvent} that caused scrolling to hit the limit. * @param axis the axis of {@code event} that caused scrolling to hit the limit. * @param isStart {@code true} if scrolling hit limit at the start of the scrolling list, and * {@code false} if the scrolling hit limit at the end of the scrolling list. + * + * @see #onScrollLimit(int, int, int, boolean) + */ + default void onScrollLimit(@NonNull MotionEvent event, int axis, boolean isStart) { + onScrollLimit(event.getDeviceId(), event.getSource(), axis, isStart); + } + + /** + * Call this when the view has scrolled by {@code deltaInPixels} due to the motion generated by + * an {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and + * on a given motion event {@code axis}. + * + * <p>This method has the same purpose as {@link #onScrollProgress(MotionEvent, int, int)}. + * When a scroll progress happens, call either this method or + * {@link #onScrollProgress(MotionEvent, int, int)}, not both. This method is useful when you + * have no direct access to the {@link MotionEvent} that caused the scroll progress. + * + * @param inputDeviceId the ID of the {@link InputDevice} that caused scroll progress. + * @param source the input source of the motion that caused scroll progress. + * @param axis the axis of {@code event} that caused scroll progress. + * @param deltaInPixels the amount of scroll progress, in pixels. + * + * @see #onScrollProgress(MotionEvent, int, int) */ - void onScrollLimit(MotionEvent event, int axis, boolean isStart); + void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels); /** - * The view has scrolled by {@code deltaInPixels} due to the motion from a given + * Call this when the view has scrolled by {@code deltaInPixels} due to the motion from a given * {@link MotionEvent} on a given {@code axis}. * * <p>The interface is not aware of the internal scroll states of the view for which scroll @@ -58,6 +134,10 @@ public interface ScrollFeedbackProvider { * @param event the {@link MotionEvent} that caused scroll progress. * @param axis the axis of {@code event} that caused scroll progress. * @param deltaInPixels the amount of scroll progress, in pixels. + * + * @see #onScrollProgress(int, int, int, int) */ - void onScrollProgress(MotionEvent event, int axis, int deltaInPixels); + default void onScrollProgress(@NonNull MotionEvent event, int axis, int deltaInPixels) { + onScrollProgress(event.getDeviceId(), event.getSource(), axis, deltaInPixels); + } } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 2a88cf074f96..0244d462473b 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.TestApi; @@ -37,6 +38,7 @@ import android.provider.Settings; import android.util.DisplayMetrics; import android.util.SparseArray; import android.util.TypedValue; +import android.view.flags.Flags; /** * Contains methods to standard constants used in the UI for timeouts, sizes, and distances. @@ -240,6 +242,9 @@ public class ViewConfiguration { /** Value used as a maximum fling velocity, when fling is not supported. */ private static final int NO_FLING_MAX_VELOCITY = Integer.MIN_VALUE; + /** @hide */ + public static final int NO_HAPTIC_SCROLL_TICK_INTERVAL = Integer.MAX_VALUE; + /** * Delay before dispatching a recurring accessibility event in milliseconds. * This delay guarantees that a recurring event will be send at most once @@ -343,6 +348,8 @@ public class ViewConfiguration { private final int mMaximumFlingVelocity; private final int mMinimumRotaryEncoderFlingVelocity; private final int mMaximumRotaryEncoderFlingVelocity; + private final int mRotaryEncoderHapticScrollFeedbackTickIntervalPixels; + private final boolean mRotaryEncoderHapticScrollFeedbackEnabled; private final int mScrollbarSize; private final int mTouchSlop; private final int mHandwritingSlop; @@ -390,6 +397,8 @@ public class ViewConfiguration { mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY; mMinimumRotaryEncoderFlingVelocity = MINIMUM_FLING_VELOCITY; mMaximumRotaryEncoderFlingVelocity = MAXIMUM_FLING_VELOCITY; + mRotaryEncoderHapticScrollFeedbackEnabled = false; + mRotaryEncoderHapticScrollFeedbackTickIntervalPixels = NO_HAPTIC_SCROLL_TICK_INTERVAL; mScrollbarSize = SCROLL_BAR_SIZE; mTouchSlop = TOUCH_SLOP; mHandwritingSlop = HANDWRITING_SLOP; @@ -529,6 +538,20 @@ public class ViewConfiguration { mMaximumRotaryEncoderFlingVelocity = configMaxRotaryEncoderFlingVelocity; } + int configRotaryEncoderHapticScrollFeedbackTickIntervalPixels = + res.getDimensionPixelSize( + com.android.internal.R.dimen + .config_rotaryEncoderAxisScrollTickInterval); + mRotaryEncoderHapticScrollFeedbackTickIntervalPixels = + configRotaryEncoderHapticScrollFeedbackTickIntervalPixels > 0 + ? configRotaryEncoderHapticScrollFeedbackTickIntervalPixels + : NO_HAPTIC_SCROLL_TICK_INTERVAL; + + mRotaryEncoderHapticScrollFeedbackEnabled = + res.getBoolean( + com.android.internal.R.bool + .config_viewRotaryEncoderHapticScrollFedbackEnabled); + mGlobalActionsKeyTimeout = res.getInteger( com.android.internal.R.integer.config_globalActionsKeyTimeout); @@ -1193,6 +1216,93 @@ public class ViewConfiguration { return mMaximumFlingVelocity; } + /** + * Checks if any kind of scroll haptic feedback is enabled for a motion generated by a specific + * input device configuration and motion axis. + * + * <h3>Obtaining the correct arguments for this method call</h3> + * <p><b>inputDeviceId</b>: if calling this method in response to a {@link MotionEvent}, use + * the device ID that is reported by the event, which can be obtained using + * {@link MotionEvent#getDeviceId()}. Otherwise, use a valid ID that is obtained from + * {@link InputDevice#getId()}, or from an {@link InputManager} instance + * ({@link InputManager#getInputDeviceIds()} gives all the valid input device IDs). + * + * <p><b>axis</b>: a {@link MotionEvent} may report data for multiple axes, and each axis may + * have multiple data points for different pointers. Use the axis whose movement produced the + * scrolls that would generate the scroll haptics. You can use + * {@link InputDevice#getMotionRanges()} to get all the {@link InputDevice.MotionRange}s for the + * {@link InputDevice}, from which you can derive all the valid axes for the device. + * + * <p><b>source</b>: use {@link MotionEvent#getSource()} if calling this method in response to a + * {@link MotionEvent}. Otherwise, use a valid source for the {@link InputDevice}. You can use + * {@link InputDevice#getMotionRanges()} to get all the {@link InputDevice.MotionRange}s for the + * {@link InputDevice}, from which you can derive all the valid sources for the device. + * + * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion that may + * produce scroll haptics. + * @param source the input source of the motion that may produce scroll haptics. + * @param axis the axis of the motion that may produce scroll haptics. + * @return {@code true} if motions generated by the provided input and motion configuration + * should produce scroll haptics. {@code false} otherwise. + */ + @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) + public boolean isHapticScrollFeedbackEnabled(int inputDeviceId, int axis, int source) { + if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) return false; + + if (source == InputDevice.SOURCE_ROTARY_ENCODER) { + return mRotaryEncoderHapticScrollFeedbackEnabled; + } + + return false; + } + + /** + * Provides the minimum scroll interval (in pixels) between consecutive scroll tick haptics for + * motions generated by a specific input device configuration and motion axis. + * + * <p><b>Scroll tick</b> here refers to an interval-based, consistent scroll feedback provided + * to the user as the user scrolls through a scrollable view. + * + * <p>If you are supporting scroll tick haptics, use this interval as the minimum pixel scroll + * distance between consecutive scroll ticks. That is, once your view has scrolled for at least + * this interval, play a haptic, and wait again until the view has further scrolled with this + * interval in the same direction before playing the next scroll haptic. + * + * <p>Some devices may support other types of scroll haptics but not interval based tick + * haptics. In those cases, this method will return {@code Integer.MAX_VALUE}. The same value + * will be returned if the device does not support scroll haptics at all (which can be checked + * via {@link #isHapticScrollFeedbackEnabled(int, int, int)}). + * + * <p>See {@link #isHapticScrollFeedbackEnabled(int, int, int)} for more details about obtaining + * the correct arguments for this method. + * + * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion that may + * produce scroll haptics. + * @param source the input source of the motion that may produce scroll haptics. + * @param axis the axis of the motion that may produce scroll haptics. + * @return the absolute value of the minimum scroll interval, in pixels, between consecutive + * scroll feedback haptics for motions generated by the provided input and motion + * configuration. If scroll haptics is disabled for the given configuration, or if the + * device does not support scroll tick haptics for the given configuration, this method + * returns {@code Integer.MAX_VALUE}. + */ + @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) + public int getHapticScrollFeedbackTickInterval(int inputDeviceId, int axis, int source) { + if (!mRotaryEncoderHapticScrollFeedbackEnabled) { + return NO_HAPTIC_SCROLL_TICK_INTERVAL; + } + + if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) { + return NO_HAPTIC_SCROLL_TICK_INTERVAL; + } + + if (source == InputDevice.SOURCE_ROTARY_ENCODER) { + return mRotaryEncoderHapticScrollFeedbackTickIntervalPixels; + } + + return NO_HAPTIC_SCROLL_TICK_INTERVAL; + } + private static boolean isInputDeviceInfoValid(int id, int axis, int source) { InputDevice device = InputManagerGlobal.getInstance().getInputDevice(id); return device != null && device.getMotionRange(axis, source) != null; diff --git a/core/java/android/view/flags/scroll_feedback_flags.aconfig b/core/java/android/view/flags/scroll_feedback_flags.aconfig new file mode 100644 index 000000000000..62c569152ee9 --- /dev/null +++ b/core/java/android/view/flags/scroll_feedback_flags.aconfig @@ -0,0 +1,8 @@ +package: "android.view.flags" + +flag { + namespace: "toolkit" + name: "scroll_feedback_api" + description: "Enable the scroll feedback APIs" + bug: "239594271" +}
\ No newline at end of file diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 367a4f5f050d..7d2690eabc96 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6698,4 +6698,9 @@ <!-- Whether unlocking and waking a device are sequenced --> <bool name="config_orderUnlockAndWake">false</bool> + + <!-- Whether scroll haptic feedback is enabled for rotary encoder scrolls on + {@link MotionEvent#AXIS_SCROLL} generated by {@link InputDevice#SOURCE_ROTARY_ENCODER} + devices. --> + <bool name="config_viewRotaryEncoderHapticScrollFedbackEnabled">false</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 1965172c8047..80c2fbf715fb 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5230,4 +5230,5 @@ <java-symbol type="bool" name="config_tvExternalInputLoggingDisplayNameFilterEnabled" /> <java-symbol type="array" name="config_tvExternalInputLoggingDeviceOnScreenDisplayNames" /> <java-symbol type="array" name="config_tvExternalInputLoggingDeviceBrandNames" /> + <java-symbol type="bool" name="config_viewRotaryEncoderHapticScrollFedbackEnabled" /> </resources> diff --git a/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java b/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java index 6bdb07d70212..a2c41e440107 100644 --- a/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java +++ b/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java @@ -19,8 +19,10 @@ package android.view; import static android.view.HapticFeedbackConstants.SCROLL_ITEM_FOCUS; import static android.view.HapticFeedbackConstants.SCROLL_LIMIT; import static android.view.HapticFeedbackConstants.SCROLL_TICK; - import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import android.content.Context; import android.platform.test.annotations.Presubmit; @@ -32,6 +34,7 @@ import androidx.test.filters.SmallTest; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import java.util.HashMap; import java.util.Map; @@ -43,28 +46,71 @@ public final class HapticScrollFeedbackProviderTest { private static final int INPUT_DEVICE_1 = 1; private static final int INPUT_DEVICE_2 = 2; - private static final int TICK_INTERVAL_PIXELS = 100; - private TestView mView; private long mCurrentTimeMillis = 1000; // arbitrary starting time value + @Mock ViewConfiguration mMockViewConfig; + private HapticScrollFeedbackProvider mProvider; @Before public void setUp() { + mMockViewConfig = mock(ViewConfiguration.class); + setHapticScrollFeedbackEnabled(true); + mView = new TestView(InstrumentationRegistry.getContext()); - mProvider = new HapticScrollFeedbackProvider(mView, TICK_INTERVAL_PIXELS); + mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig); } @Test - public void testSnapToItem() { + public void testNoFeedbackWhenFeedbackIsDisabled() { + setHapticScrollFeedbackEnabled(false); + // Call different types scroll feedback methods; non of them should produce feedback because + // feedback has been disabled. mProvider.onSnapToItem(createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL); + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 10); + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ -9); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 300); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -300); + + assertNoFeedback(mView); + } + + @Test + public void testSnapToItem_withMotionEvent() { + mProvider.onSnapToItem(createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_ITEM_FOCUS); + } + + @Test + public void testSnapToItem_withDeviceIdAndSource() { + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_ITEM_FOCUS); } @Test - public void testScrollLimit_start() { + public void testScrollLimit_start_withMotionEvent() { mProvider.onScrollLimit( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true); @@ -72,7 +118,16 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollLimit_stop() { + public void testScrollLimit_start_withDeviceIdAndSource() { + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT); + } + + @Test + public void testScrollLimit_stop_withMotionEvent() { mProvider.onScrollLimit( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); @@ -80,10 +135,17 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollProgress_zeroTickInterval() { - mProvider = - new HapticScrollFeedbackProvider( - mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 0); + public void testScrollLimit_stop_withDeviceIdAndSource() { + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT); + } + + @Test + public void testScrollProgress_zeroTickInterval_withMotionEvent() { + setHapticScrollTickInterval(0); mProvider.onScrollProgress( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 10); @@ -96,10 +158,22 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollProgress_progressEqualsOrExceedsPositiveThreshold() { - mProvider = - new HapticScrollFeedbackProvider( - mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 100); + public void testScrollProgress_zeroTickInterval_withDeviceIdAndSource() { + setHapticScrollTickInterval(0); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 30); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 20); + + assertNoFeedback(mView); + } + + @Test + public void testScrollProgress_progressEqualsOrExceedsPositiveThreshold_withMotionEvent() { + setHapticScrollTickInterval(100); mProvider.onScrollProgress( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 20); @@ -121,10 +195,30 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollProgress_progressEqualsOrExceedsNegativeThreshold() { - mProvider = - new HapticScrollFeedbackProvider( - mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 100); + public void testScrollProgress_progressEqualsOrExceedsPositiveThreshold_withDeviceIdAndSrc() { + setHapticScrollTickInterval(100); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 20); + + assertNoFeedback(mView); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 80); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 120); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 2); + } + + @Test + public void testScrollProgress_progressEqualsOrExceedsNegativeThreshold_withMotionEvent() { + setHapticScrollTickInterval(100); mProvider.onScrollProgress( createRotaryEncoderScrollEvent(), @@ -153,10 +247,34 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollProgress_positiveAndNegativeProgresses() { - mProvider = - new HapticScrollFeedbackProvider( - mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 100); + public void testScrollProgress_progressEqualsOrExceedsNegativeThreshold_withDeviceIdAndSrc() { + setHapticScrollTickInterval(100); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -20); + + assertNoFeedback(mView); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -80); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -70); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -40); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 2); + } + + @Test + public void testScrollProgress_positiveAndNegativeProgresses_withMotionEvent() { + setHapticScrollTickInterval(100); mProvider.onScrollProgress( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 20); @@ -190,10 +308,48 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollProgress_singleProgressExceedsThreshold() { - mProvider = - new HapticScrollFeedbackProvider( - mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 100); + public void testScrollProgress_positiveAndNegativeProgresses_withDeviceIdAndSource() { + setHapticScrollTickInterval(100); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 20); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -90); + + assertNoFeedback(mView); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 10); + + assertNoFeedback(mView); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -50); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 40); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 50); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 60); + + + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 2); + } + + @Test + public void testScrollProgress_singleProgressExceedsThreshold_withMotionEvent() { + setHapticScrollTickInterval(100); mProvider.onScrollProgress( createRotaryEncoderScrollEvent(), @@ -204,7 +360,18 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollLimit_startAndEndLimit_playsOnlyOneFeedback() { + public void testScrollProgress_singleProgressExceedsThreshold_withDeviceIdAndSource() { + setHapticScrollTickInterval(100); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 1000); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1); + } + + @Test + public void testScrollLimit_startAndEndLimit_playsOnlyOneFeedback_withMotionEvent() { mProvider.onScrollLimit( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); mProvider.onScrollLimit( @@ -214,7 +381,19 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollLimit_doubleStartLimit_playsOnlyOneFeedback() { + public void testScrollLimit_startAndEndLimit_playsOnlyOneFeedback_withDeviceIdAndSource() { + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT); + } + + @Test + public void testScrollLimit_doubleStartLimit_playsOnlyOneFeedback_withMotionEvent() { mProvider.onScrollLimit( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true); mProvider.onScrollLimit( @@ -224,7 +403,19 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollLimit_doubleEndLimit_playsOnlyOneFeedback() { + public void testScrollLimit_doubleStartLimit_playsOnlyOneFeedback_withDeviceIdAndSource() { + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT); + } + + @Test + public void testScrollLimit_doubleEndLimit_playsOnlyOneFeedback_withMotionEvent() { mProvider.onScrollLimit( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); mProvider.onScrollLimit( @@ -234,7 +425,37 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollLimit_enabledWithProgress() { + public void testScrollLimit_doubleEndLimit_playsOnlyOneFeedback_withDeviceIdAndSource() { + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT); + } + + @Test + public void testScrollLimit_notEnabledWithZeroProgress() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(INPUT_DEVICE_1), MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 0); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 1); + } + + @Test + public void testScrollLimit_enabledWithProgress_withMotionEvent() { mProvider.onScrollLimit( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); @@ -247,7 +468,23 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollLimit_enabledWithSnap() { + public void testScrollLimit_enabledWithProgress_withDeviceIdAndSource() { + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 80); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2); + } + + @Test + public void testScrollLimit_enabledWithSnap_withMotionEvent() { mProvider.onScrollLimit( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); @@ -259,7 +496,22 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollLimit_enabledWithDissimilarSnap() { + public void testScrollLimit_enabledWithSnap_withDeviceIdAndSource() { + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertFeedbackCount(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2); + } + + @Test + public void testScrollLimit_enabledWithDissimilarSnap_withMotionEvent() { mProvider.onScrollLimit( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); @@ -271,7 +523,22 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollLimit_enabledWithDissimilarProgress() { + public void testScrollLimit_enabledWithDissimilarSnap_withDeviceIdAndSource() { + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_X); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertFeedbackCount(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2); + } + + @Test + public void testScrollLimit_enabledWithDissimilarProgress_withMotionEvent() { mProvider.onScrollLimit( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); @@ -284,7 +551,23 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollLimit_enabledWithDissimilarLimit() { + public void testScrollLimit_enabledWithDissimilarProgress_withDeviceIdAndSource() { + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 80); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2); + } + + @Test + public void testScrollLimit_enabledWithDissimilarLimit_withMotionEvent() { mProvider.onScrollLimit( createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); @@ -296,7 +579,22 @@ public final class HapticScrollFeedbackProviderTest { } @Test - public void testScrollLimit_enabledWithMotionFromDifferentDeviceId() { + public void testScrollLimit_enabledWithDissimilarLimit_withDeviceIdAndSource() { + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + mProvider.onScrollLimit(INPUT_DEVICE_2, InputDevice.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_X, + /* isStart= */ false); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 3); + } + + @Test + public void testScrollLimit_enabledWithMotionFromDifferentDeviceId_withMotionEvent() { mProvider.onScrollLimit( createRotaryEncoderScrollEvent(INPUT_DEVICE_1), MotionEvent.AXIS_SCROLL, @@ -314,6 +612,78 @@ public final class HapticScrollFeedbackProviderTest { assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 3); } + @Test + public void testScrollLimit_enabledWithMotionFromDifferentDeviceId_withDeviceIdAndSource() { + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + mProvider.onScrollLimit( + INPUT_DEVICE_2, + InputDevice.SOURCE_ROTARY_ENCODER, + MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + mProvider.onScrollLimit( + INPUT_DEVICE_1, + InputDevice.SOURCE_ROTARY_ENCODER, + MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 3); + } + + @Test + public void testSnapToItem_differentApis() { + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + mProvider.onSnapToItem(createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_ITEM_FOCUS, 2); + } + + @Test + public void testScrollLimit_differentApis() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(INPUT_DEVICE_1), + MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 1); + + mProvider.onScrollLimit( + INPUT_DEVICE_2, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(INPUT_DEVICE_2), + MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2); + } + + @Test + public void testScrollProgress_differentApis() { + setHapticScrollTickInterval(100); + + // Neither types of APIs independently excceeds the "100" tick interval. + // But the combined deltas pass 100. + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 20); + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 40); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 30); + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 30); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1); + } + private void assertNoFeedback(TestView view) { for (int feedback : new int[] {SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}) { assertFeedbackCount(view, feedback, 0); @@ -335,6 +705,16 @@ public final class HapticScrollFeedbackProviderTest { assertThat(count).isEqualTo(expectedCount); } + private void setHapticScrollTickInterval(int interval) { + when(mMockViewConfig.getHapticScrollFeedbackTickInterval(anyInt(), anyInt(), anyInt())) + .thenReturn(interval); + } + + private void setHapticScrollFeedbackEnabled(boolean enabled) { + when(mMockViewConfig.isHapticScrollFeedbackEnabled(anyInt(), anyInt(), anyInt())) + .thenReturn(enabled); + } + private MotionEvent createTouchMoveEvent() { long downTime = mCurrentTimeMillis; long eventTime = mCurrentTimeMillis + 2; // arbitrary increment from the down time. @@ -386,4 +766,4 @@ public final class HapticScrollFeedbackProviderTest { return true; } } -} +}
\ No newline at end of file |