/*
 * Copyright (C) 2006 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 android.annotation.FlaggedApi;
import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.TestApi;
import android.annotation.UiContext;
import android.app.Activity;
import android.app.ActivityThread;
import android.app.AppGlobals;
import android.app.Application;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.hardware.input.InputManagerGlobal;
import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.StrictMode;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.LongSparseArray;
import android.util.TypedValue;
import android.view.flags.Flags;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;

/**
 * Contains methods to standard constants used in the UI for timeouts, sizes, and distances.
 */
public class ViewConfiguration {
    /**
     * Defines the width of the horizontal scrollbar and the height of the vertical scrollbar in
     * dips
     */
    private static final int SCROLL_BAR_SIZE = 4;

    /**
     * Duration of the fade when scrollbars fade away in milliseconds
     */
    private static final int SCROLL_BAR_FADE_DURATION = 250;

    /**
     * Default delay before the scrollbars fade in milliseconds
     */
    private static final int SCROLL_BAR_DEFAULT_DELAY = 300;

    /**
     * Defines the length of the fading edges in dips
     */
    private static final int FADING_EDGE_LENGTH = 12;

    /**
     * Defines the duration in milliseconds of the pressed state in child
     * components.
     */
    private static final int PRESSED_STATE_DURATION = 64;

    /**
     * Defines the default duration in milliseconds before a press turns into
     * a long press
     * @hide
     */
    public static final int DEFAULT_LONG_PRESS_TIMEOUT = 400;

    /**
     * Defines the default duration in milliseconds between the first tap's up event and the second
     * tap's down event for an interaction to be considered part of the same multi-press.
     */
    private static final int DEFAULT_MULTI_PRESS_TIMEOUT = 300;

    /**
     * Defines the default duration in milliseconds between a key being pressed and its first key
     * repeat event being generated. Historically, Android used the long press timeout as the
     * key repeat timeout, so its default value is set to long press timeout's default.
     */
    private static final int DEFAULT_KEY_REPEAT_TIMEOUT_MS = DEFAULT_LONG_PRESS_TIMEOUT;

    /**
     * Defines the default duration between successive key repeats in milliseconds.
     */
    private static final int DEFAULT_KEY_REPEAT_DELAY_MS = 50;

    /**
     * Defines the duration in milliseconds a user needs to hold down the
     * appropriate button to bring up the global actions dialog (power off,
     * lock screen, etc).
     */
    private static final int GLOBAL_ACTIONS_KEY_TIMEOUT = 500;

    /**
     * Defines the duration in milliseconds a user needs to hold down the
     * appropriate buttons (power + volume down) to trigger the screenshot chord.
     */
    private static final int SCREENSHOT_CHORD_KEY_TIMEOUT = 0;

    /**
     * Defines the duration in milliseconds a user needs to hold down the
     * appropriate button to bring up the accessibility shortcut for the first time
     */
    private static final int A11Y_SHORTCUT_KEY_TIMEOUT = 3000;

    /**
     * Defines the duration in milliseconds a user needs to hold down the
     * appropriate button to enable the accessibility shortcut once it's configured.
     */
    private static final int A11Y_SHORTCUT_KEY_TIMEOUT_AFTER_CONFIRMATION = 1000;

    /**
     * Defines the duration in milliseconds we will wait to see if a touch event
     * is a tap or a scroll. If the user does not move within this interval, it is
     * considered to be a tap.
     */
    private static final int TAP_TIMEOUT = 100;

    /**
     * Defines the duration in milliseconds we will wait to see if a touch event
     * is a jump tap. If the user does not complete the jump tap within this interval, it is
     * considered to be a tap.
     */
    private static final int JUMP_TAP_TIMEOUT = 500;

    /**
     * Defines the duration in milliseconds between the first tap's up event and
     * the second tap's down event for an interaction to be considered a
     * double-tap.
     */
    private static final int DOUBLE_TAP_TIMEOUT = 300;

    /**
     * Defines the minimum duration in milliseconds between the first tap's up event and
     * the second tap's down event for an interaction to be considered a
     * double-tap.
     */
    private static final int DOUBLE_TAP_MIN_TIME = 40;

    /**
     * Defines the maximum distance in pixels that a touch pad touch can move
     * before being released for it to be considered a tap (click) as opposed
     * to a hover movement gesture.
     */
    private static final int HOVER_TAP_SLOP = 20;

    /**
     * Defines the duration in milliseconds we want to display zoom controls in response
     * to a user panning within an application.
     */
    private static final int ZOOM_CONTROLS_TIMEOUT = 3000;

    /**
     * Inset in dips to look for touchable content when the user touches the edge of the screen
     */
    private static final int EDGE_SLOP = 12;

    /**
     * Distance a touch can wander before we think the user is scrolling in dips.
     * Note that this value defined here is only used as a fallback by legacy/misbehaving
     * applications that do not provide a Context for determining density/configuration-dependent
     * values.
     *
     * To alter this value, see the configuration resource config_viewConfigurationTouchSlop
     * in frameworks/base/core/res/res/values/config.xml or the appropriate device resource overlay.
     * It may be appropriate to tweak this on a device-specific basis in an overlay based on
     * the characteristics of the touch panel and firmware.
     */
    private static final int TOUCH_SLOP = 8;

    /** Distance a stylus touch can wander before we think the user is handwriting in dips. */
    private static final int HANDWRITING_SLOP = 2;

    /**
     * Defines the minimum size of the touch target for a scrollbar in dips
     */
    private static final int MIN_SCROLLBAR_TOUCH_TARGET = 48;

    /**
     * Distance the first touch can wander before we stop considering this event a double tap
     * (in dips)
     */
    private static final int DOUBLE_TAP_TOUCH_SLOP = TOUCH_SLOP;

    /**
     * Distance a touch can wander before we think the user is attempting a paged scroll
     * (in dips)
     *
     * Note that this value defined here is only used as a fallback by legacy/misbehaving
     * applications that do not provide a Context for determining density/configuration-dependent
     * values.
     *
     * See the note above on {@link #TOUCH_SLOP} regarding the dimen resource
     * config_viewConfigurationTouchSlop. ViewConfiguration will report a paging touch slop of
     * config_viewConfigurationTouchSlop * 2 when provided with a Context.
     */
    private static final int PAGING_TOUCH_SLOP = TOUCH_SLOP * 2;

    /**
     * Distance in dips between the first touch and second touch to still be considered a double tap
     */
    private static final int DOUBLE_TAP_SLOP = 100;

    /**
     * Distance in dips a touch needs to be outside of a window's bounds for it to
     * count as outside for purposes of dismissing the window.
     */
    private static final int WINDOW_TOUCH_SLOP = 16;

    /**
     * Margin in dips around text line bounds where stylus handwriting gestures should be supported.
     */
    private static final int HANDWRITING_GESTURE_LINE_MARGIN = 16;

    /**
     * Minimum velocity to initiate a fling, as measured in dips per second
     */
    private static final int MINIMUM_FLING_VELOCITY = 50;

    /**
     * Maximum velocity to initiate a fling, as measured in dips per second
     */
    private static final int MAXIMUM_FLING_VELOCITY = 8000;

    /** Value used as a minimum fling velocity, when fling is not supported. */
    private static final int NO_FLING_MIN_VELOCITY = Integer.MAX_VALUE;

    /** 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
     * during the {@link #SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS} time
     * frame.
     */
    private static final long SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS = 100;

    /**
     * The maximum size of View's drawing cache, expressed in bytes. This size
     * should be at least equal to the size of the screen in ARGB888 format.
     */
    @Deprecated
    private static final int MAXIMUM_DRAWING_CACHE_SIZE = 480 * 800 * 4; // ARGB8888

    /**
     * The coefficient of friction applied to flings/scrolls.
     */
    @UnsupportedAppUsage
    private static final float SCROLL_FRICTION = 0.015f;

    /**
     * Max distance in dips to overscroll for edge effects
     */
    private static final int OVERSCROLL_DISTANCE = 0;

    /**
     * Max distance in dips to overfling for edge effects
     */
    private static final int OVERFLING_DISTANCE = 6;

    /**
     * Amount to scroll in response to a horizontal {@link MotionEvent#ACTION_SCROLL} event,
     * in dips per axis value.
     */
    private static final float HORIZONTAL_SCROLL_FACTOR = 64;

    /**
     * Amount to scroll in response to a vertical {@link MotionEvent#ACTION_SCROLL} event,
     * in dips per axis value.
     */
    private static final float VERTICAL_SCROLL_FACTOR = 64;

    /**
     * Default duration to hide an action mode for.
     */
    private static final long ACTION_MODE_HIDE_DURATION_DEFAULT = 2000;

    /**
     * Defines the duration in milliseconds before an end of a long press causes a tooltip to be
     * hidden.
     */
    private static final int LONG_PRESS_TOOLTIP_HIDE_TIMEOUT = 1500;

    /**
     * Defines the duration in milliseconds before a hover event causes a tooltip to be shown.
     */
    private static final int HOVER_TOOLTIP_SHOW_TIMEOUT = 500;

    /**
     * Defines the duration in milliseconds before mouse inactivity causes a tooltip to be hidden.
     * (default variant to be used when {@link View#SYSTEM_UI_FLAG_LOW_PROFILE} is not set).
     */
    private static final int HOVER_TOOLTIP_HIDE_TIMEOUT = 15000;

    /**
     * Defines the duration in milliseconds before mouse inactivity causes a tooltip to be hidden
     * (short version to be used when {@link View#SYSTEM_UI_FLAG_LOW_PROFILE} is set).
     */
    private static final int HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT = 3000;

    /**
     * Configuration values for overriding {@link #hasPermanentMenuKey()} behavior.
     * These constants must match the definition in res/values/config.xml.
     */
    private static final int HAS_PERMANENT_MENU_KEY_AUTODETECT = 0;
    private static final int HAS_PERMANENT_MENU_KEY_TRUE = 1;
    private static final int HAS_PERMANENT_MENU_KEY_FALSE = 2;

    /**
     * The multiplication factor for inhibiting default gestures.
     */
    private static final float AMBIGUOUS_GESTURE_MULTIPLIER = 2f;

    /**
     * The timeout value in milliseconds to adjust the selection span and actions for the selected
     * text when TextClassifier has been initialized.
     */
    private static final int SMART_SELECTION_INITIALIZED_TIMEOUT_IN_MILLISECOND = 200;

    /**
     * The timeout value in milliseconds to adjust the selection span and actions for the selected
     * text when TextClassifier has not been initialized.
     */
    private static final int SMART_SELECTION_INITIALIZING_TIMEOUT_IN_MILLISECOND = 500;

    /**
     * The default number of milliseconds required to invert the text cursor's pixels.
     *
     * Keep in sync with core/res/res/values/integers.xml.
     *
     * @hide
     */
    public static final int DEFAULT_TEXT_CURSOR_BLINK_INTERVAL_MS = 500;

    /**
     * The minimum number of milliseconds required to invert the text cursor's pixels that is
     * comfortable for users with photosensitivity.
     *
     * Keep in sync with core/res/res/values/arrays.xml.
     *
     * @hide
     */
    public static final int MIN_TEXT_CURSOR_BLINK_INTERVAL_MS = 333;

    /**
     * The number of milliseconds indicating a text cursor's pixels are never inverted, i.e. the
     * text cursor does not blink at all.
     *
     * Keep in sync with core/res/res/values/integers.xml.
     *
     * @hide
     */
    public static final int NO_BLINK_TEXT_CURSOR_BLINK_INTERVAL_MS = 0;

    private static ResourceCache sResourceCache = new ResourceCache();

    private final boolean mConstructedWithContext;
    private final int mEdgeSlop;
    private final int mFadingEdgeLength;
    private final int mMinimumFlingVelocity;
    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;
    private final int mMinScalingSpan;
    private final int mHoverSlop;
    private final int mMinScrollbarTouchTarget;
    private final int mDoubleTapTouchSlop;
    private final int mPagingTouchSlop;
    private final int mDoubleTapSlop;
    private final int mWindowTouchSlop;
    private final int mHandwritingGestureLineMargin;
    private final float mAmbiguousGestureMultiplier;
    private final int mMaximumDrawingCacheSize;
    private final int mOverscrollDistance;
    private final int mOverflingDistance;
    private final boolean mViewTouchScreenHapticScrollFeedbackEnabled;
    private final boolean mFadingMarqueeEnabled;
    private final long mGlobalActionsKeyTimeout;
    private final float mVerticalScrollFactor;
    private final float mHorizontalScrollFactor;
    private final boolean mShowMenuShortcutsWhenKeyboardPresent;
    private final long mScreenshotChordKeyTimeout;
    private final int mSmartSelectionInitializedTimeout;
    private final int mSmartSelectionInitializingTimeout;
    private final boolean mPreferKeepClearForFocusEnabled;
    private final boolean mViewBasedRotaryEncoderScrollHapticsEnabledConfig;
    private final int mTapTimeoutMillis;
    private final int mDoubleTapTimeoutMillis;
    private final int mDoubleTapMinTimeMillis;
    private final float mScrollFriction;

    private int mDeviceId = Context.DEVICE_ID_INVALID;

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768915)
    private boolean sHasPermanentMenuKey;
    @UnsupportedAppUsage
    private boolean sHasPermanentMenuKeySet;

    static final LongSparseArray<ViewConfiguration> sConfigurations = new LongSparseArray<>(2);

    /**
     * @deprecated Use {@link android.view.ViewConfiguration#get(android.content.Context)} instead.
     */
    @Deprecated
    public ViewConfiguration() {
        mConstructedWithContext = false;
        mEdgeSlop = EDGE_SLOP;
        mFadingEdgeLength = FADING_EDGE_LENGTH;
        mMinimumFlingVelocity = MINIMUM_FLING_VELOCITY;
        mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY;
        mMinimumRotaryEncoderFlingVelocity = MINIMUM_FLING_VELOCITY;
        mMaximumRotaryEncoderFlingVelocity = MAXIMUM_FLING_VELOCITY;
        mRotaryEncoderHapticScrollFeedbackEnabled = false;
        mRotaryEncoderHapticScrollFeedbackTickIntervalPixels = NO_HAPTIC_SCROLL_TICK_INTERVAL;
        mViewBasedRotaryEncoderScrollHapticsEnabledConfig = false;
        mScrollbarSize = SCROLL_BAR_SIZE;
        mTouchSlop = TOUCH_SLOP;
        mHandwritingSlop = HANDWRITING_SLOP;
        mHoverSlop = TOUCH_SLOP / 2;
        mMinScrollbarTouchTarget = MIN_SCROLLBAR_TOUCH_TARGET;
        mDoubleTapTouchSlop = DOUBLE_TAP_TOUCH_SLOP;
        mPagingTouchSlop = PAGING_TOUCH_SLOP;
        mDoubleTapSlop = DOUBLE_TAP_SLOP;
        mWindowTouchSlop = WINDOW_TOUCH_SLOP;
        mHandwritingGestureLineMargin = HANDWRITING_GESTURE_LINE_MARGIN;
        mAmbiguousGestureMultiplier = AMBIGUOUS_GESTURE_MULTIPLIER;
        //noinspection deprecation
        mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE;
        mOverscrollDistance = OVERSCROLL_DISTANCE;
        mOverflingDistance = OVERFLING_DISTANCE;
        mFadingMarqueeEnabled = true;
        mGlobalActionsKeyTimeout = GLOBAL_ACTIONS_KEY_TIMEOUT;
        mHorizontalScrollFactor = HORIZONTAL_SCROLL_FACTOR;
        mVerticalScrollFactor = VERTICAL_SCROLL_FACTOR;
        mShowMenuShortcutsWhenKeyboardPresent = false;
        mScreenshotChordKeyTimeout = SCREENSHOT_CHORD_KEY_TIMEOUT;

        // Getter throws if mConstructedWithContext is false so doesn't matter what
        // this value is.
        mMinScalingSpan = 0;
        mSmartSelectionInitializedTimeout = SMART_SELECTION_INITIALIZED_TIMEOUT_IN_MILLISECOND;
        mSmartSelectionInitializingTimeout = SMART_SELECTION_INITIALIZING_TIMEOUT_IN_MILLISECOND;
        mPreferKeepClearForFocusEnabled = false;
        mViewTouchScreenHapticScrollFeedbackEnabled = false;

        mTapTimeoutMillis = sResourceCache.getTapTimeout();
        mDoubleTapTimeoutMillis = sResourceCache.getDoubleTapTimeout();
        mDoubleTapMinTimeMillis = sResourceCache.getDoubleTapMinTime();
        mScrollFriction = sResourceCache.getScrollFriction();
    }

    /**
     * Creates a new configuration for the specified visual {@link Context}. The configuration
     * depends on various parameters of the {@link Context}, like the dimension of the display or
     * the density of the display.
     *
     * @param context A visual {@link Context} used to initialize the view configuration. It must
     *                be {@link Activity} or other {@link Context} created with
     *                {@link Context#createWindowContext(int, Bundle)}.
     *
     * @see #get(android.content.Context)
     * @see android.util.DisplayMetrics
     */
    private ViewConfiguration(@NonNull @UiContext Context context) {
        mConstructedWithContext = true;
        final Resources res = context.getResources();
        final DisplayMetrics metrics = res.getDisplayMetrics();
        final Configuration config = res.getConfiguration();
        final float density = metrics.density;
        final float sizeAndDensity;
        if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_XLARGE)) {
            sizeAndDensity = density * 1.5f;
        } else {
            sizeAndDensity = density;
        }

        mEdgeSlop = (int) (sizeAndDensity * EDGE_SLOP + 0.5f);
        mFadingEdgeLength = (int) (sizeAndDensity * FADING_EDGE_LENGTH + 0.5f);
        mScrollbarSize = res.getDimensionPixelSize(R.dimen.config_scrollbarSize);
        mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f);
        mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f);

        final TypedValue multiplierValue = new TypedValue();
        res.getValue(R.dimen.config_ambiguousGestureMultiplier,
                multiplierValue,
                true /*resolveRefs*/);
        mAmbiguousGestureMultiplier = Math.max(1.0f, multiplierValue.getFloat());

        // Size of the screen in bytes, in ARGB_8888 format
        final Rect maxBounds = config.windowConfiguration.getMaxBounds();
        mMaximumDrawingCacheSize = 4 * maxBounds.width() * maxBounds.height();

        mOverscrollDistance = (int) (sizeAndDensity * OVERSCROLL_DISTANCE + 0.5f);
        mOverflingDistance = (int) (sizeAndDensity * OVERFLING_DISTANCE + 0.5f);

        if (!sHasPermanentMenuKeySet) {
            final int configVal = res.getInteger(R.integer.config_overrideHasPermanentMenuKey);

            switch (configVal) {
                default:
                case HAS_PERMANENT_MENU_KEY_AUTODETECT: {
                    IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
                    try {
                        sHasPermanentMenuKey = !wm.hasNavigationBar(context.getDisplayId());
                        sHasPermanentMenuKeySet = true;
                    } catch (RemoteException ex) {
                        sHasPermanentMenuKey = false;
                    }
                }
                break;

                case HAS_PERMANENT_MENU_KEY_TRUE:
                    sHasPermanentMenuKey = true;
                    sHasPermanentMenuKeySet = true;
                    break;

                case HAS_PERMANENT_MENU_KEY_FALSE:
                    sHasPermanentMenuKey = false;
                    sHasPermanentMenuKeySet = true;
                    break;
            }
        }

        mFadingMarqueeEnabled = res.getBoolean(R.bool.config_ui_enableFadingMarquee);
        mTouchSlop = res.getDimensionPixelSize(R.dimen.config_viewConfigurationTouchSlop);
        mHandwritingSlop = res.getDimensionPixelSize(
                R.dimen.config_viewConfigurationHandwritingSlop);
        mHoverSlop = res.getDimensionPixelSize(R.dimen.config_viewConfigurationHoverSlop);
        mMinScrollbarTouchTarget = res.getDimensionPixelSize(
                R.dimen.config_minScrollbarTouchTarget);
        mPagingTouchSlop = mTouchSlop * 2;

        mDoubleTapTouchSlop = mTouchSlop;

        mHandwritingGestureLineMargin = res.getDimensionPixelSize(
                R.dimen.config_viewConfigurationHandwritingGestureLineMargin);

        mMinimumFlingVelocity = res.getDimensionPixelSize(R.dimen.config_viewMinFlingVelocity);
        mMaximumFlingVelocity = res.getDimensionPixelSize(R.dimen.config_viewMaxFlingVelocity);

        int configMinRotaryEncoderFlingVelocity = res.getDimensionPixelSize(
                R.dimen.config_viewMinRotaryEncoderFlingVelocity);
        int configMaxRotaryEncoderFlingVelocity = res.getDimensionPixelSize(
                R.dimen.config_viewMaxRotaryEncoderFlingVelocity);
        if (configMinRotaryEncoderFlingVelocity < 0 || configMaxRotaryEncoderFlingVelocity < 0) {
            mMinimumRotaryEncoderFlingVelocity = NO_FLING_MIN_VELOCITY;
            mMaximumRotaryEncoderFlingVelocity = NO_FLING_MAX_VELOCITY;
        } else {
            mMinimumRotaryEncoderFlingVelocity = configMinRotaryEncoderFlingVelocity;
            mMaximumRotaryEncoderFlingVelocity = configMaxRotaryEncoderFlingVelocity;
        }

        int configRotaryEncoderHapticScrollFeedbackTickIntervalPixels =
                res.getDimensionPixelSize(R.dimen
                                .config_rotaryEncoderAxisScrollTickInterval);
        mRotaryEncoderHapticScrollFeedbackTickIntervalPixels =
                configRotaryEncoderHapticScrollFeedbackTickIntervalPixels > 0
                        ? configRotaryEncoderHapticScrollFeedbackTickIntervalPixels
                        : NO_HAPTIC_SCROLL_TICK_INTERVAL;

        mRotaryEncoderHapticScrollFeedbackEnabled =
                res.getBoolean(R.bool
                                .config_viewRotaryEncoderHapticScrollFedbackEnabled);

        mGlobalActionsKeyTimeout = res.getInteger(R.integer.config_globalActionsKeyTimeout);

        mHorizontalScrollFactor = res.getDimensionPixelSize(R.dimen.config_horizontalScrollFactor);
        mVerticalScrollFactor = res.getDimensionPixelSize(R.dimen.config_verticalScrollFactor);

        mShowMenuShortcutsWhenKeyboardPresent = res.getBoolean(
                R.bool.config_showMenuShortcutsWhenKeyboardPresent);

        mMinScalingSpan = res.getDimensionPixelSize(R.dimen.config_minScalingSpan);

        mScreenshotChordKeyTimeout = res.getInteger(R.integer.config_screenshotChordKeyTimeout);

        mSmartSelectionInitializedTimeout = res.getInteger(
                R.integer.config_smartSelectionInitializedTimeoutMillis);
        mSmartSelectionInitializingTimeout = res.getInteger(
                R.integer.config_smartSelectionInitializingTimeoutMillis);
        mPreferKeepClearForFocusEnabled = res.getBoolean(R.bool.config_preferKeepClearForFocus);
        mViewBasedRotaryEncoderScrollHapticsEnabledConfig =
                res.getBoolean(R.bool.config_viewBasedRotaryEncoderHapticsEnabled);
        mViewTouchScreenHapticScrollFeedbackEnabled =
                res.getBoolean(R.bool.config_viewTouchScreenHapticScrollFeedbackEnabled);

        mTapTimeoutMillis = res.getInteger(R.integer.config_tapTimeoutMillis);
        mDoubleTapTimeoutMillis = res.getInteger(R.integer.config_doubleTapTimeoutMillis);
        mDoubleTapMinTimeMillis = res.getInteger(R.integer.config_doubleTapMinTimeMillis);
        mScrollFriction = res.getFloat(R.dimen.config_scrollFriction);

        mDeviceId = context.getDeviceId();
    }

    /**
     * Returns a configuration for the specified visual {@link Context}. The configuration depends
     * on various parameters of the {@link Context}, like the dimension of the display or the
     * density of the display.
     *
     * @param context A visual {@link Context} used to initialize the view configuration. It must
     *                be {@link Activity} or other {@link Context} created with
     *                {@link Context#createWindowContext(int, Bundle)}.
     */
    // TODO(b/182007470): Use @ConfigurationContext instead
    public static ViewConfiguration get(@NonNull @UiContext Context context) {
        StrictMode.assertConfigurationContext(context, "ViewConfiguration");

        final long key = createKey(context);
        ViewConfiguration configuration = sConfigurations.get(key);
        if (configuration == null) {
            configuration = new ViewConfiguration(context);
            sConfigurations.put(key, 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();
        sResourceCache = new ResourceCache();
    }

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

    /**
     * @return The width of the horizontal scrollbar and the height of the vertical
     *         scrollbar in dips
     *
     * @deprecated Use {@link #getScaledScrollBarSize()} instead.
     */
    @Deprecated
    public static int getScrollBarSize() {
        return SCROLL_BAR_SIZE;
    }

    /**
     * @return The width of the horizontal scrollbar and the height of the vertical
     *         scrollbar in pixels
     */
    public int getScaledScrollBarSize() {
        return mScrollbarSize;
    }

    /**
     * @return the minimum size of the scrollbar thumb's touch target in pixels
     * @hide
     */
    public int getScaledMinScrollbarTouchTarget() {
        return mMinScrollbarTouchTarget;
    }

    /**
     * @return Duration of the fade when scrollbars fade away in milliseconds
     */
    public static int getScrollBarFadeDuration() {
        return SCROLL_BAR_FADE_DURATION;
    }

    /**
     * @return Default delay before the scrollbars fade in milliseconds
     */
    public static int getScrollDefaultDelay() {
        return SCROLL_BAR_DEFAULT_DELAY;
    }

    /**
     * @return the length of the fading edges in dips
     *
     * @deprecated Use {@link #getScaledFadingEdgeLength()} instead.
     */
    @Deprecated
    public static int getFadingEdgeLength() {
        return FADING_EDGE_LENGTH;
    }

    /**
     * @return the length of the fading edges in pixels
     */
    public int getScaledFadingEdgeLength() {
        return mFadingEdgeLength;
    }

    /**
     * @return the duration in milliseconds of the pressed state in child
     * components.
     */
    public static int getPressedStateDuration() {
        return sResourceCache.getPressedStateDuration();
    }

    /**
     * Used for both key and motion events.
     *
     * @return the duration in milliseconds before a press turns into
     * a long press
     */
    public static int getLongPressTimeout() {
        return AppGlobals.getIntCoreSetting(Settings.Secure.LONG_PRESS_TIMEOUT,
                DEFAULT_LONG_PRESS_TIMEOUT);
    }

    /**
     * @return the duration in milliseconds before a press turns into
     * a long press.
     */
    @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_VIEWCONFIGURATION_APIS)
    public int getLongPressTimeoutMillis() {
        return getSettingValue(Settings.Secure.LONG_PRESS_TIMEOUT, DEFAULT_LONG_PRESS_TIMEOUT);
    }

    /**
     * @return the duration in milliseconds between the first tap's up event and the second tap's
     * down event for an interaction to be considered part of the same multi-press.
     */
    public static int getMultiPressTimeout() {
        return AppGlobals.getIntCoreSetting(Settings.Secure.MULTI_PRESS_TIMEOUT,
                DEFAULT_MULTI_PRESS_TIMEOUT);
    }

    /**
     * @return the duration in milliseconds between the first tap's up event and the second tap's
     * down event for an interaction to be considered part of the same multi-press.
     */
    @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_VIEWCONFIGURATION_APIS)
    public int getMultiPressTimeoutMillis() {
        return getSettingValue(Settings.Secure.MULTI_PRESS_TIMEOUT, DEFAULT_MULTI_PRESS_TIMEOUT);
    }

    /**
     * @return the time before the first key repeat in milliseconds.
     */
    public static int getKeyRepeatTimeout() {
        return AppGlobals.getIntCoreSetting(Settings.Secure.KEY_REPEAT_TIMEOUT_MS,
                DEFAULT_KEY_REPEAT_TIMEOUT_MS);
    }

    /**
     * @return the time between successive key repeats in milliseconds.
     */
    public static int getKeyRepeatDelay() {
        return AppGlobals.getIntCoreSetting(Settings.Secure.KEY_REPEAT_DELAY_MS,
                DEFAULT_KEY_REPEAT_DELAY_MS);
    }

    /**
     * Returns the duration in milliseconds of the text cursor blink interval, which is the elapsed
     * time between inversions of the color of the text cursor's pixels.
     *
     * <p>The value is user-configurable and is constrained to the following range:
     * <ul>
     *   <li>0 (don't blink)
     *   <li>>= 333 (fast blink, the minimum blink interval that is comfortable for
     *   photosensitivity)
     * </ul>
     *
     * @return the duration in milliseconds of the text cursor blink interval
     */
    public int getTextCursorBlinkIntervalMillis() {
        int value = getSettingValue(Settings.Secure.ACCESSIBILITY_TEXT_CURSOR_BLINK_INTERVAL_MS,
                sResourceCache.getDefaultTextCursorBlinkInterval());
        int noBlink = sResourceCache.getNoBlinkTextCursorBlinkInterval();
        int minBlink = sResourceCache.getMinTextCursorBlinkInterval();

        if (value <= noBlink) {
            // Treat any negative value as no-blink.
            return noBlink;
        } else if (value <= minBlink) {
            // Constrain non-zero blink interval to >= minimum blink (333 ms).
            return minBlink;
        }

        return value;
    }

    /**
     * @return the duration in milliseconds we will wait to see if a touch event
     * is a tap or a scroll. If the user does not move within this interval, it is
     * considered to be a tap.
     */
    public static int getTapTimeout() {
        return sResourceCache.getTapTimeout();
    }

    /**
     * @return the duration in milliseconds we will wait to see if a touch event
     * is a tap or a scroll. If the user does not move within this interval, it is
     * considered to be a tap.
     */
    @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_VIEWCONFIGURATION_APIS)
    public int getTapTimeoutMillis() {
        return mTapTimeoutMillis;
    }

    /**
     * @return the duration in milliseconds we will wait to see if a touch event
     * is a jump tap. If the user does not move within this interval, it is
     * considered to be a tap.
     */
    public static int getJumpTapTimeout() {
        return sResourceCache.getJumpTapTimeout();
    }

    /**
     * @return the duration in milliseconds between the first tap's up event and
     * the second tap's down event for an interaction to be considered a
     * double-tap.
     */
    public static int getDoubleTapTimeout() {
        return sResourceCache.getDoubleTapTimeout();
    }

    /**
     * @return the duration in milliseconds between the first tap's up event and
     * the second tap's down event for an interaction to be considered a
     * double-tap.
     */
    @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_VIEWCONFIGURATION_APIS)
    public int getDoubleTapTimeoutMillis() {
        return mDoubleTapTimeoutMillis;
    }

    /**
     * @return the minimum duration in milliseconds between the first tap's
     * up event and the second tap's down event for an interaction to be considered a
     * double-tap.
     *
     * @hide
     */
    @UnsupportedAppUsage
    public static int getDoubleTapMinTime() {
        return sResourceCache.getDoubleTapMinTime();
    }

    /**
     * @return the minimum duration in milliseconds between the first tap's
     * up event and the second tap's down event for an interaction to be considered a
     * double-tap.
     *
     * @hide
     */
    public int getDoubleTapMinTimeMillis() {
        return mDoubleTapMinTimeMillis;
    }

    /**
     * @return the maximum distance in pixels that a touch pad touch can move
     * before being released for it to be considered a tap (click) as opposed
     * to a hover movement gesture.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public static int getHoverTapSlop() {
        return sResourceCache.getHoverTapSlop();
    }

    /**
     * @return Inset in dips to look for touchable content when the user touches the edge of the
     *         screen
     *
     * @deprecated Use {@link #getScaledEdgeSlop()} instead.
     */
    @Deprecated
    public static int getEdgeSlop() {
        return EDGE_SLOP;
    }

    /**
     * @return Inset in pixels to look for touchable content when the user touches the edge of the
     *         screen
     */
    public int getScaledEdgeSlop() {
        return mEdgeSlop;
    }

    /**
     * @return Distance in dips a touch can wander before we think the user is scrolling
     *
     * @deprecated Use {@link #getScaledTouchSlop()} instead.
     */
    @Deprecated
    public static int getTouchSlop() {
        return TOUCH_SLOP;
    }

    /**
     * @return Distance in pixels a touch can wander before we think the user is scrolling
     */
    public int getScaledTouchSlop() {
        return mTouchSlop;
    }

    /**
     * @return Distance in pixels a stylus touch can wander before we think the user is
     * handwriting.
     */
    public int getScaledHandwritingSlop() {
        return mHandwritingSlop;
    }

    /**
     * @return Distance in pixels a hover can wander while it is still considered "stationary".
     *
     */
    public int getScaledHoverSlop() {
        return mHoverSlop;
    }

    /**
     * @return Distance in pixels the first touch can wander before we do not consider this a
     * potential double tap event
     * @hide
     */
    @UnsupportedAppUsage
    public int getScaledDoubleTapTouchSlop() {
        return mDoubleTapTouchSlop;
    }

    /**
     * @return Distance in pixels a touch can wander before we think the user is scrolling a full
     * page
     */
    public int getScaledPagingTouchSlop() {
        return mPagingTouchSlop;
    }

    /**
     * @return Distance in dips between the first touch and second touch to still be
     *         considered a double tap
     * @deprecated Use {@link #getScaledDoubleTapSlop()} instead.
     * @hide The only client of this should be GestureDetector, which needs this
     *       for clients that still use its deprecated constructor.
     */
    @Deprecated
    @UnsupportedAppUsage
    public static int getDoubleTapSlop() {
        return DOUBLE_TAP_SLOP;
    }

    /**
     * @return Distance in pixels between the first touch and second touch to still be
     *         considered a double tap
     */
    public int getScaledDoubleTapSlop() {
        return mDoubleTapSlop;
    }

    /**
     * @return margin in pixels around text line bounds where stylus handwriting gestures should be
     *     supported.
     */
    public int getScaledHandwritingGestureLineMargin() {
        return mHandwritingGestureLineMargin;
    }

    /**
     * Interval for dispatching a recurring accessibility event in milliseconds.
     * This interval guarantees that a recurring event will be send at most once
     * during the {@link #getSendRecurringAccessibilityEventsInterval()} time frame.
     *
     * @return The delay in milliseconds.
     *
     * @hide
     */
    public static long getSendRecurringAccessibilityEventsInterval() {
        return SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS;
    }

    /**
     * @return Distance in dips a touch must be outside the bounds of a window for it
     * to be counted as outside the window for purposes of dismissing that
     * window.
     *
     * @deprecated Use {@link #getScaledWindowTouchSlop()} instead.
     */
    @Deprecated
    public static int getWindowTouchSlop() {
        return WINDOW_TOUCH_SLOP;
    }

    /**
     * @return Distance in pixels a touch must be outside the bounds of a window for it
     * to be counted as outside the window for purposes of dismissing that window.
     */
    public int getScaledWindowTouchSlop() {
        return mWindowTouchSlop;
    }

    /**
     * @return Minimum velocity to initiate a fling, as measured in dips per second.
     *
     * @deprecated Use {@link #getScaledMinimumFlingVelocity()} instead.
     */
    @Deprecated
    public static int getMinimumFlingVelocity() {
        return MINIMUM_FLING_VELOCITY;
    }

    /**
     * @return Minimum velocity to initiate a fling, as measured in pixels per second.
     */
    public int getScaledMinimumFlingVelocity() {
        return mMinimumFlingVelocity;
    }

    /**
     * @return Maximum velocity to initiate a fling, as measured in dips per second.
     *
     * @deprecated Use {@link #getScaledMaximumFlingVelocity()} instead.
     */
    @Deprecated
    public static int getMaximumFlingVelocity() {
        return MAXIMUM_FLING_VELOCITY;
    }

    /**
     * @return Maximum velocity to initiate a fling, as measured in pixels per second.
     */
    public int getScaledMaximumFlingVelocity() {
        return mMaximumFlingVelocity;
    }

    /**
     * @return Amount to scroll in response to a {@link MotionEvent#ACTION_SCROLL} event. Multiply
     * this by the event's axis value to obtain the number of pixels to be scrolled.
     *
     * @removed
     */
    public int getScaledScrollFactor() {
        return (int) mVerticalScrollFactor;
    }

    /**
     * @return Amount to scroll in response to a horizontal {@link MotionEvent#ACTION_SCROLL} event.
     * Multiply this by the event's axis value to obtain the number of pixels to be scrolled.
     */
    public float getScaledHorizontalScrollFactor() {
        return mHorizontalScrollFactor;
    }

    /**
     * @return Amount to scroll in response to a vertical {@link MotionEvent#ACTION_SCROLL} event.
     * Multiply this by the event's axis value to obtain the number of pixels to be scrolled.
     */
    public float getScaledVerticalScrollFactor() {
        return mVerticalScrollFactor;
    }

    /**
     * The maximum drawing cache size expressed in bytes.
     *
     * @return the maximum size of View's drawing cache expressed in bytes
     *
     * @deprecated Use {@link #getScaledMaximumDrawingCacheSize()} instead.
     */
    @Deprecated
    public static int getMaximumDrawingCacheSize() {
        //noinspection deprecation
        return MAXIMUM_DRAWING_CACHE_SIZE;
    }

    /**
     * The maximum drawing cache size expressed in bytes.
     *
     * @return the maximum size of View's drawing cache expressed in bytes
     */
    public int getScaledMaximumDrawingCacheSize() {
        return mMaximumDrawingCacheSize;
    }

    /**
     * @return The maximum distance a View should overscroll by when showing edge effects (in
     * pixels).
     */
    public int getScaledOverscrollDistance() {
        return mOverscrollDistance;
    }

    /**
     * @return The maximum distance a View should overfling by when showing edge effects (in
     * pixels).
     */
    public int getScaledOverflingDistance() {
        return mOverflingDistance;
    }

    /**
     * The amount of time that the zoom controls should be
     * displayed on the screen expressed in milliseconds.
     *
     * @return the time the zoom controls should be visible expressed
     * in milliseconds.
     */
    public static long getZoomControlsTimeout() {
        return sResourceCache.getZoomControlsTimeout();
    }

    /**
     * The amount of time a user needs to press the relevant key to bring up
     * the global actions dialog.
     *
     * @return how long a user needs to press the relevant key to bring up
     *   the global actions dialog.
     * @deprecated This timeout should not be used by applications
     */
    @Deprecated
    public static long getGlobalActionKeyTimeout() {
        return GLOBAL_ACTIONS_KEY_TIMEOUT;
    }

    /**
     * The amount of time a user needs to press the relevant key to bring up
     * the global actions dialog.
     *
     * @return how long a user needs to press the relevant key to bring up
     *   the global actions dialog.
     * @hide
     */
    @TestApi
    public long getDeviceGlobalActionKeyTimeout() {
        return mGlobalActionsKeyTimeout;
    }

    /**
     * The amount of time a user needs to press the relevant keys to trigger
     * the screenshot chord.
     *
     * @return how long a user needs to press the relevant keys to trigger
     *   the screenshot chord.
     * @hide
     */
    public long getScreenshotChordKeyTimeout() {
        return mScreenshotChordKeyTimeout;
    }

    /**
     * The amount of time a user needs to press the relevant keys to activate the accessibility
     * shortcut.
     *
     * @return how long a user needs to press the relevant keys to activate the accessibility
     *   shortcut.
     * @hide
     */
    public long getAccessibilityShortcutKeyTimeout() {
        return A11Y_SHORTCUT_KEY_TIMEOUT;
    }

    /**
     * @return The amount of time a user needs to press the relevant keys to activate the
     *   accessibility shortcut after it's confirmed that accessibility shortcut is used.
     * @hide
     */
    public long getAccessibilityShortcutKeyTimeoutAfterConfirmation() {
        return A11Y_SHORTCUT_KEY_TIMEOUT_AFTER_CONFIRMATION;
    }

    /**
     * The amount of friction applied to scrolls and flings.
     *
     * @return A scalar dimensionless value representing the coefficient of
     *         friction.
     */
    public static float getScrollFriction() {
        return sResourceCache.getScrollFriction();
    }

    /**
     * The amount of friction applied to scrolls and flings.
     *
     * @return A scalar dimensionless value representing the coefficient of
     *         friction.
     */
    @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_VIEWCONFIGURATION_APIS)
    public float getScrollFrictionAmount() {
        return mScrollFriction;
    }

    /**
     * @return the default duration in milliseconds for {@link ActionMode#hide(long)}.
     */
    public static long getDefaultActionModeHideDuration() {
        return sResourceCache.getDefaultActionModeHideDuration();
    }

    /**
     * The multiplication factor for inhibiting default gestures.
     *
     * If a MotionEvent has {@link android.view.MotionEvent#CLASSIFICATION_AMBIGUOUS_GESTURE} set,
     * then certain actions, such as scrolling, will be inhibited. However, to account for the
     * possibility of an incorrect classification, existing gesture thresholds (e.g. scrolling
     * touch slop and the long-press timeout) should be scaled by this factor and remain in effect.
     *
     * @deprecated Use {@link #getScaledAmbiguousGestureMultiplier()}.
     */
    @Deprecated
    @FloatRange(from = 1.0)
    public static float getAmbiguousGestureMultiplier() {
        return AMBIGUOUS_GESTURE_MULTIPLIER;
    }

    /**
     * The multiplication factor for inhibiting default gestures.
     *
     * If a MotionEvent has {@link android.view.MotionEvent#CLASSIFICATION_AMBIGUOUS_GESTURE} set,
     * then certain actions, such as scrolling, will be inhibited. However, to account for the
     * possibility of an incorrect classification, existing gesture thresholds (e.g. scrolling
     * touch slop and the long-press timeout) should be scaled by this factor and remain in effect.
     */
    @FloatRange(from = 1.0)
    public float getScaledAmbiguousGestureMultiplier() {
        return mAmbiguousGestureMultiplier;
    }

    /**
     * Report if the device has a permanent menu key available to the user.
     *
     * <p>As of Android 3.0, devices may not have a permanent menu key available.
     * Apps should use the action bar to present menu options to users.
     * However, there are some apps where the action bar is inappropriate
     * or undesirable. This method may be used to detect if a menu key is present.
     * If not, applications should provide another on-screen affordance to access
     * functionality.
     *
     * @return true if a permanent menu key is present, false otherwise.
     */
    public boolean hasPermanentMenuKey() {
        return sHasPermanentMenuKey;
    }

    /**
     * Minimum absolute value of velocity to initiate a fling for 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>Before utilizing this method to get a minimum fling velocity for a motion generated by the
     * input device, scale the velocity of the motion events generated by the input device to pixels
     * per second.
     *
     * <p>For instance, if you tracked {@link MotionEvent#AXIS_SCROLL} vertical velocities generated
     * from a {@link InputDevice#SOURCE_ROTARY_ENCODER}, the velocity returned from
     * {@link VelocityTracker} will be in the units with which the axis values were reported in the
     * motion event. Before comparing that velocity against the minimum fling velocity specified
     * here, make sure that the {@link MotionEvent#AXIS_SCROLL} velocity from the tracker is
     * calculated in "units per second" (see {@link VelocityTracker#computeCurrentVelocity(int)},
     * {@link VelocityTracker#computeCurrentVelocity(int, float)} to adjust your velocity
     * computations to "per second"), and use {@link #getScaledVerticalScrollFactor} to change this
     * velocity value to "pixels/second".
     *
     * <p>If the provided {@code inputDeviceId} is not valid, or if the input device whose ID is
     * provided does not support the given motion event source and/or axis, this method will return
     * {@code Integer.MAX_VALUE}.
     *
     * <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 for which you obtained the
     * velocity for ({@link VelocityTracker} lets you calculate velocities for a specific axis. Use
     * the axis for which you calculated velocity). 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.
     *
     *
     * <p>This method optimizes calls over multiple input device IDs, so caching the return value of
     * the method is not necessary if you are handling multiple input devices.
     *
     * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion triggering
     *          fling.
     * @param axis the axis on which the motion triggering the fling happened. This axis should be
     *          a valid axis that can be reported by the provided input device from the provided
     *          input device source.
     * @param source the input source of the motion causing fling. This source should be a valid
     *          source for the {@link InputDevice} whose ID is {@code inputDeviceId}.
     *
     * @return the minimum velocity, in pixels/second, to trigger fling.
     *
     * @see InputDevice#getMotionRange(int, int)
     * @see InputDevice#getMotionRanges()
     * @see VelocityTracker#getAxisVelocity(int, int)
     * @see VelocityTracker#getAxisVelocity(int)
     */
    public int getScaledMinimumFlingVelocity(int inputDeviceId, int axis, int source) {
        if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) return NO_FLING_MIN_VELOCITY;

        if (source == InputDevice.SOURCE_ROTARY_ENCODER) return mMinimumRotaryEncoderFlingVelocity;

        return mMinimumFlingVelocity;
    }

    /**
     * Maximum absolute value of velocity to initiate a fling for 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>Similar to {@link #getScaledMinimumFlingVelocity(int, int, int)}, but for maximum fling
     * velocity, instead of minimum. Also, unlike that method which returns
     * {@code Integer.MAX_VALUE} for bad input device ID, source and/or motion event axis inputs,
     * this method returns {@code Integer.MIN_VALUE} for such bad inputs.
     */
    public int getScaledMaximumFlingVelocity(int inputDeviceId, int axis, int source) {
        if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) return NO_FLING_MAX_VELOCITY;

        if (source == InputDevice.SOURCE_ROTARY_ENCODER) return mMaximumRotaryEncoderFlingVelocity;

        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.
     *
     * <p>See {@link ScrollFeedbackProvider} for details on the arguments that should be passed to
     * the methods in this class.
     *
     * <p>If the provided input device ID, source, and motion axis are not supported by this Android
     * device, this method returns {@code false}. In other words, if the {@link InputDevice}
     * represented by the provided {code inputDeviceId} does not have a
     * {@link InputDevice.MotionRange} with the provided {@code axis} and {@code source}, the method
     * returns {@code false}.
     *
     * <p>If the provided input device ID, source, and motion axis are supported by this Android
     * device, this method returns {@code true} only if the provided arguments are supported for
     * scroll haptics. Otherwise, this method returns {@code false}.
     *
     * @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
     *      can produce scroll haptics. {@code false} otherwise.
     *
     * @see #getHapticScrollFeedbackTickInterval(int, int, int)
     * @see InputDevice#getMotionRanges()
     * @see InputDevice#getMotionRange(int)
     * @see InputDevice#getMotionRange(int, int)
     *
     * @hide
     */
    public boolean isHapticScrollFeedbackEnabled(int inputDeviceId, int axis, int source) {
        if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) return false;

        if (source == InputDevice.SOURCE_ROTARY_ENCODER && axis == MotionEvent.AXIS_SCROLL) {
            return mRotaryEncoderHapticScrollFeedbackEnabled;
        }

        if ((source & InputDevice.SOURCE_TOUCHSCREEN) != 0) {
            return mViewTouchScreenHapticScrollFeedbackEnabled;
        }

        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}.
     *
     * @see #isHapticScrollFeedbackEnabled(int, int, int)
     *
     * @hide
     */
    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 && axis == MotionEvent.AXIS_SCROLL) {
            return mRotaryEncoderHapticScrollFeedbackTickIntervalPixels;
        }

        return NO_HAPTIC_SCROLL_TICK_INTERVAL;
    }

    /**
     * Checks if the View-based haptic scroll feedback implementation is enabled for
     * {@link InputDevice#SOURCE_ROTARY_ENCODER}s.
     *
     * @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;
    }

    /**
     * Check if shortcuts should be displayed in menus.
     *
     * @return {@code True} if shortcuts should be displayed in menus.
     */
    public boolean shouldShowMenuShortcutsWhenKeyboardPresent() {
        return mShowMenuShortcutsWhenKeyboardPresent;
    }

    /**
     * Retrieves the distance in pixels between touches that must be reached for a gesture to be
     * interpreted as scaling.
     *
     * In general, scaling shouldn't start until this distance has been met or surpassed, and
     * scaling should end when the distance in pixels between touches drops below this distance.
     *
     * @return The distance in pixels
     * @throws IllegalStateException if this method is called on a ViewConfiguration that was
     *         instantiated using a constructor with no Context parameter.
     */
    public int getScaledMinimumScalingSpan() {
        if (!mConstructedWithContext) {
            throw new IllegalStateException("Min scaling span cannot be determined when this "
                    + "method is called on a ViewConfiguration that was instantiated using a "
                    + "constructor with no Context parameter");
        }
        return mMinScalingSpan;
    }

    /**
     * @hide
     * @return Whether or not marquee should use fading edges.
     */
    @UnsupportedAppUsage
    public boolean isFadingMarqueeEnabled() {
        return mFadingMarqueeEnabled;
    }

    /**
     * @return the timeout value in milliseconds to adjust the selection span and actions for the
     *         selected text when TextClassifier has been initialized.
     * @hide
     */
    public int getSmartSelectionInitializedTimeout() {
        return mSmartSelectionInitializedTimeout;
    }

    /**
     * @return the timeout value in milliseconds to adjust the selection span and actions for the
     *         selected text when TextClassifier has not been initialized.
     * @hide
     */
    public int getSmartSelectionInitializingTimeout() {
        return mSmartSelectionInitializingTimeout;
    }

    /**
     * @return {@code true} if Views should set themselves as preferred to keep clear when focused,
     * {@code false} otherwise.
     * @hide
     */
    @TestApi
    public boolean isPreferKeepClearForFocusEnabled() {
        return mPreferKeepClearForFocusEnabled;
    }

    /**
     * @return the duration in milliseconds before an end of a long press causes a tooltip to be
     * hidden
     * @hide
     */
    @TestApi
    public static int getLongPressTooltipHideTimeout() {
        return LONG_PRESS_TOOLTIP_HIDE_TIMEOUT;
    }

    /**
     * @return the duration in milliseconds before a hover event causes a tooltip to be shown
     * @hide
     */
    @TestApi
    public static int getHoverTooltipShowTimeout() {
        return HOVER_TOOLTIP_SHOW_TIMEOUT;
    }

    /**
     * @return the duration in milliseconds before mouse inactivity causes a tooltip to be hidden
     * (default variant to be used when {@link View#SYSTEM_UI_FLAG_LOW_PROFILE} is not set).
     * @hide
     */
    @TestApi
    public static int getHoverTooltipHideTimeout() {
        return HOVER_TOOLTIP_HIDE_TIMEOUT;
    }

    /**
     * @return the duration in milliseconds before mouse inactivity causes a tooltip to be hidden
     * (shorter variant to be used when {@link View#SYSTEM_UI_FLAG_LOW_PROFILE} is set).
     * @hide
     */
    @TestApi
    public static int getHoverTooltipHideShortTimeout() {
        return HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT;
    }

    private int getSettingValue(String key, int defaultValue) {
        if (mDeviceId != Context.DEVICE_ID_INVALID) {
            return AppGlobals.getIntCoreSetting(key, defaultValue, mDeviceId);
        }
        return AppGlobals.getIntCoreSetting(key, defaultValue);
    }

    private static int getDisplayDensity(Context context) {
        final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        return (int) (100.0f * metrics.density);
    }

    /**
     * Returns a key of type long using the display density and deviceId from the {@link Context}.
     */
    private static long createKey(Context context) {
        int displayDensity = getDisplayDensity(context);
        return (((long) displayDensity) << 32) | (context.getDeviceId() & 0xffffffffL);
    }

    /**
     * Fetches resource values statically and caches them locally for fast lookup. Note that these
     * values will not be updated during the lifetime of a process, even if resource overlays are
     * applied.
     */
    private static final class ResourceCache {

        private int mPressedStateDuration = -1;
        private int mTapTimeout = -1;
        private int mJumpTapTimeout = -1;
        private int mDoubleTapTimeout = -1;
        private int mDoubleTapMinTime = -1;
        private int mHoverTapSlop = -1;
        private long mZoomControlsTimeout = -1L;
        private float mScrollFriction = -1f;
        private long mDefaultActionModeHideDuration = -1L;
        private int mDefaultTextCursorBlinkInterval = -1;
        private int mMinTextCursorBlinkInterval = -1;
        private int mNoBlinkTextCursorBlinkInterval = -1;

        public int getPressedStateDuration() {
            if (mPressedStateDuration < 0) {
                Resources resources = getCurrentResources();
                mPressedStateDuration = resources != null
                        ? resources.getInteger(R.integer.config_pressedStateDurationMillis)
                        : PRESSED_STATE_DURATION;
            }
            return mPressedStateDuration;
        }

        public int getTapTimeout() {
            if (mTapTimeout < 0) {
                Resources resources = getCurrentResources();
                mTapTimeout = resources != null
                        ? resources.getInteger(R.integer.config_tapTimeoutMillis)
                        : TAP_TIMEOUT;
            }
            return mTapTimeout;
        }

        public int getJumpTapTimeout() {
            if (mJumpTapTimeout < 0) {
                Resources resources = getCurrentResources();
                mJumpTapTimeout = resources != null
                        ? resources.getInteger(R.integer.config_jumpTapTimeoutMillis)
                        : JUMP_TAP_TIMEOUT;
            }
            return mJumpTapTimeout;
        }

        public int getDoubleTapTimeout() {
            if (mDoubleTapTimeout < 0) {
                Resources resources = getCurrentResources();
                mDoubleTapTimeout = resources != null
                        ? resources.getInteger(R.integer.config_doubleTapTimeoutMillis)
                        : DOUBLE_TAP_TIMEOUT;
            }
            return mDoubleTapTimeout;
        }

        public int getDoubleTapMinTime() {
            if (mDoubleTapMinTime < 0) {
                Resources resources = getCurrentResources();
                mDoubleTapMinTime = resources != null
                        ? resources.getInteger(R.integer.config_doubleTapMinTimeMillis)
                        : DOUBLE_TAP_MIN_TIME;
            }
            return mDoubleTapMinTime;
        }

        public int getHoverTapSlop() {
            if (mHoverTapSlop < 0) {
                Resources resources = getCurrentResources();
                mHoverTapSlop = resources != null
                        ? resources.getDimensionPixelSize(R.dimen.config_hoverTapSlop)
                        : HOVER_TAP_SLOP;
            }
            return mHoverTapSlop;
        }

        public long getZoomControlsTimeout() {
            if (mZoomControlsTimeout < 0) {
                Resources resources = getCurrentResources();
                mZoomControlsTimeout = resources != null
                        ? resources.getInteger(R.integer.config_zoomControlsTimeoutMillis)
                        : ZOOM_CONTROLS_TIMEOUT;
            }
            return mZoomControlsTimeout;
        }

        public float getScrollFriction() {
            if (mScrollFriction < 0) {
                Resources resources = getCurrentResources();
                mScrollFriction = resources != null
                        ? resources.getFloat(R.dimen.config_scrollFriction)
                        : SCROLL_FRICTION;
            }
            return mScrollFriction;
        }

        public long getDefaultActionModeHideDuration() {
            if (mDefaultActionModeHideDuration < 0) {
                Resources resources = getCurrentResources();
                mDefaultActionModeHideDuration = resources != null
                        ? resources.getInteger(R.integer.config_defaultActionModeHideDurationMillis)
                        : ACTION_MODE_HIDE_DURATION_DEFAULT;
            }
            return mDefaultActionModeHideDuration;
        }

        public int getDefaultTextCursorBlinkInterval() {
            if (mDefaultTextCursorBlinkInterval < 0) {
                Resources resources = getCurrentResources();
                mDefaultTextCursorBlinkInterval = resources != null
                        ? resources.getInteger(
                        R.integer.def_accessibility_text_cursor_blink_interval_ms)
                        : DEFAULT_TEXT_CURSOR_BLINK_INTERVAL_MS;
            }
            return mDefaultTextCursorBlinkInterval;
        }

        public int getNoBlinkTextCursorBlinkInterval() {
            if (mNoBlinkTextCursorBlinkInterval < 0) {
                Resources resources = getCurrentResources();
                mNoBlinkTextCursorBlinkInterval = resources != null
                        ? resources.getInteger(
                        R.integer.no_blink_accessibility_text_cursor_blink_interval_ms)
                        : NO_BLINK_TEXT_CURSOR_BLINK_INTERVAL_MS;
            }
            return mNoBlinkTextCursorBlinkInterval;
        }

        public int getMinTextCursorBlinkInterval() {
            if (mMinTextCursorBlinkInterval < 0) {
                mMinTextCursorBlinkInterval = MIN_TEXT_CURSOR_BLINK_INTERVAL_MS;
                Resources resources = getCurrentResources();
                if (resources != null) {
                    int[] textCursorBlinkIntervals = resources.getIntArray(
                            R.array.accessibility_text_cursor_blink_intervals);
                    if (textCursorBlinkIntervals.length > 0) {
                        mMinTextCursorBlinkInterval = textCursorBlinkIntervals[0];
                    }
                }
            }
            return mMinTextCursorBlinkInterval;
        }

        private static Resources getCurrentResources() {
            Application application = ActivityThread.currentApplication();
            Context context = application != null ? application.getApplicationContext() : null;
            return context != null ? context.getResources() : null;
        }
    }
}
