Add support for system prediction
In Android U there is a system prediction API available; this CL
adds support for it to the Jetpack prediction library.
Bug: 232941452
Test: Used sample app on a supported device
Relnote: "Add support for Android U system prediction API"
Change-Id: I7261fd0bdcfe0283ff9edbb4e19940bc4731c83e
diff --git a/input/input-motionprediction/build.gradle b/input/input-motionprediction/build.gradle
index e5e6f105..08f5074 100644
--- a/input/input-motionprediction/build.gradle
+++ b/input/input-motionprediction/build.gradle
@@ -24,6 +24,8 @@
dependencies {
api("androidx.annotation:annotation:1.2.0")
+ implementation("androidx.core:core:1.10.1")
+
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
index e976419..a08bba9 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
@@ -16,12 +16,15 @@
package androidx.input.motionprediction;
+import android.content.Context;
+import android.os.Build;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.input.motionprediction.kalman.KalmanMotionEventPredictor;
+import androidx.input.motionprediction.system.SystemMotionEventPredictor;
/**
* There is a gap between the time a user touches the screen and that information is reported to the
@@ -37,6 +40,7 @@
* Record a user's movement to the predictor. You should call this for every
* {@link android.view.MotionEvent} that is received by the associated
* {@link android.view.View}.
+ *
* @param event the {@link android.view.MotionEvent} the associated view received and that
* needs to be recorded.
* @throws IllegalArgumentException if an inconsistent MotionEvent stream is sent.
@@ -45,6 +49,7 @@
/**
* Compute a prediction
+ *
* @return the predicted {@link android.view.MotionEvent}, or null if not possible to make a
* prediction.
*/
@@ -52,11 +57,24 @@
MotionEvent predict();
/**
- * Create a new motion predictor associated to a specific {@link android.view.View}
+ * Create a new motion predictor associated to a specific {@link android.view.View}.
+ *
+ * For devices running Android versions before U, the predicions are provided by a library based
+ * on a Kalman filter; from Android U, a system API is available, but predictions may not be
+ * supported for all strokes (for instance, it may be limited to stylus events). In these cases,
+ * the Kalman filter library will be used; to determine if a `MotionEvent` will be handled by
+ * the system prediction, use {@link android.view.MotionPredictor#isPredictionAvailable}.
+ *
* @param view the view to associated to this predictor
* @return the new predictor instance
*/
- static @NonNull MotionEventPredictor newInstance(@NonNull View view) {
- return new KalmanMotionEventPredictor(view.getContext());
+ @NonNull
+ static MotionEventPredictor newInstance(@NonNull View view) {
+ Context context = view.getContext();
+ if (Build.VERSION.SDK_INT >= 34) {
+ return SystemMotionEventPredictor.newInstance(context);
+ } else {
+ return new KalmanMotionEventPredictor(context);
+ }
}
}
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/system/SystemMotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/system/SystemMotionEventPredictor.java
new file mode 100644
index 0000000..7184156
--- /dev/null
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/system/SystemMotionEventPredictor.java
@@ -0,0 +1,102 @@
+/*
+ * 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 androidx.input.motionprediction.system;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.MotionPredictor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.input.motionprediction.MotionEventPredictor;
+import androidx.input.motionprediction.kalman.MultiPointerPredictor;
+import androidx.input.motionprediction.utils.PredictionEstimator;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ */
+@RestrictTo(LIBRARY)
+@RequiresApi(34)
+public class SystemMotionEventPredictor implements MotionEventPredictor {
+ private MultiPointerPredictor mKalmanPredictor = null;
+ private final MotionPredictor mSystemPredictor;
+ private final PredictionEstimator mPredictionEstimator;
+ private boolean mUsingSystemPredictor = true;
+ // Source is composed by flags, and -1 is not a valid value
+ private int mLastRecordedSource = -1;
+ // As of Android U, -2 is used internally as an invalid device id. Even if this would change
+ // at some point, the source is checked first, which means that it will never be read before
+ // it has been written with a valid id.
+ private int mLastRecordedDeviceId = -2;
+
+ public SystemMotionEventPredictor(@NonNull Context context) {
+ mPredictionEstimator = new PredictionEstimator(context);
+ mSystemPredictor = new MotionPredictor(context);
+ }
+
+ @Override
+ public void record(@NonNull MotionEvent event) {
+ mPredictionEstimator.record(event);
+ int source = event.getSource();
+ int deviceId = event.getDeviceId();
+ if (mLastRecordedSource != source || mLastRecordedDeviceId != deviceId) {
+ mUsingSystemPredictor = mSystemPredictor.isPredictionAvailable(deviceId, source);
+ mLastRecordedDeviceId = deviceId;
+ mLastRecordedSource = source;
+ }
+ if (mUsingSystemPredictor) {
+ mSystemPredictor.record(event);
+ } else {
+ getKalmanPredictor().onTouchEvent(event);
+ }
+ }
+
+ @Nullable
+ @Override
+ public MotionEvent predict() {
+ final int predictionTimeDelta = mPredictionEstimator.estimate();
+ if (mUsingSystemPredictor) {
+ return mSystemPredictor.predict(TimeUnit.MILLISECONDS.toNanos(predictionTimeDelta));
+ } else {
+ return getKalmanPredictor().predict(predictionTimeDelta);
+ }
+ }
+
+ private MultiPointerPredictor getKalmanPredictor() {
+ if (mKalmanPredictor == null) {
+ mKalmanPredictor = new MultiPointerPredictor();
+ }
+ return mKalmanPredictor;
+ }
+
+ /**
+ * Builds a new instance of the system motion event prediction
+ *
+ * @param context the application context
+ * @return the new instance
+ */
+ @NonNull
+ public static SystemMotionEventPredictor newInstance(@NonNull Context context) {
+ return new SystemMotionEventPredictor(context);
+ }
+}
+