Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 616806e5 authored by Yeabkal Wubshit's avatar Yeabkal Wubshit
Browse files

Rotary encoder scroll haptics in View class

This change implements scroll haptics in View class for rotary encoders.
The View class implementation allows to avoid regression for devices
which have had rotary scroll haptics in forks of previous Android
releases.
The long term plan for scroll haptics is to use the
ScrollFeedbackProvider in widgets. As such, this implementation will
eventually be removed in future Android releases once the
ScrollFeedbackProvider has been well integrated with major widgets.

We have also fixed a bug in HapticScrollFeedbackProvider, which was
necessary in using it for View-based rotary haptics. The bug was that it
was allowing scroll-limit haptics without any non-limit scroll events.
Tests have accordingly been adjusted.

Bug: 299587011
Test: manual, unit tests

Change-Id: Ifaeb89bd5bdc8c806c718d4aa8087cc0d2bf3ae5
parent 668e1749
Loading
Loading
Loading
Loading
+21 −4
Original line number Diff line number Diff line
@@ -47,9 +47,17 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider {
    public @interface HapticScrollFeedbackAxis {}

    private static final int TICK_INTERVAL_NO_TICK = 0;
    private static final boolean INITIAL_END_OF_LIST_HAPTICS_ENABLED = false;

    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;


    // Info about the cause of the latest scroll event.
@@ -63,18 +71,21 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider {
    /** 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 mCanPlayLimitFeedback = INITIAL_END_OF_LIST_HAPTICS_ENABLED;
    private boolean mHapticScrollFeedbackEnabled = false;

    public HapticScrollFeedbackProvider(@NonNull View view) {
        this(view, ViewConfiguration.get(view.getContext()));
        this(view, ViewConfiguration.get(view.getContext()),
                /* disabledIfViewPlaysScrollHaptics= */ true);
    }

    /** @hide */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public HapticScrollFeedbackProvider(View view, ViewConfiguration viewConfig) {
    public HapticScrollFeedbackProvider(
            View view, ViewConfiguration viewConfig, boolean disabledIfViewPlaysScrollHaptics) {
        mView = view;
        mViewConfig = viewConfig;
        mDisabledIfViewPlaysScrollHaptics = disabledIfViewPlaysScrollHaptics;
    }

    @Override
@@ -136,13 +147,19 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider {

    private void maybeUpdateCurrentConfig(int deviceId, int source, int axis) {
        if (mAxis != axis || mSource != source || mDeviceId != deviceId) {
            if (mDisabledIfViewPlaysScrollHaptics
                    && (source == InputDevice.SOURCE_ROTARY_ENCODER)
                    && mViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) {
                mHapticScrollFeedbackEnabled = false;
                return;
            }
            mSource = source;
            mAxis = axis;
            mDeviceId = deviceId;

            mHapticScrollFeedbackEnabled =
                    mViewConfig.isHapticScrollFeedbackEnabled(deviceId, axis, source);
            mCanPlayLimitFeedback = true;
            mCanPlayLimitFeedback = INITIAL_END_OF_LIST_HAPTICS_ENABLED;
            mTotalScrollPixels = 0;
            updateTickIntervals(deviceId, source, axis);
        }
+93 −1
Original line number Diff line number Diff line
@@ -919,6 +919,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     */
    private static boolean sCompatibilityDone = false;
    /** @hide */
    public HapticScrollFeedbackProvider mScrollFeedbackProvider = null;
    /**
     * Use the old (broken) way of building MeasureSpecs.
     */
@@ -3605,6 +3608,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     *               1                  PFLAG4_IMPORTANT_FOR_CREDENTIAL_MANAGER
     *              1                   PFLAG4_TRAVERSAL_TRACING_ENABLED
     *             1                    PFLAG4_RELAYOUT_TRACING_ENABLED
     *            1                     PFLAG4_ROTARY_HAPTICS_DETERMINED
     *           1                      PFLAG4_ROTARY_HAPTICS_ENABLED
     *          1                       PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT
     *         1                        PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT
     * |-------|-------|-------|-------|
     */
@@ -3703,6 +3710,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     */
    private static final int PFLAG4_RELAYOUT_TRACING_ENABLED = 0x000080000;
    /** Indicates if rotary scroll haptics support for the view has been determined. */
    private static final int PFLAG4_ROTARY_HAPTICS_DETERMINED = 0x100000;
    /**
     * Indicates if rotary scroll haptics is enabled for this view.
     * The source of truth for this info is a ViewConfiguration API; this bit only caches the value.
     */
    private static final int PFLAG4_ROTARY_HAPTICS_ENABLED = 0x200000;
    /** Indicates if there has been a scroll event since the last rotary input. */
    private static final int PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT = 0x400000;
    /**
     * Indicates if there has been a rotary input that may generate a scroll event.
     * This flag is important so that a scroll event can be properly attributed to a rotary input.
     */
    private static final int PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT = 0x800000;
    /* End of masks for mPrivateFlags4 */
    /** @hide */
@@ -15894,6 +15919,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
    }
    private boolean dispatchGenericMotionEventInternal(MotionEvent event) {
        final boolean isRotaryEncoderEvent = event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER);
        if (isRotaryEncoderEvent) {
            // Determine and cache rotary scroll haptics support if it's not yet determined.
            // Caching the support is important for two reasons:
            // 1) Limits call to `ViewConfiguration#get`, which we should avoid if possible.
            // 2) Limits latency from the `ViewConfiguration` API, which may be slow due to feature
            //    flag querying.
            if ((mPrivateFlags4 & PFLAG4_ROTARY_HAPTICS_DETERMINED) == 0) {
                if (ViewConfiguration.get(mContext)
                        .isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) {
                    mPrivateFlags4 |= PFLAG4_ROTARY_HAPTICS_ENABLED;
                }
                mPrivateFlags4 |= PFLAG4_ROTARY_HAPTICS_DETERMINED;
            }
        }
        final boolean processForRotaryScrollHaptics =
                isRotaryEncoderEvent && ((mPrivateFlags4 & PFLAG4_ROTARY_HAPTICS_ENABLED) != 0);
        if (processForRotaryScrollHaptics) {
            mPrivateFlags4 &= ~PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT;
            mPrivateFlags4 |= PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnGenericMotionListener != null
@@ -15902,7 +15949,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
            return true;
        }
        if (onGenericMotionEvent(event)) {
        final boolean onGenericMotionEventResult = onGenericMotionEvent(event);
        // 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) {
            if ((mPrivateFlags4 & PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT) != 0) {
                doRotaryProgressForScrollHaptics(event);
            } else {
                doRotaryLimitForScrollHaptics(event);
            }
        }
        if (onGenericMotionEventResult) {
            return true;
        }
@@ -17783,6 +17841,38 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        }
    }
    private HapticScrollFeedbackProvider getScrollFeedbackProvider() {
        if (mScrollFeedbackProvider == null) {
            mScrollFeedbackProvider = new HapticScrollFeedbackProvider(this,
                    ViewConfiguration.get(mContext), /* disabledIfViewPlaysScrollHaptics= */ false);
        }
        return mScrollFeedbackProvider;
    }
    private void doRotaryProgressForScrollHaptics(MotionEvent rotaryEvent) {
        final float axisScrollValue = rotaryEvent.getAxisValue(MotionEvent.AXIS_SCROLL);
        final float verticalScrollFactor =
                ViewConfiguration.get(mContext).getScaledVerticalScrollFactor();
        final int scrollAmount = -Math.round(axisScrollValue * verticalScrollFactor);
        getScrollFeedbackProvider().onScrollProgress(
                rotaryEvent.getDeviceId(), InputDevice.SOURCE_ROTARY_ENCODER,
                MotionEvent.AXIS_SCROLL, scrollAmount);
    }
    private void doRotaryLimitForScrollHaptics(MotionEvent rotaryEvent) {
        final boolean isStart = rotaryEvent.getAxisValue(MotionEvent.AXIS_SCROLL) > 0;
        getScrollFeedbackProvider().onScrollLimit(
                rotaryEvent.getDeviceId(), InputDevice.SOURCE_ROTARY_ENCODER,
                MotionEvent.AXIS_SCROLL, isStart);
    }
    private void processScrollEventForRotaryEncoderHaptics() {
        if ((mPrivateFlags4 |= PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT) != 0) {
            mPrivateFlags4 |= PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT;
            mPrivateFlags4 &= ~PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT;
        }
    }
    /**
     * 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
@@ -17798,6 +17888,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        notifySubtreeAccessibilityStateChangedIfNeeded();
        postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt);
        processScrollEventForRotaryEncoderHaptics();
        mBackgroundSizeChanged = true;
        mDefaultFocusHighlightSizeChanged = true;
        if (mForegroundInfo != null) {
+35 −2
Original line number Diff line number Diff line
@@ -377,6 +377,7 @@ public class ViewConfiguration {
    private final int mSmartSelectionInitializedTimeout;
    private final int mSmartSelectionInitializingTimeout;
    private final boolean mPreferKeepClearForFocusEnabled;
    private final boolean mViewBasedRotaryEncoderScrollHapticsEnabledConfig;

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768915)
    private boolean sHasPermanentMenuKey;
@@ -401,6 +402,7 @@ public class ViewConfiguration {
        mMaximumRotaryEncoderFlingVelocity = MAXIMUM_FLING_VELOCITY;
        mRotaryEncoderHapticScrollFeedbackEnabled = false;
        mRotaryEncoderHapticScrollFeedbackTickIntervalPixels = NO_HAPTIC_SCROLL_TICK_INTERVAL;
        mViewBasedRotaryEncoderScrollHapticsEnabledConfig = false;
        mScrollbarSize = SCROLL_BAR_SIZE;
        mTouchSlop = TOUCH_SLOP;
        mHandwritingSlop = HANDWRITING_SLOP;
@@ -577,6 +579,9 @@ public class ViewConfiguration {
                com.android.internal.R.integer.config_smartSelectionInitializingTimeoutMillis);
        mPreferKeepClearForFocusEnabled = res.getBoolean(
                com.android.internal.R.bool.config_preferKeepClearForFocus);
        mViewBasedRotaryEncoderScrollHapticsEnabledConfig =
                res.getBoolean(
                        com.android.internal.R.bool.config_viewBasedRotaryEncoderHapticsEnabled);
    }

    /**
@@ -592,8 +597,7 @@ public class ViewConfiguration {
    public static ViewConfiguration get(@NonNull @UiContext Context context) {
        StrictMode.assertConfigurationContext(context, "ViewConfiguration");

        final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        final int density = (int) (100.0f * metrics.density);
        final int density = getDisplayDensity(context);

        ViewConfiguration configuration = sConfigurations.get(density);
        if (configuration == null) {
@@ -616,6 +620,16 @@ public class ViewConfiguration {
        sConfigurations.clear();
    }

    /**
     * Sets the ViewConfiguration cached instanc for a given Context for testing.
     *
     * @hide
     */
    @VisibleForTesting
    public static void setInstanceForTesting(Context context, ViewConfiguration instance) {
        sConfigurations.put(getDisplayDensity(context), instance);
    }

    /**
     * @return The width of the horizontal scrollbar and the height of the vertical
     *         scrollbar in dips
@@ -1325,6 +1339,20 @@ public class ViewConfiguration {
        return NO_HAPTIC_SCROLL_TICK_INTERVAL;
    }

    /**
     * Checks if the View-based haptic scroll feedback implementation is enabled for
     * {@link InputDevice#SOURCE_ROTARY_ENCODER}s.
     *
     * <p>If this method returns {@code true}, the {@link HapticScrollFeedbackProvider} will be
     * muted for rotary encoders in favor of View's scroll haptics implementation.
     *
     * @hide
     */
    public boolean isViewBasedRotaryEncoderHapticScrollFeedbackEnabled() {
        return mViewBasedRotaryEncoderScrollHapticsEnabledConfig
                && Flags.useViewBasedRotaryEncoderScrollHaptics();
    }

    private static boolean isInputDeviceInfoValid(int id, int axis, int source) {
        InputDevice device = InputManagerGlobal.getInstance().getInputDevice(id);
        return device != null && device.getMotionRange(axis, source) != null;
@@ -1434,4 +1462,9 @@ public class ViewConfiguration {
    public static int getHoverTooltipHideShortTimeout() {
        return HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT;
    }

    private static final int getDisplayDensity(Context context) {
        final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        return (int) (100.0f * metrics.density);
    }
}
+7 −0
Original line number Diff line number Diff line
@@ -6,3 +6,10 @@ flag {
    description: "Enable the scroll feedback APIs"
    bug: "239594271"
}

flag {
    namespace: "toolkit"
    name: "use_view_based_rotary_encoder_scroll_haptics"
    description: "If enabled, the rotary encoder scroll haptic implementation in the View class will be used, and the HapticScrollFeedbackProvider logic for rotary encoder haptic will be muted."
    bug: "299587011"
}
 No newline at end of file
+3 −0
Original line number Diff line number Diff line
@@ -6711,4 +6711,7 @@
         {@link MotionEvent#AXIS_SCROLL} generated by {@link InputDevice#SOURCE_ROTARY_ENCODER}
         devices. -->
    <bool name="config_viewRotaryEncoderHapticScrollFedbackEnabled">false</bool>
    <!-- Whether the View-based scroll haptic feedback implementation is enabled for
         {@link InputDevice#SOURCE_ROTARY_ENCODER}s. -->
    <bool name="config_viewBasedRotaryEncoderHapticsEnabled">false</bool>
</resources>
Loading