Loading core/java/android/view/HapticScrollFeedbackProvider.java +16 −12 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package android.view; import static android.view.flags.Flags.dynamicViewRotaryHapticsConfiguration; import android.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; Loading @@ -41,13 +43,8 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { private final View mView; private final ViewConfiguration mViewConfig; /** * Flag to disable the logic in this class if the View-based scroll haptics implementation is * enabled. If {@code false}, this class will continue to run despite the View's scroll * haptics implementation being enabled. This value should be set to {@code true} when this * class is directly used by the View class. */ private final boolean mDisabledIfViewPlaysScrollHaptics; /** Whether or not this provider is being used directly by the View class. */ private final boolean mIsFromView; // Info about the cause of the latest scroll event. Loading @@ -65,17 +62,23 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { private boolean mHapticScrollFeedbackEnabled = false; public HapticScrollFeedbackProvider(@NonNull View view) { this(view, ViewConfiguration.get(view.getContext()), /* disabledIfViewPlaysScrollHaptics= */ true); this(view, ViewConfiguration.get(view.getContext()), /* isFromView= */ false); } /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public HapticScrollFeedbackProvider( View view, ViewConfiguration viewConfig, boolean disabledIfViewPlaysScrollHaptics) { View view, ViewConfiguration viewConfig, boolean isFromView) { mView = view; mViewConfig = viewConfig; mDisabledIfViewPlaysScrollHaptics = disabledIfViewPlaysScrollHaptics; mIsFromView = isFromView; if (dynamicViewRotaryHapticsConfiguration() && !isFromView) { // Disable the View class's rotary scroll feedback logic if this provider is not being // directly used by the View class. This is to avoid double rotary scroll feedback: // one from the View class, and one from this provider instance (i.e. mute the View // class's rotary feedback and enable this provider). view.disableRotaryScrollFeedback(); } } @Override Loading Loading @@ -151,7 +154,8 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { mAxis = axis; mDeviceId = deviceId; if (mDisabledIfViewPlaysScrollHaptics if (!dynamicViewRotaryHapticsConfiguration() && !mIsFromView && (source == InputDevice.SOURCE_ROTARY_ENCODER) && mViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) { mHapticScrollFeedbackEnabled = false; Loading core/java/android/view/View.java +21 −5 Original line number Diff line number Diff line Loading @@ -16754,9 +16754,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags4 |= PFLAG4_ROTARY_HAPTICS_DETERMINED; } } final boolean processForRotaryScrollHaptics = isRotaryEncoderEvent && ((mPrivateFlags4 & PFLAG4_ROTARY_HAPTICS_ENABLED) != 0); if (processForRotaryScrollHaptics) { if (isRotaryEncoderEvent && ((mPrivateFlags4 & PFLAG4_ROTARY_HAPTICS_ENABLED) != 0)) { mPrivateFlags4 &= ~PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT; mPrivateFlags4 |= PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT; } Loading @@ -16773,7 +16771,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Process scroll haptics after `onGenericMotionEvent`, since that's where scrolling usually // happens. Some views may return false from `onGenericMotionEvent` even if they have done // scrolling, so disregard the return value when processing for scroll haptics. if (processForRotaryScrollHaptics) { // Check for `PFLAG4_ROTARY_HAPTICS_ENABLED` again, because the View implementation may // call `disableRotaryScrollFeedback` in `onGenericMotionEvent`, which could change the // value of `PFLAG4_ROTARY_HAPTICS_ENABLED`. if (isRotaryEncoderEvent && ((mPrivateFlags4 & PFLAG4_ROTARY_HAPTICS_ENABLED) != 0)) { if ((mPrivateFlags4 & PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT) != 0) { doRotaryProgressForScrollHaptics(event); } else { Loading Loading @@ -18716,7 +18717,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private HapticScrollFeedbackProvider getScrollFeedbackProvider() { if (mScrollFeedbackProvider == null) { mScrollFeedbackProvider = new HapticScrollFeedbackProvider(this, ViewConfiguration.get(mContext), /* disabledIfViewPlaysScrollHaptics= */ false); ViewConfiguration.get(mContext), /* isFromView= */ true); } return mScrollFeedbackProvider; } Loading Loading @@ -18745,6 +18746,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } /** * Disables the rotary scroll feedback implementation of the View class. * * <p>Note that this does NOT disable all rotary scroll feedback; it just disables the logic * implemented within the View class. The child implementation of the View may implement its own * rotary scroll feedback logic or use {@link ScrollFeedbackProvider} to generate rotary scroll * feedback. */ void disableRotaryScrollFeedback() { // Force set PFLAG4_ROTARY_HAPTICS_DETERMINED to avoid recalculating // PFLAG4_ROTARY_HAPTICS_ENABLED under any circumstance. mPrivateFlags4 |= PFLAG4_ROTARY_HAPTICS_DETERMINED; mPrivateFlags4 &= ~PFLAG4_ROTARY_HAPTICS_ENABLED; } /** * This is called in response to an internal scroll in this view (i.e., the * view scrolled its own contents). This is typically as a result of core/java/android/view/flags/scroll_feedback_flags.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -23,3 +23,10 @@ flag { bug: "331830899" is_fixed_read_only: true } flag { namespace: "wear_frameworks" name: "dynamic_view_rotary_haptics_configuration" description: "Whether ScrollFeedbackProvider dynamically disables View-based rotary haptics." bug: "377998870 " } core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java +25 −5 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package android.view; import static android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED; import static android.view.flags.Flags.FLAG_DYNAMIC_VIEW_ROTARY_HAPTICS_CONFIGURATION; import static android.view.HapticFeedbackConstants.SCROLL_ITEM_FOCUS; import static android.view.HapticFeedbackConstants.SCROLL_LIMIT; import static android.view.HapticFeedbackConstants.SCROLL_TICK; Loading Loading @@ -74,12 +75,13 @@ public final class HapticScrollFeedbackProviderTest { mView = new TestView(InstrumentationRegistry.getContext()); mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* disabledIfViewPlaysScrollHaptics= */ true); /* isFromView= */ false); mSetFlagsRule.disableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); } @Test public void testRotaryEncoder_noFeedbackWhenViewBasedFeedbackIsEnabled() { mSetFlagsRule.disableFlags(FLAG_DYNAMIC_VIEW_ROTARY_HAPTICS_CONFIGURATION); when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) .thenReturn(true); setHapticScrollTickInterval(5); Loading @@ -96,8 +98,25 @@ public final class HapticScrollFeedbackProviderTest { assertNoFeedback(mView); } @Test public void testRotaryEncoder_dynamicViewRotaryFeedback_enabledEvenWhenViewFeedbackIsEnabled() { mSetFlagsRule.enableFlags(FLAG_DYNAMIC_VIEW_ROTARY_HAPTICS_CONFIGURATION); when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) .thenReturn(true); setHapticScrollTickInterval(5); mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* isFromView= */ false); mProvider.onScrollProgress( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 10); assertFeedbackCount(mView, SCROLL_TICK, 1); } @Test public void testRotaryEncoder_inputDeviceCustomized_noFeedbackWhenViewBasedFeedbackIsEnabled() { mSetFlagsRule.disableFlags(FLAG_DYNAMIC_VIEW_ROTARY_HAPTICS_CONFIGURATION); mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) Loading @@ -119,7 +138,7 @@ public final class HapticScrollFeedbackProviderTest { @Test public void testRotaryEncoder_feedbackWhenDisregardingViewBasedScrollHaptics() { mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* disabledIfViewPlaysScrollHaptics= */ false); /* isFromView= */ true); when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) .thenReturn(true); setHapticScrollTickInterval(5); Loading @@ -144,7 +163,7 @@ public final class HapticScrollFeedbackProviderTest { List<HapticFeedbackRequest> requests = new ArrayList<>(); mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* disabledIfViewPlaysScrollHaptics= */ false); /* isFromView= */ true); when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) .thenReturn(true); setHapticScrollTickInterval(5); Loading Loading @@ -917,19 +936,20 @@ public final class HapticScrollFeedbackProviderTest { @Test public void testNonRotaryInputFeedbackNotBlockedByRotaryUnavailability() { mSetFlagsRule.disableFlags(FLAG_DYNAMIC_VIEW_ROTARY_HAPTICS_CONFIGURATION); when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) .thenReturn(true); setHapticScrollFeedbackEnabled(true); setHapticScrollTickInterval(5); mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* disabledIfViewPlaysScrollHaptics= */ true); /* isFromView= */ false); // Expect one feedback here. Touch input should provide feedback since scroll feedback has // been enabled via `setHapticScrollFeedbackEnabled(true)`. mProvider.onScrollProgress( INPUT_DEVICE_1, InputDevice.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_Y, /* deltaInPixels= */ 10); // Because `isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()` is false and // Because `isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()` is true and // `disabledIfViewPlaysScrollHaptics` is true, the scroll progress from rotary encoders will // produce no feedback. mProvider.onScrollProgress( Loading core/tests/coretests/src/android/view/RotaryScrollHapticsTest.java +43 −0 Original line number Diff line number Diff line Loading @@ -30,8 +30,12 @@ import static org.mockito.Mockito.when; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.annotation.Nullable; import android.content.Context; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.view.flags.Flags; import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; Loading @@ -39,6 +43,7 @@ import androidx.test.filters.SmallTest; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; Loading @@ -48,6 +53,8 @@ import org.mockito.Mock; @RunWith(AndroidJUnit4.class) @Presubmit public final class RotaryScrollHapticsTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private static final int TEST_ROTARY_DEVICE_ID = 1; private static final int TEST_RANDOM_DEVICE_ID = 2; Loading Loading @@ -166,6 +173,26 @@ public final class RotaryScrollHapticsTest { verifyNoScrollLimit(); } @Test @EnableFlags(Flags.FLAG_DYNAMIC_VIEW_ROTARY_HAPTICS_CONFIGURATION) public void testChildViewImplementationUsesScrollFeedbackProvider_doesNoScrollFeedback() { mView.configureGenericMotion(/* result= */ false, /* scroll= */ true); mView.mUsesCustomScrollFeedbackProvider = true; // Send multiple generic motion events, to catch bugs where the behavior is WAI only for the // first dispatch, but buggy for future calls. mView.dispatchGenericMotionEvent(createRotaryEvent(20)); mView.dispatchGenericMotionEvent(createRotaryEvent(10)); mView.dispatchGenericMotionEvent(createRotaryEvent(30)); // Verify that the base View class's ScrollFeedbackProvider produces no scroll progress // or limit events, because there's a custom ScrollFeedbackProvider used by the child // View class implementation, which should hint the base View class to disable its own // ScrollFeedbackProvider usage. verifyNoScrollProgress(); verifyNoScrollLimit(); } @Test public void testScrollProgress_genericMotionEventCallbackReturningTrue_doesScrollProgress() { mView.configureGenericMotion(/* result= */ true, /* scroll= */ true); Loading Loading @@ -208,6 +235,9 @@ public final class RotaryScrollHapticsTest { private static final class TestGenericMotionEventControllingView extends View { private boolean mGenericMotionResult; private boolean mScrollOnGenericMotion; private boolean mUsesCustomScrollFeedbackProvider = false; @Nullable private ScrollFeedbackProvider mCustomScrollFeedbackProvider; TestGenericMotionEventControllingView(Context context) { super(context); Loading @@ -222,6 +252,19 @@ public final class RotaryScrollHapticsTest { public boolean onGenericMotionEvent(MotionEvent event) { if (mScrollOnGenericMotion) { scrollTo(100, 200); // scroll values random (not relevant for tests). if (mUsesCustomScrollFeedbackProvider) { // Mimic how a real child class of View would instantiate and use the // ScrollFeedbackProvider API. if (mCustomScrollFeedbackProvider == null) { mCustomScrollFeedbackProvider = ScrollFeedbackProvider.createProvider(this); } float axisScrollValue = event.getAxisValue(AXIS_SCROLL); mCustomScrollFeedbackProvider.onScrollProgress( event.getDeviceId(), event.getSource(), MotionEvent.AXIS_SCROLL, (int) (axisScrollValue * TEST_SCALED_VERTICAL_SCROLL_FACTOR)); } } return mGenericMotionResult; } Loading Loading
core/java/android/view/HapticScrollFeedbackProvider.java +16 −12 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package android.view; import static android.view.flags.Flags.dynamicViewRotaryHapticsConfiguration; import android.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; Loading @@ -41,13 +43,8 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { private final View mView; private final ViewConfiguration mViewConfig; /** * Flag to disable the logic in this class if the View-based scroll haptics implementation is * enabled. If {@code false}, this class will continue to run despite the View's scroll * haptics implementation being enabled. This value should be set to {@code true} when this * class is directly used by the View class. */ private final boolean mDisabledIfViewPlaysScrollHaptics; /** Whether or not this provider is being used directly by the View class. */ private final boolean mIsFromView; // Info about the cause of the latest scroll event. Loading @@ -65,17 +62,23 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { private boolean mHapticScrollFeedbackEnabled = false; public HapticScrollFeedbackProvider(@NonNull View view) { this(view, ViewConfiguration.get(view.getContext()), /* disabledIfViewPlaysScrollHaptics= */ true); this(view, ViewConfiguration.get(view.getContext()), /* isFromView= */ false); } /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public HapticScrollFeedbackProvider( View view, ViewConfiguration viewConfig, boolean disabledIfViewPlaysScrollHaptics) { View view, ViewConfiguration viewConfig, boolean isFromView) { mView = view; mViewConfig = viewConfig; mDisabledIfViewPlaysScrollHaptics = disabledIfViewPlaysScrollHaptics; mIsFromView = isFromView; if (dynamicViewRotaryHapticsConfiguration() && !isFromView) { // Disable the View class's rotary scroll feedback logic if this provider is not being // directly used by the View class. This is to avoid double rotary scroll feedback: // one from the View class, and one from this provider instance (i.e. mute the View // class's rotary feedback and enable this provider). view.disableRotaryScrollFeedback(); } } @Override Loading Loading @@ -151,7 +154,8 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { mAxis = axis; mDeviceId = deviceId; if (mDisabledIfViewPlaysScrollHaptics if (!dynamicViewRotaryHapticsConfiguration() && !mIsFromView && (source == InputDevice.SOURCE_ROTARY_ENCODER) && mViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) { mHapticScrollFeedbackEnabled = false; Loading
core/java/android/view/View.java +21 −5 Original line number Diff line number Diff line Loading @@ -16754,9 +16754,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags4 |= PFLAG4_ROTARY_HAPTICS_DETERMINED; } } final boolean processForRotaryScrollHaptics = isRotaryEncoderEvent && ((mPrivateFlags4 & PFLAG4_ROTARY_HAPTICS_ENABLED) != 0); if (processForRotaryScrollHaptics) { if (isRotaryEncoderEvent && ((mPrivateFlags4 & PFLAG4_ROTARY_HAPTICS_ENABLED) != 0)) { mPrivateFlags4 &= ~PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT; mPrivateFlags4 |= PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT; } Loading @@ -16773,7 +16771,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Process scroll haptics after `onGenericMotionEvent`, since that's where scrolling usually // happens. Some views may return false from `onGenericMotionEvent` even if they have done // scrolling, so disregard the return value when processing for scroll haptics. if (processForRotaryScrollHaptics) { // Check for `PFLAG4_ROTARY_HAPTICS_ENABLED` again, because the View implementation may // call `disableRotaryScrollFeedback` in `onGenericMotionEvent`, which could change the // value of `PFLAG4_ROTARY_HAPTICS_ENABLED`. if (isRotaryEncoderEvent && ((mPrivateFlags4 & PFLAG4_ROTARY_HAPTICS_ENABLED) != 0)) { if ((mPrivateFlags4 & PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT) != 0) { doRotaryProgressForScrollHaptics(event); } else { Loading Loading @@ -18716,7 +18717,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private HapticScrollFeedbackProvider getScrollFeedbackProvider() { if (mScrollFeedbackProvider == null) { mScrollFeedbackProvider = new HapticScrollFeedbackProvider(this, ViewConfiguration.get(mContext), /* disabledIfViewPlaysScrollHaptics= */ false); ViewConfiguration.get(mContext), /* isFromView= */ true); } return mScrollFeedbackProvider; } Loading Loading @@ -18745,6 +18746,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } /** * Disables the rotary scroll feedback implementation of the View class. * * <p>Note that this does NOT disable all rotary scroll feedback; it just disables the logic * implemented within the View class. The child implementation of the View may implement its own * rotary scroll feedback logic or use {@link ScrollFeedbackProvider} to generate rotary scroll * feedback. */ void disableRotaryScrollFeedback() { // Force set PFLAG4_ROTARY_HAPTICS_DETERMINED to avoid recalculating // PFLAG4_ROTARY_HAPTICS_ENABLED under any circumstance. mPrivateFlags4 |= PFLAG4_ROTARY_HAPTICS_DETERMINED; mPrivateFlags4 &= ~PFLAG4_ROTARY_HAPTICS_ENABLED; } /** * This is called in response to an internal scroll in this view (i.e., the * view scrolled its own contents). This is typically as a result of
core/java/android/view/flags/scroll_feedback_flags.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -23,3 +23,10 @@ flag { bug: "331830899" is_fixed_read_only: true } flag { namespace: "wear_frameworks" name: "dynamic_view_rotary_haptics_configuration" description: "Whether ScrollFeedbackProvider dynamically disables View-based rotary haptics." bug: "377998870 " }
core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java +25 −5 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package android.view; import static android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED; import static android.view.flags.Flags.FLAG_DYNAMIC_VIEW_ROTARY_HAPTICS_CONFIGURATION; import static android.view.HapticFeedbackConstants.SCROLL_ITEM_FOCUS; import static android.view.HapticFeedbackConstants.SCROLL_LIMIT; import static android.view.HapticFeedbackConstants.SCROLL_TICK; Loading Loading @@ -74,12 +75,13 @@ public final class HapticScrollFeedbackProviderTest { mView = new TestView(InstrumentationRegistry.getContext()); mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* disabledIfViewPlaysScrollHaptics= */ true); /* isFromView= */ false); mSetFlagsRule.disableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); } @Test public void testRotaryEncoder_noFeedbackWhenViewBasedFeedbackIsEnabled() { mSetFlagsRule.disableFlags(FLAG_DYNAMIC_VIEW_ROTARY_HAPTICS_CONFIGURATION); when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) .thenReturn(true); setHapticScrollTickInterval(5); Loading @@ -96,8 +98,25 @@ public final class HapticScrollFeedbackProviderTest { assertNoFeedback(mView); } @Test public void testRotaryEncoder_dynamicViewRotaryFeedback_enabledEvenWhenViewFeedbackIsEnabled() { mSetFlagsRule.enableFlags(FLAG_DYNAMIC_VIEW_ROTARY_HAPTICS_CONFIGURATION); when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) .thenReturn(true); setHapticScrollTickInterval(5); mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* isFromView= */ false); mProvider.onScrollProgress( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 10); assertFeedbackCount(mView, SCROLL_TICK, 1); } @Test public void testRotaryEncoder_inputDeviceCustomized_noFeedbackWhenViewBasedFeedbackIsEnabled() { mSetFlagsRule.disableFlags(FLAG_DYNAMIC_VIEW_ROTARY_HAPTICS_CONFIGURATION); mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) Loading @@ -119,7 +138,7 @@ public final class HapticScrollFeedbackProviderTest { @Test public void testRotaryEncoder_feedbackWhenDisregardingViewBasedScrollHaptics() { mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* disabledIfViewPlaysScrollHaptics= */ false); /* isFromView= */ true); when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) .thenReturn(true); setHapticScrollTickInterval(5); Loading @@ -144,7 +163,7 @@ public final class HapticScrollFeedbackProviderTest { List<HapticFeedbackRequest> requests = new ArrayList<>(); mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* disabledIfViewPlaysScrollHaptics= */ false); /* isFromView= */ true); when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) .thenReturn(true); setHapticScrollTickInterval(5); Loading Loading @@ -917,19 +936,20 @@ public final class HapticScrollFeedbackProviderTest { @Test public void testNonRotaryInputFeedbackNotBlockedByRotaryUnavailability() { mSetFlagsRule.disableFlags(FLAG_DYNAMIC_VIEW_ROTARY_HAPTICS_CONFIGURATION); when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) .thenReturn(true); setHapticScrollFeedbackEnabled(true); setHapticScrollTickInterval(5); mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* disabledIfViewPlaysScrollHaptics= */ true); /* isFromView= */ false); // Expect one feedback here. Touch input should provide feedback since scroll feedback has // been enabled via `setHapticScrollFeedbackEnabled(true)`. mProvider.onScrollProgress( INPUT_DEVICE_1, InputDevice.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_Y, /* deltaInPixels= */ 10); // Because `isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()` is false and // Because `isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()` is true and // `disabledIfViewPlaysScrollHaptics` is true, the scroll progress from rotary encoders will // produce no feedback. mProvider.onScrollProgress( Loading
core/tests/coretests/src/android/view/RotaryScrollHapticsTest.java +43 −0 Original line number Diff line number Diff line Loading @@ -30,8 +30,12 @@ import static org.mockito.Mockito.when; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.annotation.Nullable; import android.content.Context; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.view.flags.Flags; import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; Loading @@ -39,6 +43,7 @@ import androidx.test.filters.SmallTest; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; Loading @@ -48,6 +53,8 @@ import org.mockito.Mock; @RunWith(AndroidJUnit4.class) @Presubmit public final class RotaryScrollHapticsTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private static final int TEST_ROTARY_DEVICE_ID = 1; private static final int TEST_RANDOM_DEVICE_ID = 2; Loading Loading @@ -166,6 +173,26 @@ public final class RotaryScrollHapticsTest { verifyNoScrollLimit(); } @Test @EnableFlags(Flags.FLAG_DYNAMIC_VIEW_ROTARY_HAPTICS_CONFIGURATION) public void testChildViewImplementationUsesScrollFeedbackProvider_doesNoScrollFeedback() { mView.configureGenericMotion(/* result= */ false, /* scroll= */ true); mView.mUsesCustomScrollFeedbackProvider = true; // Send multiple generic motion events, to catch bugs where the behavior is WAI only for the // first dispatch, but buggy for future calls. mView.dispatchGenericMotionEvent(createRotaryEvent(20)); mView.dispatchGenericMotionEvent(createRotaryEvent(10)); mView.dispatchGenericMotionEvent(createRotaryEvent(30)); // Verify that the base View class's ScrollFeedbackProvider produces no scroll progress // or limit events, because there's a custom ScrollFeedbackProvider used by the child // View class implementation, which should hint the base View class to disable its own // ScrollFeedbackProvider usage. verifyNoScrollProgress(); verifyNoScrollLimit(); } @Test public void testScrollProgress_genericMotionEventCallbackReturningTrue_doesScrollProgress() { mView.configureGenericMotion(/* result= */ true, /* scroll= */ true); Loading Loading @@ -208,6 +235,9 @@ public final class RotaryScrollHapticsTest { private static final class TestGenericMotionEventControllingView extends View { private boolean mGenericMotionResult; private boolean mScrollOnGenericMotion; private boolean mUsesCustomScrollFeedbackProvider = false; @Nullable private ScrollFeedbackProvider mCustomScrollFeedbackProvider; TestGenericMotionEventControllingView(Context context) { super(context); Loading @@ -222,6 +252,19 @@ public final class RotaryScrollHapticsTest { public boolean onGenericMotionEvent(MotionEvent event) { if (mScrollOnGenericMotion) { scrollTo(100, 200); // scroll values random (not relevant for tests). if (mUsesCustomScrollFeedbackProvider) { // Mimic how a real child class of View would instantiate and use the // ScrollFeedbackProvider API. if (mCustomScrollFeedbackProvider == null) { mCustomScrollFeedbackProvider = ScrollFeedbackProvider.createProvider(this); } float axisScrollValue = event.getAxisValue(AXIS_SCROLL); mCustomScrollFeedbackProvider.onScrollProgress( event.getDeviceId(), event.getSource(), MotionEvent.AXIS_SCROLL, (int) (axisScrollValue * TEST_SCALED_VERTICAL_SCROLL_FACTOR)); } } return mGenericMotionResult; } Loading