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);
+    }
+}
+