chromium/ui/android/java/src/org/chromium/ui/MotionEventUtils.java

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.ui;

import android.os.Build;
import android.view.MotionEvent;

import androidx.annotation.Nullable;

import org.chromium.base.TraceEvent;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Class with helper methods for MotionEvent.
 *
 * <p>Not thread safe.
 */
public class MotionEventUtils {

    @Nullable private static Method sGetTimeNanoMethod;
    private static boolean sFailedReflection;

    /**
     * Returns the time in nanoseconds of the given MotionEvent.
     *
     * <p>This method exists as a utility pre API 34 (Android U) there was no public method to get
     * nanoseconds. So we call the hidden SDK method "getEventTimeNano" via reflection. If the
     * reflection fails, the time in milliseconds extended to nanoseconds will be returned.
     */
    public static long getEventTimeNanos(MotionEvent event) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            return event.getEventTimeNanos();
        }
        if (sFailedReflection) {
            return event.getEventTime() * 1_000_000;
        }
        long timeNs = 0;
        // We are calling a method that was set as maxSDK=P, there are strictmode violations but
        // suppressing it with StrictModeContext.allowAllVmPolicies() (or event just NonSDKUsage
        // suppression) results in a binder call which takes 1.2ms at the median. See
        // crbug/1454299#c21. So we just allow the violation to occur on Android P to Android U.
        try {
            if (sGetTimeNanoMethod == null) {
                sGetTimeNanoMethod = MotionEvent.class.getMethod("getEventTimeNano");
            }
            timeNs = (long) sGetTimeNanoMethod.invoke(event);
        } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            TraceEvent.instant("MotionEventUtils::getEventTimeNano error", e.toString());
            sFailedReflection = true;
            timeNs = event.getEventTime() * 1_000_000;
        }
        return timeNs;
    }

    private static long approximateNanosFromEvent(MotionEvent event, int pos) {
        return event.getHistoricalEventTime(pos) * 1_000_000;
    }

    private static boolean sFailedDoubleReflection;
    private static Method sGetHistoricalEventTimeNanoMethod;

    /**
     * Returns the time in nanoseconds, but with precision to milliseconds, of the given
     * MotionEvent. There is no SDK method which returns the event time in nanoseconds, pre Android
     * API 34 (Android U) so we just extend milliseconds to nanoseconds in that case.
     */
    public static long getHistoricalEventTimeNanos(MotionEvent event, int pos) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            return event.getHistoricalEventTimeNanos(pos);
        }
        if (sFailedDoubleReflection) {
            return approximateNanosFromEvent(event, pos);
        }
        try {
            // Before Android U the method was declared in core/java/android/view/MotionEvent.java
            // as "public final long getHistoricalEventTimeNano(int pos)". It was added in 2012. All
            // Android releases supported by Chrome should have it.
            if (sGetHistoricalEventTimeNanoMethod == null) {
                Method getDeclaredMethod =
                        Class.class.getDeclaredMethod(
                                "getDeclaredMethod", String.class, Class[].class);
                Class[] cArg = new Class[1];
                cArg[0] = int.class;
                Method method =
                        (Method)
                                getDeclaredMethod.invoke(
                                        event.getClass(), "getHistoricalEventTimeNano", cArg);
                method.setAccessible(true);
                sGetHistoricalEventTimeNanoMethod = method;
            }
            return (long) sGetHistoricalEventTimeNanoMethod.invoke(event, pos);
        } catch (Exception e) {
            TraceEvent.instant("MotionEventUtils::getHistoricalEventTimeNanos error", e.toString());
            sFailedDoubleReflection = true;
            return approximateNanosFromEvent(event, pos);
        }
    }

    /**
     * Checks if the motion event was generated by a touch (precisely speaking, any non-mouse
     * sources including fingers and styluses) or by a primary button click (mouse / trackpad etc).
     */
    public static boolean isTouchOrPrimaryButton(boolean fromMouse, int buttons) {
        return !fromMouse || isPrimaryButton(buttons);
    }

    /**
     * Checks if the motion event was generated by a primary button (from mouse, trackpad etc). This
     * button constant is not set in response to simple touches with a finger or stylus tip. The
     * user must actually push a button.
     */
    public static boolean isPrimaryButton(int buttons) {
        return (buttons & MotionEvent.BUTTON_PRIMARY) != 0;
    }
}