summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt18
-rw-r--r--core/java/android/view/HapticScrollFeedbackProvider.java75
-rw-r--r--core/java/android/view/ScrollFeedbackProvider.java96
-rw-r--r--core/java/android/view/ViewConfiguration.java110
-rw-r--r--core/java/android/view/flags/scroll_feedback_flags.aconfig8
-rw-r--r--core/res/res/values/config.xml5
-rw-r--r--core/res/res/values/symbols.xml1
-rw-r--r--core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java454
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