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

Commit f28ac6cc authored by Yeabkal Wubshit's avatar Yeabkal Wubshit Committed by Android (Google) Code Review
Browse files

Merge "Dynamically disable View-based rotary haptics" into main

parents ebf24d9c bf0dd5d7
Loading
Loading
Loading
Loading
+16 −12
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.view;

import static android.view.flags.Flags.dynamicViewRotaryHapticsConfiguration;

import android.annotation.NonNull;

import com.android.internal.annotations.VisibleForTesting;
@@ -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.
@@ -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
@@ -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;
+21 −5
Original line number Diff line number Diff line
@@ -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;
        }
@@ -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 {
@@ -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;
    }
@@ -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
+7 −0
Original line number Diff line number Diff line
@@ -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 "
}
+25 −5
Original line number Diff line number Diff line
@@ -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;
@@ -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);
@@ -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())
@@ -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);
@@ -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);
@@ -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(
+43 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;

@@ -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);
@@ -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);
@@ -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;
        }