chromium/ui/android/java/src/org/chromium/ui/base/WindowAndroid.java

// Copyright 2012 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.base;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.PendingIntent;
import android.app.UiModeManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Build;
import android.os.IBinder;
import android.os.Process;
import android.util.TypedValue;
import android.view.Display;
import android.view.Surface;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;

import org.jni_zero.CalledByNative;
import org.jni_zero.CalledByNativeForTesting;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.ActivityState;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.LifetimeAssert;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.PackageManagerUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.UnownedUserDataHost;
import org.chromium.ui.InsetObserver;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.display.DisplayAndroid;
import org.chromium.ui.display.DisplayAndroid.DisplayAndroidObserver;
import org.chromium.ui.gfx.OverlayTransform;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.permissions.AndroidPermissionDelegate;
import org.chromium.ui.permissions.PermissionCallback;
import org.chromium.ui.widget.Toast;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;

/** The window base class that has the minimum functionality. */
@JNINamespace("ui")
public class WindowAndroid implements AndroidPermissionDelegate, DisplayAndroidObserver {
    private static final String TAG = "WindowAndroid";
    private static final ImmutableWeakReference<Activity> NULL_ACTIVITY_WEAK_REF =
            new ImmutableWeakReference<>(null);

    // Arbitrary error margin to account for cases where the display's refresh rate might not
    // exactly match the target rate.
    private static final float MAX_REFRESH_RATE_DELTA = 2.f;

    private final LifetimeAssert mLifetimeAssert;
    private IntentRequestTrackerImpl mIntentRequestTracker;

    private KeyboardVisibilityDelegate mKeyboardVisibilityDelegate =
            KeyboardVisibilityDelegate.getInstance();

    private InsetObserver mInsetObserver;

    // Native pointer to the c++ WindowAndroid object.
    private long mNativeWindowAndroid;
    private final DisplayAndroid mDisplayAndroid;

    // A string used as a key to store intent errors in a bundle
    static final String WINDOW_CALLBACK_ERRORS = "window_callback_errors";

    // Error code returned when an Intent fails to start an Activity.
    public static final int START_INTENT_FAILURE = -1;

    private boolean mIsDestroyed;

    // We use a weak reference here to prevent this from leaking in WebView.
    private final ImmutableWeakReference<Context> mContextRef;

    // We track all animations over content and provide a drawing placeholder for them.
    private HashSet<Animator> mAnimationsOverContent = new HashSet<>();
    private View mAnimationPlaceholderView;

    /** A mechanism for observing and updating the application window's bottom inset. */
    private ApplicationViewportInsetSupplier mApplicationBottomInsetSupplier =
            new ApplicationViewportInsetSupplier();

    private AndroidPermissionDelegate mPermissionDelegate;

    // Note that this state lives in Java, rather than in the native BeginFrameSource because
    // clients may pause VSync before the native WindowAndroid is created.
    private boolean mVSyncPaused;

    // List of display modes with the same dimensions as the current mode but varying refresh rate.
    private List<Display.Mode> mSupportedRefreshRateModes;

    // A container for UnownedUserData objects that are not owned by, but can be accessed through
    // WindowAndroid.
    private final UnownedUserDataHost mUnownedUserDataHost = new UnownedUserDataHost();

    private float mRefreshRate;
    private boolean mHasFocus = true;
    private OverlayTransformApiHelper mOverlayTransformApiHelper;

    // The information required to draw a replica of the progress bar drawn in
    // java UI in composited UI.
    public static class ProgressBarConfig {
        // The background color of the progress bar.
        public int backgroundColor;
        // The height of both the progress indicator and the background.
        public int heightPhysical;
        // The color of the progress indicator.
        public int color;
        // The hairline drawn underneath the bottom edge of the progress bar.
        public int hairlineHeightPhysical;
        // The color of the hairline.
        public int hairlineColor;

        public static interface Provider {
            ProgressBarConfig getProgressBarConfig();
        }
    }

    private ProgressBarConfig.Provider mProgressBarConfigProvider;

    /** An interface to notify listeners that a context menu is closed. */
    public interface OnCloseContextMenuListener {
        /** Called when a context menu has been closed. */
        void onContextMenuClosed();
    }

    /** An interface to notify listeners of the changes in activity state. */
    public interface ActivityStateObserver {
        /** Called when the activity goes into paused state. */
        void onActivityPaused();

        /** Called when the activity goes into resumed state. */
        void onActivityResumed();

        /** Called when the activity goes into destroyed state. */
        void onActivityDestroyed();
    }

    private ObserverList<ActivityStateObserver> mActivityStateObservers = new ObserverList<>();

    /** An interface to notify listeners of the changes in selection handles state. */
    public interface SelectionHandlesObserver {
        /** Called when the selection handles state changes. */
        void onSelectionHandlesStateChanged(boolean active);
    }

    private boolean mSelectionHandlesActive;
    private ObserverList<SelectionHandlesObserver> mSelectionHandlesObservers =
            new ObserverList<>();

    private final boolean mAllowChangeRefreshRate;

    /** Gets the view for readback. */
    public View getReadbackView() {
        return null;
    }

    private final ObserverList<OnCloseContextMenuListener> mContextMenuCloseListeners =
            new ObserverList<>();

    private ModalDialogManager mModalDialogManagerForTesting;

    /**
     * @param context The application {@link Context}.
     */
    public WindowAndroid(Context context) {
        this(context, DisplayAndroid.getNonMultiDisplay(context));
    }

    protected WindowAndroid(Context context, IntentRequestTracker tracker) {
        this(context, DisplayAndroid.getNonMultiDisplay(context));
        mIntentRequestTracker = (IntentRequestTrackerImpl) tracker;
    }

    /**
     * @param context The application {@link Context}.
     * @param display The application {@link DisplayAndroid}.
     */
    @SuppressLint("UseSparseArrays")
    protected WindowAndroid(Context context, DisplayAndroid display) {
        mLifetimeAssert = LifetimeAssert.create(this);
        // context does not have the same lifetime guarantees as an application context so we can't
        // hold a strong reference to it.
        mContextRef = new ImmutableWeakReference<>(context);
        mDisplayAndroid = display;
        mDisplayAndroid.addObserver(this);

        // Using this setting is gated to Q due to bugs on Razer phones which can freeze the device
        // if the API is used. See crbug.com/990646.
        // Disable refresh rate change on TV platforms, as it may cause black screen flicker due to
        // display mode changes.
        mAllowChangeRefreshRate = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !isTv(context);

        // Multiple refresh rate support is only available on M+.
        recomputeSupportedRefreshRates();

        // Configuration.isDisplayServerWideColorGamut must be queried from the window's context.
        // Because of crbug.com/756180, many devices report true for isScreenWideColorGamut in
        // 8.0.0, even when they don't actually support wide color gamut.
        // TODO(boliu): Observe configuration changes to update the value of isScreenWideColorGamut.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
                && !Build.VERSION.RELEASE.equals("8.0.0")
                && ContextUtils.activityFromContext(context) != null) {
            Configuration configuration = context.getResources().getConfiguration();
            boolean isScreenWideColorGamut = configuration.isScreenWideColorGamut();
            display.updateIsDisplayServerWideColorGamut(isScreenWideColorGamut);
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) {
            mOverlayTransformApiHelper = OverlayTransformApiHelper.create(this);
        }
    }

    private static boolean isTv(Context context) {
        UiModeManager uiModeManager =
                (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
        return uiModeManager != null
                && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
    }

    @CalledByNativeForTesting
    private static long createForTesting() {
        WindowAndroid windowAndroid = new WindowAndroid(ContextUtils.getApplicationContext());
        // |windowAndroid.getNativePointer()| creates native WindowAndroid object
        // which stores a global ref to |windowAndroid|. Therefore |windowAndroid|
        // is not immediately eligible for gc.
        return windowAndroid.getNativePointer();
    }

    @CalledByNative
    private void clearNativePointer() {
        mNativeWindowAndroid = 0;
    }

    /** Set the delegate that will handle android permissions requests. */
    public void setAndroidPermissionDelegate(AndroidPermissionDelegate delegate) {
        mPermissionDelegate = delegate;
    }

    /** Gets the {@link IntentRequestTracker} associated with the WindowAndroid's activity. */
    @Nullable
    public final IntentRequestTracker getIntentRequestTracker() {
        if (mIntentRequestTracker == null) {
            Log.w(
                    TAG,
                    "Cannot get IntentRequestTracker as the WindowAndroid is neither "
                            + "a ActivityWindowAndroid or a FragmentWindowAndroid.");
        }
        return mIntentRequestTracker;
    }

    /** Set the provider to pull the progress bar config from */
    public final void setProgressBarConfigProvider(ProgressBarConfig.Provider provider) {
        mProgressBarConfigProvider = provider;
    }

    /**
     * Shows an intent and returns the results to the callback object.
     *
     * @param intent The PendingIntent that needs to be shown.
     * @param callback The object that will receive the results for the intent.
     * @param errorId The ID of error string to be shown if activity is paused before intent
     *     results, or null if no message is required.
     * @return Whether the intent was shown.
     */
    public boolean showIntent(PendingIntent intent, IntentCallback callback, Integer errorId) {
        if (mIntentRequestTracker == null) {
            Log.d(TAG, "Can't show intent as context is not an Activity: " + intent);
            return false;
        }
        return mIntentRequestTracker.showCancelableIntent(intent, callback, errorId) >= 0;
    }

    /**
     * Shows an intent and returns the results to the callback object.
     * @param intent   The intent that needs to be shown.
     * @param callback The object that will receive the results for the intent.
     * @param errorId  The ID of error string to be shown if activity is paused before intent
     *                 results, or null if no message is required.
     * @return Whether the intent was shown.
     */
    public boolean showIntent(Intent intent, IntentCallback callback, Integer errorId) {
        if (mIntentRequestTracker == null) {
            Log.d(TAG, "Can't show intent as context is not an Activity: " + intent);
            return false;
        }
        return mIntentRequestTracker.showCancelableIntent(intent, callback, errorId) >= 0;
    }

    /**
     * Shows an intent that could be canceled and returns the results to the callback object.
     * @param  intent   The PendingIntent that needs to be shown.
     * @param  callback The object that will receive the results for the intent.
     * @param  errorId  The ID of error string to be shown if activity is paused before intent
     *                  results, or null if no message is required.
     * @return A non-negative request code that could be used for finishActivity, or
     *         START_INTENT_FAILURE if failed.
     */
    public int showCancelableIntent(
            PendingIntent intent, IntentCallback callback, Integer errorId) {
        if (mIntentRequestTracker == null) {
            Log.d(TAG, "Can't show intent as context is not an Activity: " + intent);
            return START_INTENT_FAILURE;
        }
        return mIntentRequestTracker.showCancelableIntent(intent, callback, errorId);
    }

    /**
     * Shows an intent that could be canceled and returns the results to the callback object.
     * @param  intent   The intent that needs to be shown.
     * @param  callback The object that will receive the results for the intent.
     * @param  errorId  The ID of error string to be shown if activity is paused before intent
     *                  results, or null if no message is required.
     * @return A non-negative request code that could be used for finishActivity, or
     *         START_INTENT_FAILURE if failed.
     */
    public int showCancelableIntent(Intent intent, IntentCallback callback, Integer errorId) {
        if (mIntentRequestTracker == null) {
            Log.d(TAG, "Can't show intent as context is not an Activity: " + intent);
            return START_INTENT_FAILURE;
        }
        return mIntentRequestTracker.showCancelableIntent(intent, callback, errorId);
    }

    public int showCancelableIntent(
            Callback<Integer> intentTrigger, IntentCallback callback, Integer errorId) {
        if (mIntentRequestTracker == null) {
            Log.d(TAG, "Can't show intent as context is not an Activity");
            return START_INTENT_FAILURE;
        }
        return mIntentRequestTracker.showCancelableIntent(intentTrigger, callback, errorId);
    }

    /**
     * Force finish another activity that you had previously started with showCancelableIntent.
     * @param requestCode The request code returned from showCancelableIntent.
     */
    public void cancelIntent(int requestCode) {
        if (mIntentRequestTracker == null) {
            Log.d(TAG, "Can't cancel intent as context is not an Activity: " + requestCode);
            return;
        }
        mIntentRequestTracker.cancelIntent(requestCode);
    }

    /**
     * Removes a callback from the list of pending intents, so that nothing happens if/when the
     * result for that intent is received.
     *
     * @param callback The object that should have received the results
     * @return True if the callback was removed, false if it was not found.
     */
    public boolean removeIntentCallback(IntentCallback callback) {
        if (mIntentRequestTracker == null) return false;
        return mIntentRequestTracker.removeIntentCallback(callback);
    }

    /**
     * Determine whether access to a particular permission is granted.
     * @param permission The permission whose access is to be checked.
     * @return Whether access to the permission is granted.
     */
    @CalledByNative
    @Override
    public boolean hasPermission(String permission) {
        if (mPermissionDelegate != null) return mPermissionDelegate.hasPermission(permission);

        return ApiCompatibilityUtils.checkPermission(
                        ContextUtils.getApplicationContext(),
                        permission,
                        Process.myPid(),
                        Process.myUid())
                == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * Determine whether the specified permission can be requested.
     *
     * <p>
     * A permission can be requested in the following states:
     * 1.) Default un-granted state, permission can be requested
     * 2.) Permission previously requested but denied by the user, but the user did not select
     *     "Never ask again".
     *
     * @param permission The permission name.
     * @return Whether the requesting the permission is allowed.
     */
    @CalledByNative
    @Override
    public boolean canRequestPermission(String permission) {
        if (mPermissionDelegate != null) {
            return mPermissionDelegate.canRequestPermission(permission);
        }

        Log.w(
                TAG,
                "Cannot determine the request permission state as the context "
                        + "is not an Activity");
        assert false
                : "Failed to determine the request permission state using a WindowAndroid "
                        + "without an Activity";
        return false;
    }

    /**
     * Determine whether the specified permission is revoked by policy.
     *
     * @param permission The permission name.
     * @return Whether the permission is revoked by policy and the user has no ability to change it.
     */
    @Override
    public boolean isPermissionRevokedByPolicy(String permission) {
        if (mPermissionDelegate != null) {
            return mPermissionDelegate.isPermissionRevokedByPolicy(permission);
        }

        Log.w(
                TAG,
                "Cannot determine the policy permission state as the context "
                        + "is not an Activity");
        assert false
                : "Failed to determine the policy permission state using a WindowAndroid "
                        + "without an Activity";
        return false;
    }

    /**
     * Requests the specified permissions are granted for further use.
     * @param permissions The list of permissions to request access to.
     * @param callback The callback to be notified whether the permissions were granted.
     */
    @Override
    public void requestPermissions(String[] permissions, PermissionCallback callback) {
        if (mPermissionDelegate != null) {
            mPermissionDelegate.requestPermissions(permissions, callback);
            return;
        }

        Log.w(TAG, "Cannot request permissions as the context is not an Activity");
        assert false : "Failed to request permissions using a WindowAndroid without an Activity";
    }

    @Override
    public boolean handlePermissionResult(
            int requestCode, String[] permissions, int[] grantResults) {
        if (mPermissionDelegate != null) {
            return mPermissionDelegate.handlePermissionResult(
                    requestCode, permissions, grantResults);
        }
        return false;
    }

    @Override
    public boolean shouldShowRequestPermissionRationale(String permission) {
        return mPermissionDelegate != null
                && mPermissionDelegate.shouldShowRequestPermissionRationale(permission);
    }

    /**
     * Displays an error message with a provided error message string.
     * @param error The error message string to be displayed.
     */
    public static void showError(String error) {
        if (error != null) {
            Toast.makeText(ContextUtils.getApplicationContext(), error, Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * Displays an error message from the given resource id.
     * @param resId The error message string's resource id.
     */
    public static void showError(int resId) {
        showError(ContextUtils.getApplicationContext().getString(resId));
    }

    /** Broadcasts the given intent to all interested BroadcastReceivers. */
    public void sendBroadcast(Intent intent) {
        ContextUtils.getApplicationContext().sendBroadcast(intent);
    }

    /**
     * @return DisplayAndroid instance belong to this window.
     */
    public DisplayAndroid getDisplay() {
        return mDisplayAndroid;
    }

    /**
     * @return A reference to owning Activity.  The returned WeakReference will never be null, but
     *         the contained Activity can be null (either if it has been garbage collected or if
     *         this is in the context of a WebView that was not created using an Activity).
     *         The returned WeakReference is immutable and calling clear will throw an exception.
     */
    public WeakReference<Activity> getActivity() {
        return NULL_ACTIVITY_WEAK_REF;
    }

    /**
     * @return The application context for this activity.
     */
    public Context getApplicationContext() {
        return ContextUtils.getApplicationContext();
    }

    /**
     * Notify any observers that the visibility of the Android Window associated
     * with this Window has changed.
     * @param visible whether the View is visible.
     */
    public void onVisibilityChanged(boolean visible) {
        if (mNativeWindowAndroid == 0) return;
        WindowAndroidJni.get()
                .onVisibilityChanged(mNativeWindowAndroid, WindowAndroid.this, visible);
    }

    /**
     * For window instances associated with an activity, notifies any listeners
     * that the activity has been stopped.
     */
    protected void onActivityStopped() {
        if (mNativeWindowAndroid == 0) return;
        WindowAndroidJni.get().onActivityStopped(mNativeWindowAndroid, WindowAndroid.this);
    }

    /**
     * For window instances associated with an activity, notifies any listeners
     * that the activity has been started.
     */
    protected void onActivityStarted() {
        if (mNativeWindowAndroid == 0) return;
        WindowAndroidJni.get().onActivityStarted(mNativeWindowAndroid, WindowAndroid.this);
    }

    protected void onActivityPaused() {
        for (ActivityStateObserver observer : mActivityStateObservers) observer.onActivityPaused();
    }

    protected void onActivityResumed() {
        for (ActivityStateObserver observer : mActivityStateObservers) observer.onActivityResumed();
    }

    protected void onActivityDestroyed() {
        for (ActivityStateObserver observer : mActivityStateObservers) {
            observer.onActivityDestroyed();
        }
    }

    /** Adds a new {@link ActivityStateObserver} instance. */
    public void addActivityStateObserver(ActivityStateObserver observer) {
        assert !mActivityStateObservers.hasObserver(observer);
        mActivityStateObservers.addObserver(observer);
    }

    /** Removes a new {@link ActivityStateObserver} instance. */
    public void removeActivityStateObserver(ActivityStateObserver observer) {
        assert mActivityStateObservers.hasObserver(observer);
        mActivityStateObservers.removeObserver(observer);
    }

    public void addSelectionHandlesObserver(SelectionHandlesObserver observer) {
        assert !mSelectionHandlesObservers.hasObserver(observer);
        mSelectionHandlesObservers.addObserver(observer);
        observer.onSelectionHandlesStateChanged(mSelectionHandlesActive);
    }

    public void removeSelectionHandlesObserver(SelectionHandlesObserver observer) {
        assert mSelectionHandlesObservers.hasObserver(observer);
        mSelectionHandlesObservers.removeObserver(observer);
    }

    /** Removes a new {@link ActivityStateObserver} instance. */
    @CalledByNative
    private void onSelectionHandlesStateChanged(boolean active) {
        mSelectionHandlesActive = active;
        for (SelectionHandlesObserver observer : mSelectionHandlesObservers) {
            observer.onSelectionHandlesStateChanged(active);
        }
    }

    @CalledByNative
    private int[] getProgressBarConfig() {
        int[] result = new int[5];

        if (mProgressBarConfigProvider == null) {
            Arrays.fill(result, 0);
            return result;
        }

        ProgressBarConfig config = mProgressBarConfigProvider.getProgressBarConfig();
        result[0] = config.backgroundColor;
        result[1] = config.heightPhysical;
        result[2] = config.color;
        result[3] = config.hairlineHeightPhysical;
        result[4] = config.hairlineColor;
        return result;
    }

    /**
     * @return Current state of the associated {@link Activity}. Can be overridden to return the
     *     correct state. {@code ActivityState.DESTROYED} by default.
     */
    @ActivityState
    public int getActivityState() {
        return ActivityState.DESTROYED;
    }

    /** An interface that intent callback objects have to implement. */
    public interface IntentCallback {
        /**
         * Handles the data returned by the requested intent.
         * @param resultCode Result code of the requested intent.
         * @param data The data returned by the intent.
         */
        void onIntentCompleted(int resultCode, Intent data);
    }

    /**
     * Tests that an activity is available to handle the passed in intent.
     * @param  intent The intent to check.
     * @return True if an activity is available to process this intent when started, meaning that
     *         Context.startActivity will not throw ActivityNotFoundException.
     */
    public boolean canResolveActivity(Intent intent) {
        return PackageManagerUtils.canResolveActivity(intent);
    }

    /**
     * Returns the ModalDialogManager to be used with this window.
     *
     * @return a {@link ModalDialogManager} for this window, or null if there is none.
     */
    public @Nullable ModalDialogManager getModalDialogManager() {
        // Will always be null except for some tests.
        return mModalDialogManagerForTesting;
    }

    /** Set the ModalDialogManager for testing. */
    @CalledByNativeForTesting
    private void setModalDialogManagerForTesting(ModalDialogManager modalDialogManager) {
        mModalDialogManagerForTesting = modalDialogManager;
    }

    /**
     * @return Whether this instance is destroyed.
     */
    public boolean isDestroyed() {
        return mIsDestroyed;
    }

    /** Destroys the c++ WindowAndroid object if one has been created. */
    @CalledByNative
    public void destroy() {
        LifetimeAssert.setSafeToGc(mLifetimeAssert, true);
        mIsDestroyed = true;
        mDisplayAndroid.removeObserver(this);
        if (mNativeWindowAndroid != 0) {
            // Native code clears |mNativeWindowAndroid|.
            WindowAndroidJni.get().destroy(mNativeWindowAndroid, WindowAndroid.this);
        }

        mUnownedUserDataHost.destroy();
        mApplicationBottomInsetSupplier.destroy();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) {
            if (mOverlayTransformApiHelper != null) {
                mOverlayTransformApiHelper.destroy();
            }
        }
    }

    /**
     * Returns a pointer to the c++ AndroidWindow object and calls the initializer if
     * the object has not been previously initialized.
     * @return A pointer to the c++ AndroidWindow.
     */
    @CalledByNative
    private long getNativePointer() {
        if (mNativeWindowAndroid == 0) {
            mNativeWindowAndroid =
                    WindowAndroidJni.get()
                            .init(
                                    WindowAndroid.this,
                                    mDisplayAndroid.getDisplayId(),
                                    getMouseWheelScrollFactor(),
                                    getWindowIsWideColorGamut());
            WindowAndroidJni.get()
                    .setVSyncPaused(mNativeWindowAndroid, WindowAndroid.this, mVSyncPaused);
        }
        return mNativeWindowAndroid;
    }

    /**
     * Returns current wheel scroll factor (physical pixels per mouse scroll click).
     * @return wheel scroll factor or zero if attr retrieval fails.
     */
    private float getMouseWheelScrollFactor() {
        TypedValue outValue = new TypedValue();
        Context context = getContext().get();
        if (context != null
                && context.getTheme()
                        .resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true)) {
            // This is the same attribute used by Android Views to scale wheel
            // event motion into scroll deltas.
            return outValue.getDimension(context.getResources().getDisplayMetrics());
        }
        return 0;
    }

    // Helper to get the android Window. Always null for application context. Need to null check
    // result returning value.
    @Nullable
    public Window getWindow() {
        Activity activity = ContextUtils.activityFromContext(mContextRef.get());
        if (activity == null || activity.isFinishing()) return null;
        return activity.getWindow();
    }

    // This is android.view.Window.isWideColorGamut, which is only set if the passed in Context is
    // an Activity. This is normally not needed for apps which can decide whether to enable wide
    // gamut (on supported hardware and os). However it is important for embedders like WebView
    // which do not make the wide gamut decision to check this at run time.
    private boolean getWindowIsWideColorGamut() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return false;
        Window window = getWindow();
        if (window == null) return false;
        return window.isWideColorGamut();
    }

    /**
     * Set the animation placeholder view, which we set to 'draw' during animations, such that
     * animations aren't clipped by the SurfaceView 'hole'. This can be the SurfaceView itself or a
     * view directly on top of it. This could be extended to many views if we ever need it.
     */
    public void setAnimationPlaceholderView(View view) {
        mAnimationPlaceholderView = view;
    }

    protected void setKeyboardDelegate(KeyboardVisibilityDelegate keyboardDelegate) {
        mKeyboardVisibilityDelegate = keyboardDelegate;
        // TODO(crbug.com/343936788): Remove - callers should use the window to get the delegate.
        KeyboardVisibilityDelegate.setInstance(keyboardDelegate);
    }

    /**
     * The returned {@link KeyboardVisibilityDelegate} can read and influence the soft keyboard.
     *
     * @return a {@link KeyboardVisibilityDelegate} specific for this window.
     */
    public KeyboardVisibilityDelegate getKeyboardDelegate() {
        return mKeyboardVisibilityDelegate;
    }

    /** Returns the {@link InsetObserver} for the root view of the activity or null. */
    public InsetObserver getInsetObserver() {
        if (mInsetObserver == null) {
            Window window = getWindow();
            if (window == null) return null;

            mInsetObserver =
                    new InsetObserver(
                            new ImmutableWeakReference<>(window.getDecorView().getRootView()));
        }
        return mInsetObserver;
    }

    /**
     * @return A mechanism for updating and observing the bottom inset of the browser window.
     */
    public ApplicationViewportInsetSupplier getApplicationBottomInsetSupplier() {
        return mApplicationBottomInsetSupplier;
    }

    /** Adds a listener that will be notified whenever a ContextMenu is closed. */
    public void addContextMenuCloseListener(OnCloseContextMenuListener listener) {
        mContextMenuCloseListeners.addObserver(listener);
    }

    /**
     * Removes a listener from the list of listeners that will be notified when a
     * ContextMenu is closed.
     */
    public void removeContextMenuCloseListener(OnCloseContextMenuListener listener) {
        mContextMenuCloseListeners.removeObserver(listener);
    }

    /**
     * This hook is called whenever the context menu is being closed (either by
     * the user canceling the menu with the back/menu button, or when an item is
     * selected).
     */
    public void onContextMenuClosed() {
        for (OnCloseContextMenuListener listener : mContextMenuCloseListeners) {
            listener.onContextMenuClosed();
        }
    }

    /**
     * Start a post-layout animation on top of web content.
     *
     * By default, Android optimizes what it shows on top of SurfaceViews (saves power).
     * Effectively, layouts determine what gets drawn and post-layout animations outside
     * of this area may be 'clipped'. Using this method to start such animations will
     * ensure that nothing is clipped during the animation, and restore the optimal
     * state when the animation ends.
     */
    public void startAnimationOverContent(Animator animation) {
        // We may not need an animation placeholder (eg. Webview doesn't use SurfaceView)
        if (mAnimationPlaceholderView == null) return;
        if (animation.isStarted()) throw new IllegalArgumentException("Already started.");
        boolean added = mAnimationsOverContent.add(animation);
        if (!added) throw new IllegalArgumentException("Already Added.");

        // We start the animation in this method to help guarantee that we never get stuck in this
        // state or leak objects into the set. Starting the animation here should guarantee that we
        // get an onAnimationEnd callback, and remove this animation.
        animation.start();

        // When the first animation starts, make the placeholder 'draw' itself.
        if (mAnimationPlaceholderView.willNotDraw()) {
            mAnimationPlaceholderView.setWillNotDraw(false);
        }

        // When the last animation ends, remove the placeholder view,
        // returning to the default optimized state.
        animation.addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        animation.removeListener(this);
                        mAnimationsOverContent.remove(animation);
                        if (mAnimationsOverContent.isEmpty()) {
                            mAnimationPlaceholderView.setWillNotDraw(true);
                        }
                    }
                });
    }

    /**
     * Getter for the current context (not necessarily the application context).
     * Make no assumptions regarding what type of Context is returned here, it could be for example
     * an Activity or a Context created specifically to target an external display.
     * The returned WeakReference is immutable and calling clear will throw an exception.
     */
    public WeakReference<Context> getContext() {
        return mContextRef;
    }

    /** Return the current window token, or null. */
    public IBinder getWindowToken() {
        Window window = getWindow();
        if (window == null) return null;
        View decorView = window.peekDecorView();
        if (decorView == null) return null;
        return decorView.getWindowToken();
    }

    /**
     * As long as there are still animations which haven't ended, this will return false.
     * @return True if all known animations have ended.
     */
    @VisibleForTesting
    public boolean haveAnimationsEnded() {
        return mAnimationsOverContent.isEmpty();
    }

    /**
     * Pauses/Unpauses VSync. When VSync is paused the compositor for this window will idle, and
     * requestAnimationFrame callbacks won't fire, etc.
     */
    public void setVSyncPaused(boolean paused) {
        if (mVSyncPaused == paused) return;
        mVSyncPaused = paused;
        if (mNativeWindowAndroid != 0) {
            WindowAndroidJni.get().setVSyncPaused(mNativeWindowAndroid, WindowAndroid.this, paused);
        }
    }

    @Override
    public void onRefreshRateChanged(float refreshRate) {
        if (mNativeWindowAndroid != 0) {
            WindowAndroidJni.get()
                    .onUpdateRefreshRate(mNativeWindowAndroid, WindowAndroid.this, refreshRate);
        }
    }

    protected void onWindowFocusChanged(boolean hasFocus) {
        mHasFocus = hasFocus;
        if (!mHasFocus) {
            // `preferredDisplayModeId` affects other windows even when this window is not in focus,
            // so reset to no preference when not in focus.
            doSetPreferredRefreshRate(0);
        } else {
            doSetPreferredRefreshRate(mRefreshRate);
        }
    }

    @Override
    public void onCurrentModeChanged(Display.Mode currentMode) {
        recomputeSupportedRefreshRates();
    }

    @Override
    public void onDisplayModesChanged(List<Display.Mode> supportedModes) {
        recomputeSupportedRefreshRates();
    }

    @RequiresApi(Build.VERSION_CODES.O)
    @CalledByNative
    public void setWideColorEnabled(boolean enabled) {
        // Although this API was added in Android O, it was buggy.
        // Restrict to Android Q, where it was fixed.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            assert !enabled;
            return;
        }
        Window window = getWindow();
        if (window == null) return;

        int colorMode =
                enabled
                        ? ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
                        : ActivityInfo.COLOR_MODE_DEFAULT;
        window.setColorMode(colorMode);
    }

    private void recomputeSupportedRefreshRates() {
        Display.Mode currentMode = mDisplayAndroid.getCurrentMode();
        List<Display.Mode> supportedModes = mDisplayAndroid.getSupportedModes();
        // Note: getCurrentMode() and getSupportedModes() can return null in some situations - see
        // crbug.com/1401514.
        if (currentMode == null || supportedModes == null || supportedModes.size() == 0) {
            return;
        }

        List<Display.Mode> supportedRefreshRateModes = new ArrayList<Display.Mode>();
        for (int i = 0; i < supportedModes.size(); ++i) {
            if (currentMode.equals(supportedModes.get(i))) {
                supportedRefreshRateModes.add(supportedModes.get(i));
                continue;
            }

            // Only advertise refresh rates which wouldn't change other configurations on the
            // Display.
            if (currentMode.getPhysicalWidth() == supportedModes.get(i).getPhysicalWidth()
                    && currentMode.getPhysicalHeight() == supportedModes.get(i).getPhysicalHeight()
                    && currentMode.getRefreshRate() != supportedModes.get(i).getRefreshRate()) {
                supportedRefreshRateModes.add(supportedModes.get(i));
                continue;
            }
        }

        boolean changed = !supportedRefreshRateModes.equals(mSupportedRefreshRateModes);
        if (changed) {
            mSupportedRefreshRateModes = supportedRefreshRateModes;
            if (mNativeWindowAndroid != 0) {
                WindowAndroidJni.get()
                        .onSupportedRefreshRatesUpdated(
                                mNativeWindowAndroid,
                                WindowAndroid.this,
                                getSupportedRefreshRates());
            }
        }
    }

    @CalledByNative
    private float getRefreshRate() {
        return mDisplayAndroid.getRefreshRate();
    }

    @SuppressLint("NewApi")
    // mSupportedRefreshRateModes should only be set if Display.Mode is available.
    @CalledByNative
    private float[] getSupportedRefreshRates() {
        if (mSupportedRefreshRateModes == null || !mAllowChangeRefreshRate) return null;

        float[] supportedRefreshRates = new float[mSupportedRefreshRateModes.size()];
        for (int i = 0; i < mSupportedRefreshRateModes.size(); ++i) {
            supportedRefreshRates[i] = mSupportedRefreshRateModes.get(i).getRefreshRate();
        }
        return supportedRefreshRates;
    }

    @SuppressLint("NewApi")
    @CalledByNative
    private void setPreferredRefreshRate(float preferredRefreshRate) {
        mRefreshRate = preferredRefreshRate;
        if (mHasFocus) doSetPreferredRefreshRate(preferredRefreshRate);
    }

    private void doSetPreferredRefreshRate(float preferredRefreshRate) {
        if (mSupportedRefreshRateModes == null || !mAllowChangeRefreshRate) return;

        int preferredModeId = getPreferredModeId(preferredRefreshRate);
        Window window = getWindow();
        if (window == null) return;
        WindowManager.LayoutParams params = window.getAttributes();
        if (params.preferredDisplayModeId == preferredModeId) return;

        params.preferredDisplayModeId = preferredModeId;
        window.setAttributes(params);
    }

    @SuppressLint("NewApi")
    // mSupportedRefreshRateModes should only be set if Display.Mode is available.
    private int getPreferredModeId(float preferredRefreshRate) {
        if (preferredRefreshRate == 0) return 0;

        Display.Mode preferredMode = null;
        float preferredModeDelta = Float.MAX_VALUE;

        for (int i = 0; i < mSupportedRefreshRateModes.size(); ++i) {
            Display.Mode mode = mSupportedRefreshRateModes.get(i);
            float delta = Math.abs(preferredRefreshRate - mode.getRefreshRate());
            if (delta < preferredModeDelta) {
                preferredModeDelta = delta;
                preferredMode = mode;
            }
        }

        if (preferredModeDelta > MAX_REFRESH_RATE_DELTA) {
            Log.e(TAG, "Refresh rate not supported : " + preferredRefreshRate);
            return 0;
        }

        return preferredMode.getModeId();
    }

    /**
     * @return The {@link UnownedUserDataHost} attached to the current {@link WindowAndroid}.
     */
    public UnownedUserDataHost getUnownedUserDataHost() {
        return mUnownedUserDataHost;
    }

    void onOverlayTransformUpdated() {
        if (mNativeWindowAndroid != 0) {
            WindowAndroidJni.get().onOverlayTransformUpdated(mNativeWindowAndroid, this);
        }
    }

    @CalledByNative
    private @OverlayTransform int getOverlayTransform() {
        int overlayTransform = OverlayTransform.INVALID;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2
                && mOverlayTransformApiHelper != null) {
            overlayTransform = mOverlayTransformApiHelper.getOverlayTransform();
        }
        // Fallback to display rotation
        if (overlayTransform == OverlayTransform.INVALID) {
            switch (mDisplayAndroid.getRotation()) {
                case Surface.ROTATION_0:
                    return OverlayTransform.NONE;
                case Surface.ROTATION_90:
                    return OverlayTransform.ROTATE_CLOCKWISE_90;
                case Surface.ROTATION_180:
                    return OverlayTransform.ROTATE_CLOCKWISE_180;
                case Surface.ROTATION_270:
                    return OverlayTransform.ROTATE_CLOCKWISE_270;
                default:
                    return OverlayTransform.NONE;
            }
        }
        return overlayTransform;
    }

    public void setUnfoldLatencyBeginTime(long beginTimestampMs) {
        try (TraceEvent e = TraceEvent.scoped("setUnfoldLatencyBeginTime")) {
            if (mNativeWindowAndroid != 0) {
                WindowAndroidJni.get()
                        .sendUnfoldLatencyBeginTimestamp(mNativeWindowAndroid, beginTimestampMs);
            }
        }
    }

    @NativeMethods
    interface Natives {
        long init(
                WindowAndroid caller,
                int displayId,
                float scrollFactor,
                boolean windowIsWideColorGamut);

        void onVisibilityChanged(long nativeWindowAndroid, WindowAndroid caller, boolean visible);

        void onActivityStopped(long nativeWindowAndroid, WindowAndroid caller);

        void onActivityStarted(long nativeWindowAndroid, WindowAndroid caller);

        void setVSyncPaused(long nativeWindowAndroid, WindowAndroid caller, boolean paused);

        void onUpdateRefreshRate(long nativeWindowAndroid, WindowAndroid caller, float refreshRate);

        void destroy(long nativeWindowAndroid, WindowAndroid caller);

        void onSupportedRefreshRatesUpdated(
                long nativeWindowAndroid, WindowAndroid caller, float[] supportedRefreshRates);

        void onOverlayTransformUpdated(long nativeWindowAndroid, WindowAndroid caller);

        void sendUnfoldLatencyBeginTimestamp(long nativeWindowAndroid, long beginTimestampMs);
    }
}