PredictionEstimator.java

/*
 * 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();
        }
    }
}