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

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

Merge changes Ifaeb89bd,If502a4ac into main

* changes:
  Rotary encoder scroll haptics in View class
  Add ViewConfigurationPerfTest
parents 93e28906 616806e5
Loading
Loading
Loading
Loading
+61 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.view;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import android.content.Context;

import androidx.benchmark.BenchmarkState;
import androidx.benchmark.junit4.BenchmarkRule;
import androidx.test.filters.SmallTest;

import org.junit.Rule;
import org.junit.Test;

@SmallTest
public class ViewConfigurationPerfTest {
    @Rule
    public final BenchmarkRule mBenchmarkRule = new BenchmarkRule();

    private final Context mContext = getInstrumentation().getTargetContext();

    @Test
    public void testGet_newViewConfiguration() {
        final BenchmarkState state = mBenchmarkRule.getState();

        while (state.keepRunning()) {
            state.pauseTiming();
            // Reset cache so that `ViewConfiguration#get` creates a new instance.
            ViewConfiguration.resetCacheForTesting();
            state.resumeTiming();

            ViewConfiguration.get(mContext);
        }
    }

    @Test
    public void testGet_cachedViewConfiguration() {
        final BenchmarkState state = mBenchmarkRule.getState();
        // Do `get` once to make sure there's something cached.
        ViewConfiguration.get(mContext);

        while (state.keepRunning()) {
            ViewConfiguration.get(mContext);
        }
    }
}
+21 −4
Original line number Original line Diff line number Diff line
@@ -47,9 +47,17 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider {
    public @interface HapticScrollFeedbackAxis {}
    public @interface HapticScrollFeedbackAxis {}


    private static final int TICK_INTERVAL_NO_TICK = 0;
    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 View mView;
    private final ViewConfiguration mViewConfig;
    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.
    // 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. */
    /** The tick interval corresponding to the current InputDevice/source/axis. */
    private int mTickIntervalPixels = TICK_INTERVAL_NO_TICK;
    private int mTickIntervalPixels = TICK_INTERVAL_NO_TICK;
    private int mTotalScrollPixels = 0;
    private int mTotalScrollPixels = 0;
    private boolean mCanPlayLimitFeedback = true;
    private boolean mCanPlayLimitFeedback = INITIAL_END_OF_LIST_HAPTICS_ENABLED;
    private boolean mHapticScrollFeedbackEnabled = false;
    private boolean mHapticScrollFeedbackEnabled = false;


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


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


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


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


            mHapticScrollFeedbackEnabled =
            mHapticScrollFeedbackEnabled =
                    mViewConfig.isHapticScrollFeedbackEnabled(deviceId, axis, source);
                    mViewConfig.isHapticScrollFeedbackEnabled(deviceId, axis, source);
            mCanPlayLimitFeedback = true;
            mCanPlayLimitFeedback = INITIAL_END_OF_LIST_HAPTICS_ENABLED;
            mTotalScrollPixels = 0;
            mTotalScrollPixels = 0;
            updateTickIntervals(deviceId, source, axis);
            updateTickIntervals(deviceId, source, axis);
        }
        }
+93 −1
Original line number Original line Diff line number Diff line
@@ -919,6 +919,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     */
     */
    private static boolean sCompatibilityDone = false;
    private static boolean sCompatibilityDone = false;
    /** @hide */
    public HapticScrollFeedbackProvider mScrollFeedbackProvider = null;
    /**
    /**
     * Use the old (broken) way of building MeasureSpecs.
     * 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_IMPORTANT_FOR_CREDENTIAL_MANAGER
     *              1                   PFLAG4_TRAVERSAL_TRACING_ENABLED
     *              1                   PFLAG4_TRAVERSAL_TRACING_ENABLED
     *             1                    PFLAG4_RELAYOUT_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;
    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 */
    /* End of masks for mPrivateFlags4 */
    /** @hide */
    /** @hide */
@@ -15894,6 +15919,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
    }
    }
    private boolean dispatchGenericMotionEventInternal(MotionEvent event) {
    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
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnGenericMotionListener != null
        if (li != null && li.mOnGenericMotionListener != null
@@ -15902,7 +15949,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
            return true;
            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;
            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
     * 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
     * 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();
        notifySubtreeAccessibilityStateChangedIfNeeded();
        postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt);
        postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt);
        processScrollEventForRotaryEncoderHaptics();
        mBackgroundSizeChanged = true;
        mBackgroundSizeChanged = true;
        mDefaultFocusHighlightSizeChanged = true;
        mDefaultFocusHighlightSizeChanged = true;
        if (mForegroundInfo != null) {
        if (mForegroundInfo != null) {
+49 −2
Original line number Original line Diff line number Diff line
@@ -40,6 +40,8 @@ import android.util.SparseArray;
import android.util.TypedValue;
import android.util.TypedValue;
import android.view.flags.Flags;
import android.view.flags.Flags;


import com.android.internal.annotations.VisibleForTesting;

/**
/**
 * Contains methods to standard constants used in the UI for timeouts, sizes, and distances.
 * Contains methods to standard constants used in the UI for timeouts, sizes, and distances.
 */
 */
@@ -375,6 +377,7 @@ public class ViewConfiguration {
    private final int mSmartSelectionInitializedTimeout;
    private final int mSmartSelectionInitializedTimeout;
    private final int mSmartSelectionInitializingTimeout;
    private final int mSmartSelectionInitializingTimeout;
    private final boolean mPreferKeepClearForFocusEnabled;
    private final boolean mPreferKeepClearForFocusEnabled;
    private final boolean mViewBasedRotaryEncoderScrollHapticsEnabledConfig;


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


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


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


        ViewConfiguration configuration = sConfigurations.get(density);
        ViewConfiguration configuration = sConfigurations.get(density);
        if (configuration == null) {
        if (configuration == null) {
@@ -602,6 +608,28 @@ public class ViewConfiguration {
        return configuration;
        return configuration;
    }
    }


    /**
     * Removes cached ViewConfiguration instances, so that we can ensure `get` constructs a new
     * ViewConfiguration instance. This is useful for testing the behavior and performance of
     * creating ViewConfiguration the first time.
     *
     * @hide
     */
    @VisibleForTesting
    public static void resetCacheForTesting() {
        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
     * @return The width of the horizontal scrollbar and the height of the vertical
     *         scrollbar in dips
     *         scrollbar in dips
@@ -1311,6 +1339,20 @@ public class ViewConfiguration {
        return NO_HAPTIC_SCROLL_TICK_INTERVAL;
        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) {
    private static boolean isInputDeviceInfoValid(int id, int axis, int source) {
        InputDevice device = InputManagerGlobal.getInstance().getInputDevice(id);
        InputDevice device = InputManagerGlobal.getInstance().getInputDevice(id);
        return device != null && device.getMotionRange(axis, source) != null;
        return device != null && device.getMotionRange(axis, source) != null;
@@ -1420,4 +1462,9 @@ public class ViewConfiguration {
    public static int getHoverTooltipHideShortTimeout() {
    public static int getHoverTooltipHideShortTimeout() {
        return HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT;
        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 Original line Diff line number Diff line
@@ -6,3 +6,10 @@ flag {
    description: "Enable the scroll feedback APIs"
    description: "Enable the scroll feedback APIs"
    bug: "239594271"
    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
Loading