Make ScrollFeedbackProvider APIs Public
This change cleans up the existing ScrollFeedbackProvider APIs and
makes them public. As we plan to use these APIs in Compose as well, this
change adds a variant for each API method that takes the raw motion
properties (instead of a MotionEvent, since MotionEvent is not
necessarily accessible from Compose).
Unit tests have been added to the new APIs. These tests are mostly the
exact same tests as the ones existing for the MotionEvent-based APIs,
just with different arguments.
The APIs are launched as a flagged API.
Bug: 299213080
Test: atest HapticScrollFeedbackProviderTest
Test: CTS tests added
Change-Id: I6de611cd34d103ee4f208d421c31b7a3233f2d7c
diff --git a/core/api/current.txt b/core/api/current.txt
index 14ddf40..8de8ab8 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -50040,6 +50040,13 @@
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 @@
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 @@
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 @@
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 7e103a5..fba23ba 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 @@
*
* <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 @@
* 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 @@
// 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 @@
}
@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 8d3491d..6f760c5 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 @@
*
* @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)
*/
- void onScrollLimit(MotionEvent event, int axis, boolean isStart);
+ default void onScrollLimit(@NonNull MotionEvent event, int axis, boolean isStart) {
+ onScrollLimit(event.getDeviceId(), event.getSource(), axis, isStart);
+ }
/**
- * 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 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 onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels);
+
+ /**
+ * 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 @@
* @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 2a88cf0..0244d46 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.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 @@
/** 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 @@
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 @@
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 @@
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 @@
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 0000000..62c5691
--- /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 367a4f5..7d2690e 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 1965172..80c2fbf 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 6bdb07d..a2c41e4 100644
--- a/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java
+++ b/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java
@@ -19,8 +19,10 @@
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 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 @@
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 testScrollLimit_start() {
+ 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_withMotionEvent() {
mProvider.onScrollLimit(
createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true);
@@ -72,7 +118,16 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
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 @@
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 @@
return true;
}
}
-}
+}
\ No newline at end of file