// 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;
}
}