Determine prediction amount dynamically

This change adds a utility to calculate th right amount of prediction
needed for the device.

Bug: 232941452
Test: tested on sample app, confirmed that prediction works as expected
Change-Id: Ifc4fe25a08479873e15ef0d8793bb31dc0d683a4
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 d51dbf4..66da8cf 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
@@ -65,6 +65,6 @@
      * @return the new predictor instance
      */
     static @NonNull MotionEventPredictor newInstance(@NonNull View view) {
-        return new KalmanMotionEventPredictor();
+        return new KalmanMotionEventPredictor(view.getContext());
     }
 }
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
index 7a7ab0f..9a41bb5 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
@@ -18,20 +18,24 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 
+import android.content.Context;
 import android.view.MotionEvent;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.input.motionprediction.MotionEventPredictor;
+import androidx.input.motionprediction.utils.PredictionEstimator;
 
 /**
  */
 @RestrictTo(LIBRARY)
 public class KalmanMotionEventPredictor implements MotionEventPredictor {
     private MultiPointerPredictor mMultiPointerPredictor = new MultiPointerPredictor();
+    private PredictionEstimator mPredictionEstimator;
 
-    public KalmanMotionEventPredictor() {
+    public KalmanMotionEventPredictor(@NonNull Context context) {
+        mPredictionEstimator = new PredictionEstimator(context);
     }
 
     @Override
@@ -39,6 +43,7 @@
         if (mMultiPointerPredictor == null) {
             return;
         }
+        mPredictionEstimator.record(event);
         mMultiPointerPredictor.onTouchEvent(event);
     }
 
@@ -48,7 +53,8 @@
         if (mMultiPointerPredictor == null) {
             return null;
         }
-        return mMultiPointerPredictor.predict(1);
+        final int predictionTimeDelta = mPredictionEstimator.estimate();
+        return mMultiPointerPredictor.predict(predictionTimeDelta);
     }
 
     @Override
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/utils/PredictionEstimator.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/utils/PredictionEstimator.java
new file mode 100644
index 0000000..3ed9a1b
--- /dev/null
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/utils/PredictionEstimator.java
@@ -0,0 +1,140 @@
+/*
+ * 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.utils;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.SystemClock;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ */
+@SuppressWarnings("deprecation")
+@RestrictTo(LIBRARY)
+public class PredictionEstimator {
+    private static final int MAX_PREDICTION_MS = 32;
+    private static final int LEGACY_FRAME_TIME_MS = 16;
+    private static final int MS_IN_A_SECOND = 1000;
+
+    private long mLastEventTime = -1;
+    private final float mFrameTimeMs;
+
+    public PredictionEstimator(@NonNull Context context) {
+        mFrameTimeMs = getFastestFrameTimeMs(context);
+    }
+
+    /** Records the needed information from the event to calculate the prediction. */
+    public void record(@NonNull MotionEvent event) {
+        mLastEventTime = event.getEventTime();
+    }
+
+    /** Return the estimated amount of prediction needed. */
+    public int estimate() {
+        if (mLastEventTime <= 0) {
+            return (int) mFrameTimeMs;
+        }
+        // The amount of prediction is the estimated amount of time it will take to land the
+        // information on the screen from now, plus the time since the last recorded MotionEvent
+        int estimatedMs = (int) (SystemClock.uptimeMillis() - mLastEventTime + mFrameTimeMs);
+        return Math.min(MAX_PREDICTION_MS, estimatedMs);
+    }
+
+    private Display getDisplayForContext(Context context) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            return Api30Impl.getDisplayForContext(context);
+        }
+        return ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
+                .getDefaultDisplay();
+    }
+
+    private float getFastestFrameTimeMs(Context context) {
+        Display defaultDisplay = getDisplayForContext(context);
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return Api23Impl.getFastestFrameTimeMs(defaultDisplay);
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            return Api21Impl.getFastestFrameTimeMs(defaultDisplay);
+        } else {
+            return LEGACY_FRAME_TIME_MS;
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+    static class Api21Impl {
+        private Api21Impl() {
+            // Not instantiable
+        }
+
+        @DoNotInline
+        static float getFastestFrameTimeMs(Display display) {
+            float[] refreshRates = display.getSupportedRefreshRates();
+            float largestRefreshRate = refreshRates[0];
+
+            for (int c = 1; c < refreshRates.length; c++) {
+                if (refreshRates[c] > largestRefreshRate) {
+                    largestRefreshRate = refreshRates[c];
+                }
+            }
+
+            return MS_IN_A_SECOND / largestRefreshRate;
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.M)
+    static class Api23Impl {
+        private Api23Impl() {
+            // Not instantiable
+        }
+
+        @DoNotInline
+        static float getFastestFrameTimeMs(Display display) {
+            Display.Mode[] displayModes = display.getSupportedModes();
+            float largestRefreshRate = displayModes[0].getRefreshRate();
+
+            for (int c = 1; c < displayModes.length; c++) {
+                float currentRefreshRate = displayModes[c].getRefreshRate();
+                if (currentRefreshRate > largestRefreshRate) {
+                    largestRefreshRate = currentRefreshRate;
+                }
+            }
+
+            return MS_IN_A_SECOND / largestRefreshRate;
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.R)
+    static class Api30Impl {
+        private Api30Impl() {
+            // Not instantiable
+        }
+
+        @DoNotInline
+        static Display getDisplayForContext(Context context) {
+            return context.getDisplay();
+        }
+    }
+}