/*
 * Copyright (C) 2018 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 android.view;

import static android.app.UiModeManager.FORCE_INVERT_TYPE_DARK;
import static android.app.UiModeManager.MODE_NIGHT_YES;
import static android.util.SequenceUtils.getInitSeq;
import static android.view.HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING;
import static android.view.InputDevice.SOURCE_ROTARY_ENCODER;
import static android.view.Surface.FRAME_RATE_CATEGORY_DEFAULT;
import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH;
import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT;
import static android.view.Surface.FRAME_RATE_CATEGORY_LOW;
import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
import static android.view.Surface.FRAME_RATE_COMPATIBILITY_AT_LEAST;
import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
import static android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
import static android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
import static android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE;
import static android.view.WindowInsetsController.APPEARANCE_OPAQUE_STATUS_BARS;
import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
import static android.view.accessibility.Flags.FLAG_FORCE_INVERT_COLOR;
import static android.view.flags.Flags.FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY;
import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
import static android.view.flags.Flags.toolkitFrameRateBySizeReadOnly;

import static com.android.cts.input.inputeventmatchers.InputEventMatchersKt.withKeyCode;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.annotation.NonNull;
import android.app.Instrumentation;
import android.app.UiModeManager;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.ForceDarkType;
import android.graphics.Rect;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Binder;
import android.os.VibrationAttributes;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.sysprop.ViewProperties;
import android.testing.TestableContext;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.MergedConfiguration;
import android.util.SequenceUtils;
import android.view.WindowInsets.Side;
import android.view.WindowInsets.Type;
import android.view.accessibility.AccessibilityManager;
import android.window.ClientWindowFrames;

import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.compatibility.common.util.ShellIdentityUtils;
import com.android.compatibility.common.util.TestUtils;
import com.android.cts.input.BlockingQueueEventVerifier;
import com.android.frameworks.coretests.R;
import com.android.window.flags.Flags;

import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.lang.reflect.Field;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Tests for {@link ViewRootImpl}
 *
 * Build/Install/Run:
 *  atest FrameworksCoreTests:ViewRootImplTest
 */
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ViewRootImplTest {
    private static final String TAG = "ViewRootImplTest";

    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    private ViewRootImpl mViewRootImpl;
    private View mView;

    private static Context sContext;
    private static Instrumentation sInstrumentation = InstrumentationRegistry.getInstrumentation();

    // The touch mode state before the test was started, needed to return the system to the original
    // state after the test completes.
    private static boolean sOriginalTouchMode;

    private CountDownLatch mAfterDrawLatch;
    private Throwable mAfterDrawThrowable;
    private native boolean nativeCreateASurfaceControlFromSurface(Surface surface);
    static {
        System.loadLibrary("viewRootImplTest_jni");
    }

    @Rule
    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();

    @BeforeClass
    public static void setUpClass() {
        sContext = sInstrumentation.getTargetContext();
        View view = new View(sContext);
        sOriginalTouchMode = view.isInTouchMode();
    }

    @AfterClass
    public static void tearDownClass() {
        sInstrumentation.setInTouchMode(sOriginalTouchMode);
    }

    @Before
    public void setUp() throws Exception {
        sInstrumentation.setInTouchMode(true);
        sInstrumentation.runOnMainSync(() ->
                mViewRootImpl = new ViewRootImpl(sContext, sContext.getDisplayNoVerify()));
    }

    @After
    public void teardown() {
        ShellIdentityUtils.invokeWithShellPermissions(() -> {
            Settings.Secure.resetToDefaults(sContext.getContentResolver(), TAG);
            Settings.System.resetToDefaults(sContext.getContentResolver(), TAG);

            var uiModeManager = sContext.getSystemService(UiModeManager.class);
            uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_NO);
        });
        if (mView != null) {
            sInstrumentation.runOnMainSync(() -> {
                WindowManager wm = sContext.getSystemService(WindowManager.class);
                wm.removeView(mView);
            });
            mView = null;
        }
        mViewRootImpl = null;
        mAfterDrawLatch = null;
        mAfterDrawThrowable = null;
    }

    @Test
    public void adjustLayoutParamsForCompatibility_layoutFullscreen() {
        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_APPLICATION);
        attrs.systemUiVisibility = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
        ViewRootImpl.adjustLayoutParamsForCompatibility(
                attrs, 0 /* appearanceControlled */, false /* behaviorControlled */);

        // Type.statusBars() must be removed.
        assertEquals(0, attrs.getFitInsetsTypes() & Type.statusBars());
    }

    @Test
    public void adjustLayoutParamsForCompatibility_layoutInScreen() {
        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_APPLICATION);
        attrs.flags = FLAG_LAYOUT_IN_SCREEN;
        ViewRootImpl.adjustLayoutParamsForCompatibility(
                attrs, 0 /* appearanceControlled */, false /* behaviorControlled */);

        // Type.statusBars() must be removed.
        assertEquals(0, attrs.getFitInsetsTypes() & Type.statusBars());
    }

    @Test
    public void adjustLayoutParamsForCompatibility_layoutHideNavigation() {
        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_APPLICATION);
        attrs.systemUiVisibility = SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
        ViewRootImpl.adjustLayoutParamsForCompatibility(
                attrs, 0 /* appearanceControlled */, false /* behaviorControlled */);

        // Type.systemBars() must be removed.
        assertEquals(0, attrs.getFitInsetsTypes() & Type.systemBars());
    }

    @Test
    public void adjustLayoutParamsForCompatibility_toast() {
        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_TOAST);
        ViewRootImpl.adjustLayoutParamsForCompatibility(
                attrs, 0 /* appearanceControlled */, false /* behaviorControlled */);

        assertTrue(attrs.isFitInsetsIgnoringVisibility());
    }

    @Test
    public void adjustLayoutParamsForCompatibility_systemAlert() {
        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_SYSTEM_ALERT);
        ViewRootImpl.adjustLayoutParamsForCompatibility(
                attrs, 0 /* appearanceControlled */, false /* behaviorControlled */);

        assertTrue(attrs.isFitInsetsIgnoringVisibility());
    }

    @Test
    public void adjustLayoutParamsForCompatibility_fitSystemBars() {
        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_APPLICATION);
        ViewRootImpl.adjustLayoutParamsForCompatibility(
                attrs, 0 /* appearanceControlled */, false /* behaviorControlled */);

        assertEquals(Type.systemBars(), attrs.getFitInsetsTypes());
    }

    @Test
    public void adjustLayoutParamsForCompatibility_fitSystemBarsAndIme() {
        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_APPLICATION);
        attrs.softInputMode |= SOFT_INPUT_ADJUST_RESIZE;
        ViewRootImpl.adjustLayoutParamsForCompatibility(
                attrs, 0 /* appearanceControlled */, false /* behaviorControlled */);

        assertEquals(Type.systemBars() | Type.ime(), attrs.getFitInsetsTypes());
    }

    @Test
    public void adjustLayoutParamsForCompatibility_noAdjustLayout() {
        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_APPLICATION);
        final int types = Type.all();
        final int sides = Side.TOP | Side.LEFT;
        final boolean fitMaxInsets = true;
        attrs.systemUiVisibility = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
        attrs.setFitInsetsTypes(types);
        attrs.setFitInsetsSides(sides);
        attrs.setFitInsetsIgnoringVisibility(fitMaxInsets);
        ViewRootImpl.adjustLayoutParamsForCompatibility(
                attrs, 0 /* appearanceControlled */, false /* behaviorControlled */);

        // Fit-insets related fields must not be adjusted due to legacy system UI visibility
        // after calling fit-insets related methods.
        assertEquals(types, attrs.getFitInsetsTypes());
        assertEquals(sides, attrs.getFitInsetsSides());
        assertEquals(fitMaxInsets, attrs.isFitInsetsIgnoringVisibility());
    }

    @Test
    public void adjustLayoutParamsForCompatibility_noAdjustAppearance() {
        final InsetsController controller = mViewRootImpl.getInsetsController();
        final WindowManager.LayoutParams attrs = mViewRootImpl.mWindowAttributes;
        final int appearance = APPEARANCE_OPAQUE_STATUS_BARS;
        controller.setSystemBarsAppearance(appearance, 0xffffffff);
        attrs.systemUiVisibility = SYSTEM_UI_FLAG_LOW_PROFILE
                | SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
                | SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
        ViewRootImpl.adjustLayoutParamsForCompatibility(attrs,
                controller.getAppearanceControlled(),
                controller.isBehaviorControlled());

        // Appearance must not be adjusted due to legacy system UI visibility after calling
        // setSystemBarsAppearance.
        assertEquals(appearance, controller.getSystemBarsAppearance());

        mViewRootImpl.setLayoutParams(new WindowManager.LayoutParams(), false);

        // Appearance must not be adjusted due to setting new LayoutParams.
        assertEquals(appearance, controller.getSystemBarsAppearance());
    }

    @Test
    public void adjustLayoutParamsForCompatibility_noAdjustBehavior() {
        final InsetsController controller = mViewRootImpl.getInsetsController();
        final WindowManager.LayoutParams attrs = mViewRootImpl.mWindowAttributes;
        final int behavior = BEHAVIOR_DEFAULT;
        controller.setSystemBarsBehavior(behavior);
        attrs.systemUiVisibility = SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
        ViewRootImpl.adjustLayoutParamsForCompatibility(attrs,
                controller.getAppearanceControlled(),
                controller.isBehaviorControlled());

        // Behavior must not be adjusted due to legacy system UI visibility after calling
        // setSystemBarsBehavior.
        assertEquals(behavior, controller.getSystemBarsBehavior());

        mViewRootImpl.setLayoutParams(new WindowManager.LayoutParams(), false);

        // Behavior must not be adjusted due to setting new LayoutParams.
        assertEquals(behavior, controller.getSystemBarsBehavior());
    }

    /**
     * Ensure scroll capture request handles a ViewRootImpl with no view tree.
     */
    @Test
    public void requestScrollCapture_withoutContentRoot() {
        final CountDownLatch latch = new CountDownLatch(1);
        mViewRootImpl.handleScrollCaptureRequest(new IScrollCaptureResponseListener.Default() {
            @Override
            public void onScrollCaptureResponse(ScrollCaptureResponse response) {
                latch.countDown();
            }
        });
        try {
            if (latch.await(100, TimeUnit.MILLISECONDS)) {
                return; // pass
            }
        } catch (InterruptedException e) { /* ignore */ }
        fail("requestScrollCapture did not respond");
    }

    /**
     * Ensure scroll capture request handles a ViewRootImpl with no view tree.
     */
    @Test
    public void requestScrollCapture_timeout() {
        View view = new View(sContext);
        view.setScrollCaptureCallback(new TestScrollCaptureCallback()); // Does nothing
        sInstrumentation.runOnMainSync(() -> {
            WindowManager.LayoutParams wmlp =
                    new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
            // Set a fake token to bypass 'is your activity running' check
            wmlp.token = new Binder();
            view.setLayoutParams(wmlp);
            mViewRootImpl.setView(view, wmlp, null);
        });

        final CountDownLatch latch = new CountDownLatch(1);
        mViewRootImpl.setScrollCaptureRequestTimeout(100);
        mViewRootImpl.handleScrollCaptureRequest(new IScrollCaptureResponseListener.Default() {
            @Override
            public void onScrollCaptureResponse(ScrollCaptureResponse response) {
                latch.countDown();
            }
        });
        try {
            if (!latch.await(2500, TimeUnit.MILLISECONDS)) {
                fail("requestScrollCapture timeout did not occur");
            }
        } catch (InterruptedException e) { /* ignore */ }
    }

    @Test
    public void whenTouchModeChanges_viewRootIsNotified() throws Exception {
        mView = new View(sContext);
        attachViewToWindow(mView);
        ViewTreeObserver viewTreeObserver = mView.getRootView().getViewTreeObserver();
        CountDownLatch latch = new CountDownLatch(1);
        ViewTreeObserver.OnTouchModeChangeListener touchModeListener = (boolean inTouchMode) -> {
            assertWithMessage("addOnTouchModeChangeListener parameter").that(
                    inTouchMode).isFalse();
            latch.countDown();
        };
        viewTreeObserver.addOnTouchModeChangeListener(touchModeListener);

        try {
            mView.requestFocusFromTouch();

            assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue();
            assertThat(mView.isInTouchMode()).isFalse();
        } finally {
            viewTreeObserver.removeOnTouchModeChangeListener(touchModeListener);
        }
    }

    @Test
    public void enteringTouchMode_whenWindowIsFocused_nothingFocused_noChange() throws Throwable {
        sInstrumentation.setInTouchMode(false);
        setUpViewAndApplyFocusStates(/* windowFocused = */ true, /* viewFocused= */ false);

        assertThat(mView.hasFocus()).isFalse();

        sInstrumentation.setInTouchMode(true);
        sInstrumentation.waitForIdleSync();
        waitForFrameRateCategoryToSettle(mView);

        assertThat(mView.hasFocus()).isFalse();
    }

    @Test
    public void enteringTouchMode_whenWindowIsFocused_withFocusedItem_noChange() throws Throwable {
        sInstrumentation.setInTouchMode(false);
        setUpViewAndApplyFocusStates(/* windowFocused= */ true, /* viewFocused= */ true);

        assertThat(mView.hasFocus()).isTrue();

        sInstrumentation.setInTouchMode(true);
        sInstrumentation.waitForIdleSync();
        waitForFrameRateCategoryToSettle(mView);

        assertThat(mView.hasFocus()).isTrue();
    }

    @Test
    public void enteringTouchMode_whenWindowIsNotFocused_nothingFocused_noChange()
            throws Throwable {
        sInstrumentation.setInTouchMode(false);
        setUpViewAndApplyFocusStates(/* windowFocused= */ false, /* viewFocused= */ false);

        assertThat(mView.hasFocus()).isFalse();

        sInstrumentation.setInTouchMode(true);
        sInstrumentation.waitForIdleSync();
        waitForFrameRateCategoryToSettle(mView);

        assertThat(mView.hasFocus()).isFalse();
    }

    @Test
    public void enteringTouchMode_whenWindowIsNotFocused_withFocusedItem_noChange()
            throws Throwable {
        sInstrumentation.setInTouchMode(false);
        setUpViewAndApplyFocusStates(/* windowFocused= */ false, /* viewFocused= */ true);

        assertThat(mView.hasFocus()).isTrue();

        sInstrumentation.setInTouchMode(true);
        sInstrumentation.waitForIdleSync();
        waitForFrameRateCategoryToSettle(mView);

        assertThat(mView.hasFocus()).isTrue();
    }

    @Test
    public void leavingTouchMode_whenWindowIsFocused_nothingFocused_assignsDefaultFocus()
            throws Throwable {
        sInstrumentation.setInTouchMode(true);
        setUpViewAndApplyFocusStates(/* windowFocused= */ true, /* viewFocused= */ false);

        assertThat(mView.hasFocus()).isFalse();

        sInstrumentation.setInTouchMode(false);
        sInstrumentation.waitForIdleSync();
        waitForFrameRateCategoryToSettle(mView);

        assertThat(mView.hasFocus()).isTrue();
    }

    @Test
    public void leavingTouchMode_whenWindowIsFocused_withFocusedItem_noChange() throws Throwable {
        sInstrumentation.setInTouchMode(true);
        setUpViewAndApplyFocusStates(/* windowFocused= */ true, /* viewFocused= */ true);

        assertThat(mView.hasFocus()).isTrue();

        sInstrumentation.setInTouchMode(false);
        sInstrumentation.waitForIdleSync();
        waitForFrameRateCategoryToSettle(mView);

        assertThat(mView.hasFocus()).isTrue();
    }

    @Test
    public void leavingTouchMode_whenWindowIsNotFocused_withFocusedItem_noChange()
            throws Throwable {
        sInstrumentation.setInTouchMode(true);
        setUpViewAndApplyFocusStates(/* windowFocused= */ false, /* viewFocused= */ true);

        assertThat(mView.hasFocus()).isTrue();

        sInstrumentation.setInTouchMode(false);
        sInstrumentation.waitForIdleSync();
        waitForFrameRateCategoryToSettle(mView);

        assertThat(mView.hasFocus()).isTrue();
    }

    @Test
    @EnableFlags(Flags.FLAG_DEFER_RESUME_FOCUS_IN_NON_FOCUSED_WINDOW)
    public void leavingTouchMode_whenWindowIsNotFocused_nothingFocused_noChange() throws Throwable {
        sInstrumentation.setInTouchMode(true);
        setUpViewAndApplyFocusStates(/* windowFocused= */ false, /* viewFocused= */ false);

        assertThat(mView.hasFocus()).isFalse();

        sInstrumentation.setInTouchMode(false);
        sInstrumentation.waitForIdleSync();
        waitForFrameRateCategoryToSettle(mView);

        assertThat(mView.hasFocus()).isFalse();
    }

    @Test
    @DisableFlags(Flags.FLAG_DEFER_RESUME_FOCUS_IN_NON_FOCUSED_WINDOW)
    public void leavingTouchMode_whenWindowIsNotFocused_nothingFocused_assignsDefaultFocus() throws
            Throwable {
        sInstrumentation.setInTouchMode(true);
        setUpViewAndApplyFocusStates(/* windowFocused= */ false, /* viewFocused= */ false);

        assertThat(mView.hasFocus()).isFalse();

        sInstrumentation.setInTouchMode(false);
        sInstrumentation.waitForIdleSync();
        waitForFrameRateCategoryToSettle(mView);

        assertThat(mView.hasFocus()).isTrue();
    }

    @Test
    public void whenDispatchFakeFocus_focusDoesNotPersist() throws Exception {
        mView = new View(sContext);
        attachViewToWindow(mView);
        mView.clearFocus();

        assertThat(mView.hasWindowFocus()).isFalse();

        mViewRootImpl = mView.getViewRootImpl();

        mViewRootImpl.dispatchCompatFakeFocus();
        assertThat(mView.hasWindowFocus()).isFalse();
    }

    @Test
    public void whenViewIsAttachedToWindow_getHostToken() {
        mView = new View(sContext);
        attachViewToWindow(mView);

        mViewRootImpl = mView.getViewRootImpl();

        assertThat(mViewRootImpl.getInputTransferToken()).isNotEqualTo(null);
    }

    /**
     * When window doesn't have focus, keys should be dropped.
     */
    @Test
    public void whenWindowDoesNotHaveFocus_keysAreDropped() {
        checkKeyEvent(() -> {
            mViewRootImpl.windowFocusChanged(false /*hasFocus*/);
        }, false /*shouldReceiveKey*/);
    }

    /**
     * When window has focus, keys should be received
     */
    @Test
    public void whenWindowHasFocus_keysAreReceived() {
        checkKeyEvent(() -> {
            mViewRootImpl.windowFocusChanged(true /*hasFocus*/);
        }, true /*shouldReceiveKey*/);
    }

    /**
     * When window is in ambient mode, keys should be dropped
     */
    @Test
    public void whenWindowIsInAmbientMode_keysAreDropped() {
        checkKeyEvent(() -> {
            mViewRootImpl.setIsAmbientMode(true /*ambient*/);
        }, false /*shouldReceiveKey*/);
    }

    /**
     * When window is paused for transition, keys should be dropped
     */
    @Test
    public void whenWindowIsPausedForTransition_keysAreDropped() {
        checkKeyEvent(() -> {
            mViewRootImpl.setPausedForTransition(true /*paused*/);
        }, false /*shouldReceiveKey*/);
    }

    @Test
    public void relayoutOnWindowSizeChange() throws Throwable {
        mView = new View(sContext);
        attachViewToWindow(mView);

        final Configuration config = mView.getResources().getConfiguration();
        final MergedConfiguration mergedConfiguration = new MergedConfiguration(config, config);
        final ClientWindowFrames frames = new ClientWindowFrames();
        frames.frame.set(config.windowConfiguration.getBounds());
        frames.frame.scale(0.5f);
        frames.seq = SequenceUtils.getInitSeq() + 10;
        final Field fieldWindow = ViewRootImpl.class.getDeclaredField("mWindow");
        fieldWindow.setAccessible(true);
        final Field fieldRelayoutSeq = ViewRootImpl.class.getDeclaredField("mRelayoutSeq");
        fieldRelayoutSeq.setAccessible(true);
        final int initRelayoutSeq = fieldRelayoutSeq.getInt(mView.getViewRootImpl());
        final IWindow iWindow = (IWindow) fieldWindow.get(mView.getViewRootImpl());
        final WindowRelayoutResult layout = new WindowRelayoutResult(frames, mergedConfiguration,
                new InsetsState(), null /* insetControls */);
        iWindow.resized(layout, true /* reportDraw */, false /* forceLayout */,
                mView.getDisplay().getDisplayId(), false /* syncWithBuffers */,
                false /* dragResizing */);
        final int[] resizedRelayoutSeq = new int[1];
        runAfterDraw(() -> {
            try {
                resizedRelayoutSeq[0] = fieldRelayoutSeq.getInt(mView.getViewRootImpl());
            } catch (IllegalAccessException e) {
                resizedRelayoutSeq[0] = -1;
            }
        });
        waitForAfterDraw();

        assertWithMessage("Window size change must invoke relayoutWindow")
                .that(resizedRelayoutSeq[0]).isGreaterThan(initRelayoutSeq);
    }

    @UiThreadTest
    @Test
    public void playSoundEffect_wrongEffectId_throwException() {
        ViewRootImpl viewRootImpl = new ViewRootImpl(sContext,
                sContext.getDisplayNoVerify());
        View view = new View(sContext);
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                TYPE_APPLICATION_OVERLAY);
        layoutParams.token = new Binder();
        view.setLayoutParams(layoutParams);
        viewRootImpl.setView(view, layoutParams, /* panelParentView= */ null);

        assertThrows(IllegalArgumentException.class,
                () -> viewRootImpl.playSoundEffect(/* effectId= */ -1));
    }

    @UiThreadTest
    @Test
    public void playSoundEffect_wrongEffectId_touchFeedbackDisabled_doNothing() {
        DisplayInfo displayInfo = new DisplayInfo();
        displayInfo.flags = Display.FLAG_TOUCH_FEEDBACK_DISABLED;
        Display display = new Display(DisplayManagerGlobal.getInstance(), /* displayId= */
                0, displayInfo, new DisplayAdjustments());
        ViewRootImpl viewRootImpl = new ViewRootImpl(sContext, display);
        View view = new View(sContext);
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                TYPE_APPLICATION_OVERLAY);
        layoutParams.token = new Binder();
        view.setLayoutParams(layoutParams);
        viewRootImpl.setView(view, layoutParams, /* panelParentView= */ null);

        viewRootImpl.playSoundEffect(/* effectId= */ -1);
    }

    @UiThreadTest
    @Test
    public void performHapticFeedback_touchFeedbackDisabled_doNothing() {
        DisplayInfo displayInfo = new DisplayInfo();
        displayInfo.flags = Display.FLAG_TOUCH_FEEDBACK_DISABLED;
        Display display = new Display(DisplayManagerGlobal.getInstance(), /* displayId= */
                0, displayInfo, new DisplayAdjustments());
        ViewRootImpl viewRootImpl = new ViewRootImpl(sContext, display);

        boolean result = viewRootImpl.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK,
                VibrationAttributes.USAGE_UNKNOWN, FLAG_IGNORE_GLOBAL_SETTING, 0 /* privFlags */);

        assertThat(result).isFalse();
    }

    @UiThreadTest
    @Test
    public void performHapticFeedbackForInputDevice_touchFeedbackDisabled_doNothing() {
        DisplayInfo displayInfo = new DisplayInfo();
        displayInfo.flags = Display.FLAG_TOUCH_FEEDBACK_DISABLED;
        Display display = new Display(DisplayManagerGlobal.getInstance(), /* displayId= */
                0, displayInfo, new DisplayAdjustments());
        ViewRootImpl viewRootImpl = new ViewRootImpl(sContext, display);

        viewRootImpl.performHapticFeedbackForInputDevice(HapticFeedbackConstants.CONTEXT_CLICK,
                1 /* inputDeviceId */,  SOURCE_ROTARY_ENCODER /* inputSource */,
                FLAG_IGNORE_GLOBAL_SETTING, 0 /* privFlags */);
    }

    /**
     * Test the default values are properly set
     */
    @UiThreadTest
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_getDefaultValues() {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        ViewRootImpl viewRootImpl = new ViewRootImpl(sContext,
                sContext.getDisplayNoVerify());
        assertEquals(FRAME_RATE_CATEGORY_DEFAULT,
                viewRootImpl.getLastPreferredFrameRateCategory());
        assertEquals(0, viewRootImpl.getLastPreferredFrameRate(), 0.1);
    }

    /**
     * Test the value of the frame rate cateogry based on the visibility of a view
     * Invsible: FRAME_RATE_CATEGORY_NO_PREFERENCE
     * Visible: FRAME_RATE_CATEGORY_NORMAL
     * Also, mIsFrameRateBoosting should be true when the visibility becomes visible
     */
    @Test
    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
            FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
    public void votePreferredFrameRate_voteFrameRateCategory_visibility_bySize() throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        attachViewToWindow(mView);
        mViewRootImpl = mView.getViewRootImpl();
        waitForFrameRateCategoryToSettle(mView);
        ViewTreeObserver.OnDrawListener failIfDrawn = () -> fail("Should not draw invisible views");
        sInstrumentation.runOnMainSync(() -> {
            mView.setVisibility(View.INVISIBLE);
            mView.invalidate();
            mView.getViewTreeObserver().addOnDrawListener(failIfDrawn);
        });
        sInstrumentation.waitForIdleSync();
        mView.getViewTreeObserver().removeOnDrawListener(failIfDrawn);

        sInstrumentation.runOnMainSync(() -> {
            mView.setVisibility(View.VISIBLE);
            mView.invalidate();
            runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_HIGH,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();
        sInstrumentation.waitForIdleSync();

        sInstrumentation.runOnMainSync(() -> {
            assertTrue(mViewRootImpl.getIsFrameRateBoosting());
        });
    }

    /**
     * Test the value of the frame rate cateogry based on the size of a view.
     * The current threshold value is 7% of the screen size
     * <7%: FRAME_RATE_CATEGORY_LOW
     */
    @Test
    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
            FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
    public void votePreferredFrameRate_voteFrameRateCategory_smallSize_bySize() throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
        wmlp.width = 1;
        wmlp.height = 1;

        sInstrumentation.runOnMainSync(() -> {
            WindowManager wm = sContext.getSystemService(WindowManager.class);
            wm.addView(mView, wmlp);
        });
        sInstrumentation.waitForIdleSync();

        mViewRootImpl = mView.getViewRootImpl();
        waitForFrameRateCategoryToSettle(mView);
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_LOW,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();
    }

    /**
     * Test the value of the frame rate cateogry based on the size of a view.
     * The current threshold value is 7% of the screen size
     * >=7% : FRAME_RATE_CATEGORY_NORMAL
     */
    @Test
    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
            FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
    public void votePreferredFrameRate_voteFrameRateCategory_normalSize_bySize() throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check

        sInstrumentation.runOnMainSync(() -> {
            WindowManager wm = sContext.getSystemService(WindowManager.class);
            Display display = wm.getDefaultDisplay();
            DisplayMetrics metrics = new DisplayMetrics();
            display.getMetrics(metrics);
            wmlp.width = (int) (metrics.widthPixels * 0.9);
            wmlp.height = (int) (metrics.heightPixels * 0.9);
            wm.addView(mView, wmlp);
        });
        sInstrumentation.waitForIdleSync();

        mViewRootImpl = mView.getViewRootImpl();
        waitForFrameRateCategoryToSettle(mView);
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_NORMAL,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();
    }

    /**
     * Test the value of the frame rate cateogry based on the visibility of a view
     * Invsible: FRAME_RATE_CATEGORY_NO_PREFERENCE
     * Visible: FRAME_RATE_CATEGORY_HIGH
     * Also, mIsFrameRateBoosting should be true when the visibility becomes visible
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_voteFrameRateCategory_visibility_defaultHigh()
            throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check

        sInstrumentation.runOnMainSync(() -> {
            DisplayMetrics metrics = sContext.getResources().getDisplayMetrics();
            wmlp.width = metrics.widthPixels / 2;
            wmlp.height = metrics.heightPixels / 2;
            WindowManager wm = sContext.getSystemService(WindowManager.class);
            wm.addView(mView, wmlp);
        });
        sInstrumentation.waitForIdleSync();
        mViewRootImpl = mView.getViewRootImpl();
        waitForFrameRateCategoryToSettle(mView);
        ViewTreeObserver.OnDrawListener failIfDrawn = () -> fail("Draw was not expected!");
        sInstrumentation.runOnMainSync(() -> {
            mView.setVisibility(View.INVISIBLE);
            mView.invalidate();
            mView.getViewTreeObserver().addOnDrawListener(failIfDrawn);
        });
        sInstrumentation.waitForIdleSync();
        sInstrumentation.runOnMainSync(
                () -> mView.getViewTreeObserver().removeOnDrawListener(failIfDrawn));

        sInstrumentation.runOnMainSync(() -> {
            mView.setVisibility(View.VISIBLE);
            mView.invalidate();
            runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_HIGH,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();
        sInstrumentation.waitForIdleSync();

        sInstrumentation.runOnMainSync(() -> {
            assertTrue(mViewRootImpl.getIsFrameRateBoosting());
        });

        waitForFrameRateCategoryToSettle(mView);

        sInstrumentation.runOnMainSync(() -> {
            mView.setVisibility(View.VISIBLE);
            mView.invalidate();
            int expected = FRAME_RATE_CATEGORY_NORMAL;
            runAfterDraw(() -> assertEquals(expected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();
    }

    /**
     * Test the value of the frame rate cateogry based on the size of a view.
     * The current threshold value is 7% of the screen size
     * <7%: FRAME_RATE_CATEGORY_NORMAL
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_voteFrameRateCategory_smallSize_defaultHigh()
            throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
        wmlp.width = 1;
        wmlp.height = 1;

        sInstrumentation.runOnMainSync(() -> {
            WindowManager wm = sContext.getSystemService(WindowManager.class);
            wm.addView(mView, wmlp);
        });
        sInstrumentation.waitForIdleSync();

        mViewRootImpl = mView.getViewRootImpl();
        waitForFrameRateCategoryToSettle(mView);
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            int expected = toolkitFrameRateBySizeReadOnly() ? FRAME_RATE_CATEGORY_LOW
                    : FRAME_RATE_CATEGORY_NORMAL;
            runAfterDraw(() -> assertEquals(expected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();
    }

    /**
     * Test the value of the frame rate cateogry based on the size of a view.
     * The current threshold value is 7% of the screen size
     * >=7% : FRAME_RATE_CATEGORY_HIGH
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_voteFrameRateCategory_normalSize_defaultHigh()
            throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check

        sInstrumentation.runOnMainSync(() -> {
            WindowManager wm = sContext.getSystemService(WindowManager.class);
            Display display = wm.getDefaultDisplay();
            DisplayMetrics metrics = new DisplayMetrics();
            display.getMetrics(metrics);
            wmlp.width = (int) (metrics.widthPixels * 0.9);
            wmlp.height = (int) (metrics.heightPixels * 0.9);
            wm.addView(mView, wmlp);
        });
        sInstrumentation.waitForIdleSync();

        mViewRootImpl = mView.getViewRootImpl();
        waitForFrameRateCategoryToSettle(mView);
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            int expected = FRAME_RATE_CATEGORY_NORMAL;
            runAfterDraw(() -> assertEquals(expected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();
    }

    /**
     * Test how values of the frame rate category are aggregated.
     * It should take the max value among all of the voted categories per frame.
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_voteFrameRateCategory_aggregate() {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        attachViewToWindow(mView);
        mViewRootImpl = mView.getViewRootImpl();
        sInstrumentation.runOnMainSync(() -> {
            assertEquals(FRAME_RATE_CATEGORY_DEFAULT,
                    mViewRootImpl.getPreferredFrameRateCategory());
        });

        // reset the frame rate category counts
        for (int i = 0; i < 5; i++) {
            sInstrumentation.runOnMainSync(() -> {
                mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
                mView.invalidate();
            });
            sInstrumentation.waitForIdleSync();
        }

        sInstrumentation.runOnMainSync(() -> {
            mViewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_LOW, 0, null);
            assertEquals(FRAME_RATE_CATEGORY_LOW, mViewRootImpl.getPreferredFrameRateCategory());
            mViewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL, 0, null);
            assertEquals(FRAME_RATE_CATEGORY_NORMAL, mViewRootImpl.getPreferredFrameRateCategory());
            mViewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT, 0, null);
            assertEquals(FRAME_RATE_CATEGORY_HIGH_HINT,
                    mViewRootImpl.getPreferredFrameRateCategory());
            mViewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH, 0, null);
            assertEquals(FRAME_RATE_CATEGORY_HIGH, mViewRootImpl.getPreferredFrameRateCategory());
            mViewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT, 0, null);
            assertEquals(FRAME_RATE_CATEGORY_HIGH, mViewRootImpl.getPreferredFrameRateCategory());
            mViewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL, 0, null);
            assertEquals(FRAME_RATE_CATEGORY_HIGH, mViewRootImpl.getPreferredFrameRateCategory());
            mViewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_LOW, 0, null);
            assertEquals(FRAME_RATE_CATEGORY_HIGH, mViewRootImpl.getPreferredFrameRateCategory());
        });
    }

    /**
     * Test the accurate aggregation of frame rate values as follows:
     * 1. When values exceed 60Hz, select the maximum value.
     * 2. If frame rates are less than 60Hz and multiple frame rates are voted,
     * prioritize 60Hz..
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_voteFrameRate_aggregate() {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        attachViewToWindow(mView);
        mViewRootImpl = mView.getViewRootImpl();
        sInstrumentation.runOnMainSync(() -> {
            assertEquals(0, mViewRootImpl.getPreferredFrameRate(), 0.1);
            assertEquals(mViewRootImpl.getFrameRateCompatibility(),
                    FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
            assertFalse(mViewRootImpl.isFrameRateConflicted());
            mViewRootImpl.votePreferredFrameRate(24, FRAME_RATE_COMPATIBILITY_AT_LEAST);
            assertEquals(24, mViewRootImpl.getPreferredFrameRate(), 0.1);
            assertEquals(FRAME_RATE_COMPATIBILITY_AT_LEAST,
                    mViewRootImpl.getFrameRateCompatibility());
            assertFalse(mViewRootImpl.isFrameRateConflicted());

            mViewRootImpl.votePreferredFrameRate(30, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
            assertEquals(30, mViewRootImpl.getPreferredFrameRate(), 0.1);
            // If there is a conflict, then set compatibility to
            // FRAME_RATE_COMPATIBILITY_FIXED_SOURCE
            assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                    mViewRootImpl.getFrameRateCompatibility());

            // Should be true since there is a conflict between 24 and 30.
            assertTrue(mViewRootImpl.isFrameRateConflicted());

            mView.invalidate();
        });
        sInstrumentation.waitForIdleSync();

        sInstrumentation.runOnMainSync(() -> {
            assertFalse(mViewRootImpl.isFrameRateConflicted());
            mViewRootImpl.votePreferredFrameRate(60, FRAME_RATE_COMPATIBILITY_AT_LEAST);
            assertEquals(60, mViewRootImpl.getPreferredFrameRate(), 0.1);
            assertEquals(FRAME_RATE_COMPATIBILITY_AT_LEAST,
                    mViewRootImpl.getFrameRateCompatibility());

            assertFalse(mViewRootImpl.isFrameRateConflicted());
            mViewRootImpl.votePreferredFrameRate(120, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
            assertEquals(120, mViewRootImpl.getPreferredFrameRate(), 0.1);
            assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                    mViewRootImpl.getFrameRateCompatibility());
            // Should be false since 60 is a divisor of 120.
            assertFalse(mViewRootImpl.isFrameRateConflicted());
            mViewRootImpl.votePreferredFrameRate(60, FRAME_RATE_COMPATIBILITY_AT_LEAST);
            assertEquals(120, mViewRootImpl.getPreferredFrameRate(), 0.1);
            // compatibility should be remained the same (FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
            // since the frame rate 60 is smaller than 120.
            assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                    mViewRootImpl.getFrameRateCompatibility());
            // Should be false since 60 is a divisor of 120.
            assertFalse(mViewRootImpl.isFrameRateConflicted());
        });
    }

    /**
     * Override the frame rate category value with setRequestedFrameRate method.
     * This function can replace the existing frameRateCategory value and
     * submit your preferred choice to the ViewRootImpl.
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_voteFrameRate_category() throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        attachViewToWindow(mView);
        sInstrumentation.waitForIdleSync();

        mViewRootImpl = mView.getViewRootImpl();
        sInstrumentation.runOnMainSync(() -> {
            assertEquals(FRAME_RATE_CATEGORY_DEFAULT,
                    mViewRootImpl.getPreferredFrameRateCategory());
        });

        // reset the frame rate category counts
        for (int i = 0; i < 5; i++) {
            sInstrumentation.runOnMainSync(() -> {
                mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
                mView.invalidate();
            });
            sInstrumentation.waitForIdleSync();
        }

        waitForFrameRateCategoryToSettle(mView);

        sInstrumentation.runOnMainSync(() -> {
            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_LOW);
            mView.invalidate();
            runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_LOW,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();
        sInstrumentation.runOnMainSync(() -> {
            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NORMAL);
            mView.invalidate();
            runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_NORMAL,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();
        sInstrumentation.runOnMainSync(() -> {
            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_HIGH);
            mView.invalidate();
            runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_HIGH,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();
    }

    /**
     * When velocity of a View is not equal to 0, we call setFrameRateCategory with HIGH.
     * Also, we shouldn't call setFrameRate.
     */
    @Test
    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
            FLAG_VIEW_VELOCITY_API})
    public void votePreferredFrameRate_voteFrameRateCategory_velocityToHigh() throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
        wmlp.width = 1;
        wmlp.height = 1;

        sInstrumentation.runOnMainSync(() -> {
            WindowManager wm = sContext.getSystemService(WindowManager.class);
            wm.addView(mView, wmlp);
        });
        sInstrumentation.waitForIdleSync();

        mViewRootImpl = mView.getViewRootImpl();
        waitForFrameRateCategoryToSettle(mView);

        sInstrumentation.runOnMainSync(() -> {
            assertEquals(0, mViewRootImpl.getPreferredFrameRate(), 0.1);
            mView.setFrameContentVelocity(100);
            mView.invalidate();
            runAfterDraw(() -> {
                int expected = toolkitFrameRateBySizeReadOnly()
                        ? FRAME_RATE_CATEGORY_LOW : FRAME_RATE_CATEGORY_NORMAL;
                assertEquals(expected, mViewRootImpl.getLastPreferredFrameRateCategory());
                assertTrue(mViewRootImpl.getLastPreferredFrameRate() >= 60f);
            });
        });
        waitForAfterDraw();
        sInstrumentation.waitForIdleSync();
    }

    /**
     * We should boost the frame rate if the value of mInsetsAnimationRunning is true.
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_insetsAnimation() {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check

        sInstrumentation.runOnMainSync(() -> {
            WindowManager wm = sContext.getSystemService(WindowManager.class);
            Display display = wm.getDefaultDisplay();
            DisplayMetrics metrics = new DisplayMetrics();
            display.getMetrics(metrics);
            wmlp.width = (int) (metrics.widthPixels * 0.9);
            wmlp.height = (int) (metrics.heightPixels * 0.9);
            wm.addView(mView, wmlp);
        });
        sInstrumentation.waitForIdleSync();

        ViewRootImpl viewRootImpl = mView.getViewRootImpl();
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            viewRootImpl.updateAnimatingTypes(Type.systemBars(), null /* statsToken */);
            mView.invalidate();
        });
        sInstrumentation.waitForIdleSync();

        sInstrumentation.runOnMainSync(() -> {
            assertEquals(FRAME_RATE_CATEGORY_HIGH,
                    viewRootImpl.getLastPreferredFrameRateCategory());
        });
    }


    /**
     * Test FrameRateBoostOnTouchEnabled API
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_frameRateBoostOnTouch() {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        attachViewToWindow(mView);
        sInstrumentation.waitForIdleSync();

        ViewRootImpl viewRootImpl = mView.getViewRootImpl();
        final WindowManager.LayoutParams attrs = viewRootImpl.mWindowAttributes;
        assertTrue(attrs.getFrameRateBoostOnTouchEnabled());
        assertEquals(attrs.getFrameRateBoostOnTouchEnabled(),
                viewRootImpl.getFrameRateBoostOnTouchEnabled());

        sInstrumentation.runOnMainSync(() -> {
            attrs.setFrameRateBoostOnTouchEnabled(false);
            viewRootImpl.setLayoutParams(attrs, false);
        });
        sInstrumentation.waitForIdleSync();

        sInstrumentation.runOnMainSync(() -> {
            final WindowManager.LayoutParams newAttrs = viewRootImpl.mWindowAttributes;
            assertFalse(newAttrs.getFrameRateBoostOnTouchEnabled());
            assertEquals(newAttrs.getFrameRateBoostOnTouchEnabled(),
                    viewRootImpl.getFrameRateBoostOnTouchEnabled());
        });
    }

    /**
     * Test votePreferredFrameRate_voteFrameRateTimeOut
     * If no frame rate is voted in 100 milliseconds, the value of
     * mPreferredFrameRate should be set to 0.
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_voteFrameRateTimeOut() throws InterruptedException {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        final long delay = 200L;

        mView = new View(sContext);
        attachViewToWindow(mView);
        sInstrumentation.waitForIdleSync();
        ViewRootImpl viewRootImpl = mView.getViewRootImpl();

        sInstrumentation.runOnMainSync(() -> {
            assertEquals(0, viewRootImpl.getPreferredFrameRate(), 0.1);
            assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                    viewRootImpl.getFrameRateCompatibility());
            assertFalse(viewRootImpl.isFrameRateConflicted());
            viewRootImpl.votePreferredFrameRate(24, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
            assertEquals(24, viewRootImpl.getPreferredFrameRate(), 0.1);
            assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                    viewRootImpl.getFrameRateCompatibility());
            assertFalse(viewRootImpl.isFrameRateConflicted());
            mView.invalidate();
            assertEquals(24, viewRootImpl.getPreferredFrameRate(), 0.1);
            assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                    viewRootImpl.getFrameRateCompatibility());
            assertFalse(viewRootImpl.isFrameRateConflicted());
        });

        Thread.sleep(delay);
        assertEquals(0, viewRootImpl.getPreferredFrameRate(), 0.1);
        assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                viewRootImpl.getFrameRateCompatibility());
        assertFalse(viewRootImpl.isFrameRateConflicted());
    }

    /**
     * A View should either vote a frame rate or a frame rate category instead of both.
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_voteFrameRateOnly() throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        float frameRate = 20;
        attachViewToWindow(mView);
        sInstrumentation.waitForIdleSync();

        mViewRootImpl = mView.getViewRootImpl();
        waitForFrameRateCategoryToSettle(mView);
        sInstrumentation.runOnMainSync(() -> {
            assertEquals(FRAME_RATE_CATEGORY_DEFAULT,
                    mViewRootImpl.getPreferredFrameRateCategory());

            mView.setRequestedFrameRate(frameRate);
            mView.invalidate();
            runAfterDraw(() -> {
                int expected = FRAME_RATE_CATEGORY_NORMAL;
                assertEquals(expected, mViewRootImpl.getLastPreferredFrameRateCategory());
                assertEquals(frameRate, mViewRootImpl.getLastPreferredFrameRate(), 0.1);
            });
        });
        waitForAfterDraw();

        // reset the frame rate category counts
        for (int i = 0; i < 5; i++) {
            sInstrumentation.runOnMainSync(() -> {
                mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
                mView.invalidate();
            });
            sInstrumentation.waitForIdleSync();
        }

        sInstrumentation.runOnMainSync(() -> {
            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_LOW);
            mView.invalidate();
            runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_LOW,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();
    }

    /**
     * Test the logic of infrequent layer:
     * - NORMAL for infrequent update: FT2-FT1 > 100 && FT3-FT2 > 100.
     * - HIGH/NORMAL based on size for frequent update: (FT3-FT2) + (FT2 - FT1) < 100.
     * - otherwise, use the previous category value.
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_infrequentLayer_defaultHigh() throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        final long delay = 200L;

        mView = new View(sContext);
        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
        int expected = FRAME_RATE_CATEGORY_NORMAL;

        sInstrumentation.runOnMainSync(() -> {
            WindowManager wm = sContext.getSystemService(WindowManager.class);
            Display display = wm.getDefaultDisplay();
            DisplayMetrics metrics = new DisplayMetrics();
            display.getMetrics(metrics);
            wmlp.width = (int) (metrics.widthPixels * 0.9);
            wmlp.height = (int) (metrics.heightPixels * 0.9);
            wm.addView(mView, wmlp);
        });
        sInstrumentation.waitForIdleSync();

        mViewRootImpl = mView.getViewRootImpl();
        waitForFrameRateCategoryToSettle(mView);

        // In transition from frequent update to infrequent update
        Thread.sleep(delay);
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            runAfterDraw(() -> assertEquals(expected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();

        // In transition from frequent update to infrequent update
        Thread.sleep(delay);
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            runAfterDraw(() -> assertEquals(expected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });

        // Infrequent update
        Thread.sleep(delay);

        // The expected category is normal for intermittent.
        int intermittentExpected = FRAME_RATE_CATEGORY_NORMAL;

        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            runAfterDraw(() -> assertEquals(intermittentExpected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();

        // When the View vote, it's still considered as intermittent update state
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            runAfterDraw(() -> assertEquals(intermittentExpected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();

        // Becomes frequent update state
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            runAfterDraw(() -> assertEquals(expected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
    }

    @Test
    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
            FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
    public void votePreferredFrameRate_infrequentLayer_smallView_voteForLow() throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        final long delay = 200L;

        mView = new View(sContext);
        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
        wmlp.width = 1;
        wmlp.height = 1;

        // The view is a small view, and it should vote for category low only.
        int expected = FRAME_RATE_CATEGORY_LOW;

        sInstrumentation.runOnMainSync(() -> {
            WindowManager wm = sContext.getSystemService(WindowManager.class);
            wm.addView(mView, wmlp);
        });
        sInstrumentation.waitForIdleSync();

        mViewRootImpl = mView.getViewRootImpl();
        waitForFrameRateCategoryToSettle(mView);

        // In transition from frequent update to infrequent update
        Thread.sleep(delay);
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            runAfterDraw(() -> assertEquals(expected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();

        // In transition from frequent update to infrequent update
        Thread.sleep(delay);
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            runAfterDraw(() -> assertEquals(expected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });

        // Infrequent update
        Thread.sleep(delay);

        // The view is small, the expected category is still low for intermittent.
        int intermittentExpected = FRAME_RATE_CATEGORY_LOW;

        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            runAfterDraw(() -> assertEquals(intermittentExpected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();

        // When the View vote, it's still considered as intermittent update state
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            runAfterDraw(() -> assertEquals(intermittentExpected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();

        // Becomes frequent update state
        sInstrumentation.runOnMainSync(() -> {
            mView.invalidate();
            runAfterDraw(() -> assertEquals(expected,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
    }

    /**
     * Test the IsFrameRatePowerSavingsBalanced values are properly set
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_isFrameRatePowerSavingsBalanced() {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        attachViewToWindow(mView);
        sInstrumentation.waitForIdleSync();

        ViewRootImpl viewRoot = mView.getViewRootImpl();
        final WindowManager.LayoutParams attrs = viewRoot.mWindowAttributes;
        assertTrue(attrs.isFrameRatePowerSavingsBalanced());
        assertEquals(attrs.isFrameRatePowerSavingsBalanced(),
                viewRoot.isFrameRatePowerSavingsBalanced());

        sInstrumentation.runOnMainSync(() -> {
            attrs.setFrameRatePowerSavingsBalanced(false);
            viewRoot.setLayoutParams(attrs, false);
        });
        sInstrumentation.waitForIdleSync();

        sInstrumentation.runOnMainSync(() -> {
            final WindowManager.LayoutParams newAttrs = viewRoot.mWindowAttributes;
            assertFalse(newAttrs.isFrameRatePowerSavingsBalanced());
            assertEquals(newAttrs.isFrameRatePowerSavingsBalanced(),
                    viewRoot.isFrameRatePowerSavingsBalanced());
        });
    }

    /**
     * Test the TextureView heuristic:
     * 1. Store the last 3 invalidates time - FT1, FT2, FT3.
     * 2. If FT2-FT1 > 15ms && FT3-FT2 > 15ms -> vote for NORMAL category
     */
    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_applyTextureViewHeuristic() throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        final long delay = 30L;

        mView = new TextureView(sContext);
        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check

        sInstrumentation.runOnMainSync(() -> {
            WindowManager wm = sContext.getSystemService(WindowManager.class);
            Display display = wm.getDefaultDisplay();
            DisplayMetrics metrics = new DisplayMetrics();
            display.getMetrics(metrics);
            wmlp.width = (int) (metrics.widthPixels * 0.9);
            wmlp.height = (int) (metrics.heightPixels * 0.9);
            wm.addView(mView, wmlp);
        });
        sInstrumentation.waitForIdleSync();

        mViewRootImpl = mView.getViewRootImpl();

        waitForFrameRateCategoryToSettle(mView);

        // reset the frame rate category counts
        for (int i = 0; i < 5; i++) {
            Thread.sleep(delay);
            sInstrumentation.runOnMainSync(() -> {
                mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
                mView.invalidate();
            });
            sInstrumentation.waitForIdleSync();
        }

        Thread.sleep(delay);
        sInstrumentation.runOnMainSync(() -> {
            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_DEFAULT);
            mView.invalidate();
            runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_NORMAL,
                    mViewRootImpl.getLastPreferredFrameRateCategory()));
        });
        waitForAfterDraw();
    }

    @Test
    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
    public void votePreferredFrameRate_resetWhenDestroyingSurface()
            throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check

        sInstrumentation.runOnMainSync(() -> {
            WindowManager wm = sContext.getSystemService(WindowManager.class);
            wm.addView(mView, wmlp);
        });
        sInstrumentation.waitForIdleSync();

        mViewRootImpl = mView.getViewRootImpl();

        waitForFrameRateCategoryToSettle(mView);

        sInstrumentation.runOnMainSync(() -> {
            mViewRootImpl.getView().setVisibility(View.INVISIBLE);
            mViewRootImpl.mSurface.release();
            mView.invalidate();
        });
        sInstrumentation.waitForIdleSync();

        assertEquals(false, mViewRootImpl.mSurface.isValid());
        assertEquals(FRAME_RATE_CATEGORY_DEFAULT,
                mViewRootImpl.getLastPreferredFrameRateCategory());
        assertEquals(FRAME_RATE_CATEGORY_DEFAULT,
                mViewRootImpl.getPreferredFrameRateCategory());
        assertEquals(0, mViewRootImpl.getLastPreferredFrameRate(), 0.1);
        assertEquals(0, mViewRootImpl.getPreferredFrameRate(), 0.1);
    }

    @Test
    public void votePreferredFrameRate_reset() throws Throwable {
        if (!ViewProperties.vrr_enabled().orElse(true)) {
            return;
        }
        mView = new View(sContext);
        double delta = 0.1;
        float pixelsPerSecond = 1000_000;
        float expectedFrameRate = 120;
        attachViewToWindow(mView);
        sInstrumentation.waitForIdleSync();
        ViewRootImpl viewRoot = mView.getViewRootImpl();
        waitForFrameRateCategoryToSettle(mView);

        sInstrumentation.runOnMainSync(() -> {
            mView.setFrameContentVelocity(pixelsPerSecond);
            mView.invalidate();
            assertEquals(0, viewRoot.getPreferredFrameRate(), delta);
            assertEquals(0, viewRoot.getLastPreferredFrameRate(), delta);
            runAfterDraw(() -> {
                assertEquals(expectedFrameRate, viewRoot.getPreferredFrameRate(), delta);
                assertEquals(expectedFrameRate, viewRoot.getLastPreferredFrameRate(), delta);
            });
        });
        waitForAfterDraw();
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_systemLightMode_returnsNone() throws Exception {
        waitForSystemNightModeActivated(false);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));

    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_systemNightModeAndDisableForceInvertColor_returnsNone()
            throws Exception {
        waitForSystemNightModeActivated(true);

        enableForceInvertColor(false);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_isLightTheme_returnsForceInvertColorDark() throws Exception {
        // Set up configurations for force invert color
        waitForSystemNightModeActivated(true);
        enableForceInvertColor(true);

        setUpViewAttributes(/* isLightTheme= */ true, /* isForceDarkAllowed= */ false);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType()
                        == ForceDarkType.FORCE_INVERT_COLOR_DARK));
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_isOverrideDefault_lightTheme_forceInverted()
            throws Exception {
        TestableContext testableContext = new TestableContext(sContext);
        final UiModeManager mockUiModeManager = mock(UiModeManager.class);
        when(mockUiModeManager.getForceInvertOverrideState()).thenReturn(
                UiModeManager.FORCE_INVERT_PACKAGE_ALLOWED);
        testableContext.addMockSystemService(Context.UI_MODE_SERVICE, mockUiModeManager);
        sInstrumentation.runOnMainSync(() -> mViewRootImpl =
                new ViewRootImpl(testableContext, testableContext.getDisplayNoVerify()));

        when(mockUiModeManager.getForceInvertState()).thenReturn(FORCE_INVERT_TYPE_DARK);
        when(mockUiModeManager.getNightMode()).thenReturn(MODE_NIGHT_YES);

        waitForSystemNightModeActivated(testableContext, true);

        setUpViewAttributes(testableContext, /* isLightTheme= */ true, /* isForceDarkAllowed= */
                false);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType()
                        == ForceDarkType.FORCE_INVERT_COLOR_DARK));
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_isOverrideEnabled_lightTheme_forceInverted()
            throws Exception {
        TestableContext testableContext = new TestableContext(sContext);
        final UiModeManager mockUiModeManager = mock(UiModeManager.class);
        when(mockUiModeManager.getForceInvertOverrideState()).thenReturn(
                UiModeManager.FORCE_INVERT_PACKAGE_ALWAYS_ENABLE);
        testableContext.addMockSystemService(Context.UI_MODE_SERVICE, mockUiModeManager);
        sInstrumentation.runOnMainSync(() -> mViewRootImpl =
                new ViewRootImpl(testableContext, testableContext.getDisplayNoVerify()));

        when(mockUiModeManager.getForceInvertState()).thenReturn(FORCE_INVERT_TYPE_DARK);
        when(mockUiModeManager.getNightMode()).thenReturn(MODE_NIGHT_YES);

        waitForSystemNightModeActivated(testableContext, true);

        setUpViewAttributes(testableContext, /* isLightTheme= */ true, /* isForceDarkAllowed= */
                false);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType()
                        == ForceDarkType.FORCE_INVERT_COLOR_DARK));
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_isOverrideEnabled_darkTheme_forceInverted()
            throws Exception {
        TestableContext testableContext = new TestableContext(sContext);
        final UiModeManager mockUiModeManager = mock(UiModeManager.class);
        when(mockUiModeManager.getForceInvertOverrideState()).thenReturn(
                UiModeManager.FORCE_INVERT_PACKAGE_ALWAYS_ENABLE);
        testableContext.addMockSystemService(Context.UI_MODE_SERVICE, mockUiModeManager);
        sInstrumentation.runOnMainSync(() -> mViewRootImpl =
                new ViewRootImpl(testableContext, testableContext.getDisplayNoVerify()));

        when(mockUiModeManager.getForceInvertState()).thenReturn(FORCE_INVERT_TYPE_DARK);
        when(mockUiModeManager.getNightMode()).thenReturn(MODE_NIGHT_YES);

        waitForSystemNightModeActivated(testableContext, true);

        setUpViewAttributes(testableContext, /* isLightTheme= */ false, /* isForceDarkAllowed= */
                false);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType()
                        == ForceDarkType.FORCE_INVERT_COLOR_DARK));
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_isOverrideDisabled_lightTheme_returnsNone()
            throws Exception {
        TestableContext testableContext = new TestableContext(sContext);
        final UiModeManager mockUiModeManager = mock(UiModeManager.class);
        when(mockUiModeManager.getForceInvertOverrideState()).thenReturn(
                UiModeManager.FORCE_INVERT_PACKAGE_ALWAYS_DISABLE);
        testableContext.addMockSystemService(Context.UI_MODE_SERVICE, mockUiModeManager);
        sInstrumentation.runOnMainSync(() -> mViewRootImpl =
                new ViewRootImpl(testableContext, testableContext.getDisplayNoVerify()));

        when(mockUiModeManager.getForceInvertState()).thenReturn(FORCE_INVERT_TYPE_DARK);
        when(mockUiModeManager.getNightMode()).thenReturn(MODE_NIGHT_YES);

        waitForSystemNightModeActivated(testableContext, true);

        setUpViewAttributes(testableContext, /* isLightTheme= */ true, /* isForceDarkAllowed= */
                false);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_isOverrideDisabled_darkTheme_returnsNone()
            throws Exception {
        TestableContext testableContext = new TestableContext(sContext);
        final UiModeManager mockUiModeManager = mock(UiModeManager.class);
        when(mockUiModeManager.getForceInvertOverrideState()).thenReturn(
                UiModeManager.FORCE_INVERT_PACKAGE_ALWAYS_DISABLE);
        testableContext.addMockSystemService(Context.UI_MODE_SERVICE, mockUiModeManager);
        sInstrumentation.runOnMainSync(() -> mViewRootImpl =
                new ViewRootImpl(testableContext, testableContext.getDisplayNoVerify()));

        when(mockUiModeManager.getForceInvertState()).thenReturn(FORCE_INVERT_TYPE_DARK);
        when(mockUiModeManager.getNightMode()).thenReturn(MODE_NIGHT_YES);

        waitForSystemNightModeActivated(testableContext, true);

        setUpViewAttributes(testableContext, /* isLightTheme= */ false, /* isForceDarkAllowed= */
                false);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_notLightTheme_returnsNone() throws Exception {
        // Set up configurations for force invert color
        waitForSystemNightModeActivated(true);
        enableForceInvertColor(true);

        setUpViewAttributes(/* isLightTheme= */ false, /* isForceDarkAllowed= */ false);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void forceInvertOffForceDarkOff_forceDarkModeDisabled() {
        // Set up configurations for force invert color
        waitForSystemNightModeActivated(true);
        enableForceInvertColor(false);

        // Set up view attributes
        setUpViewAttributes(
                /* isLightTheme= */ false,
                /* isForceDarkAllowed= */ false);

        assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.NONE);
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void forceInvertOffForceDarkOn_forceDarkModeEnabled() {
        // Set up configurations for force invert color
        waitForSystemNightModeActivated(true);
        enableForceInvertColor(false);

        // Set up view attributes
        setUpViewAttributes(
                /* isLightTheme= */ true,
                /* isForceDarkAllowed= */ true);

        assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.FORCE_DARK);
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void forceInvertOnForceDarkOn_forceDarkModeEnabled() {
        // Set up configurations for force invert color
        waitForSystemNightModeActivated(true);
        enableForceInvertColor(true);

        // Setup view attributes
        setUpViewAttributes(
                /* isLightTheme= */ true,
                /* isForceDarkAllowed= */ true);

        assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.FORCE_DARK);
    }

    @Test
    public void testASurfaceControl_createFromWindow() throws Throwable {
        mView = new View(sContext);
        attachViewToWindow(mView);
        sInstrumentation.runOnMainSync(() -> {
            mView.setVisibility(View.VISIBLE);
            mView.invalidate();
            runAfterDraw(()->{});
        });
        waitForAfterDraw();
        mViewRootImpl = mView.getViewRootImpl();
        Log.d(TAG, "mViewRootImpl.mSurface=" + mViewRootImpl.mSurface);
        assertTrue("Could not create ASurfaceControl from VRI surface",
                nativeCreateASurfaceControlFromSurface(mViewRootImpl.mSurface));
    }

    @Test
    public void testHandleInsetsControlChanged() {
        mView = new View(sContext);
        attachViewToWindow(mView);

        mViewRootImpl = mView.getViewRootImpl();
        final InsetsController controller = mViewRootImpl.getInsetsController();

        final InsetsState state0 = new InsetsState();
        final InsetsState state1 = new InsetsState();
        state0.setDisplayFrame(new Rect(0, 0, 500, 1000));
        state0.setSeq(getInitSeq() + 10000);
        state1.setDisplayFrame(new Rect(0, 0, 1500, 2000));
        state1.setSeq(getInitSeq() + 10001);
        final InsetsSourceControl.Array array = new InsetsSourceControl.Array();

        sInstrumentation.runOnMainSync(() -> {
            mViewRootImpl.handleInsetsControlChanged(state0, array);
            assertEquals(state0, controller.getLastDispatchedState());

            mViewRootImpl.handleInsetsControlChanged(state1, array);
            assertEquals(state1, controller.getLastDispatchedState());

            // Skip the stale value.
            mViewRootImpl.handleInsetsControlChanged(state0, array);
            assertEquals(state1, controller.getLastDispatchedState());
            assertNotEquals(state0, controller.getLastDispatchedState());
        });
    }

    @Test
    public void testAdjustAccessibilityFocusedBounds_largeEnoughBoundsAreUnchanged() {
        final int strokeWidth = sContext.getSystemService(AccessibilityManager.class)
                .getAccessibilityFocusStrokeWidth();
        final int left, top, width, height;
        left = top = 100;
        width = height = strokeWidth * 2;
        final Rect bounds = new Rect(left, top, left + width, top + height);
        final Rect originalBounds = new Rect(bounds);

        mViewRootImpl.adjustAccessibilityFocusedRectBoundsIfNeeded(bounds);

        assertThat(bounds).isEqualTo(originalBounds);
    }

    @Test
    public void testAdjustAccessibilityFocusedBounds_smallBoundsAreExpanded() {
        final int strokeWidth = sContext.getSystemService(AccessibilityManager.class)
                .getAccessibilityFocusStrokeWidth();
        final int left, top, width, height;
        left = top = 100;
        width = height = strokeWidth;
        final Rect bounds = new Rect(left, top, left + width, top + height);
        final Rect originalBounds = new Rect(bounds);

        mViewRootImpl.adjustAccessibilityFocusedRectBoundsIfNeeded(bounds);

        // Bounds should be centered on the same point, but expanded to at least strokeWidth * 2
        assertThat(bounds.centerX()).isEqualTo(originalBounds.centerX());
        assertThat(bounds.centerY()).isEqualTo(originalBounds.centerY());
        assertThat(bounds.width()).isAtLeast(strokeWidth * 2);
        assertThat(bounds.height()).isAtLeast(strokeWidth * 2);
    }

    @Test
    public void testOffThreadRendererViewsAccess() throws Throwable {
        mView = new View(sContext);
        attachViewToWindow(mView);
        AtomicInteger threadRunning = new AtomicInteger(1);
        View[] views = new View[10];
        for (int i = 0; i < 10; i++) {
            views[i] = new View(sContext);
        }
        Thread offThread = new Thread(() -> {
            while (threadRunning.get() > 0) {
                for (int i = 0; i < 10; i++) {
                    mViewRootImpl.addThreadedRendererView(views[i]);
                }
                for (int i = 0; i < 10; i++) {
                    mViewRootImpl.removeThreadedRendererView(views[i]);
                }
            }
        });
        offThread.start();
        try {
            for (int i = 0; i < 1000; i++) {
                sInstrumentation.runOnMainSync(() -> {
                    mView.invalidate();
                    runAfterDraw(() -> {
                    });
                });
                waitForAfterDraw();
            }
        } finally {
            threadRunning.set(0);
        }
    }

    private void setUpViewAndApplyFocusStates(boolean windowFocused, boolean viewFocused)
            throws Throwable {
        mView = new View(sContext);
        mView.setFocusableInTouchMode(true);
        mView.setFocusable(true);
        attachViewToWindow(mView);
        sInstrumentation.runOnMainSync(() -> {
            mView.setVisibility(View.VISIBLE);
            mView.layout(0, 0, 100, 100);
            mView.invalidate();
            runAfterDraw(() -> {
            });
        });
        waitForAfterDraw();
        mViewRootImpl = mView.getViewRootImpl();
        waitForFrameRateCategoryToSettle(mView);

        sInstrumentation.runOnMainSync(() -> {
            mViewRootImpl.windowFocusChanged(windowFocused);
        });
        sInstrumentation.waitForIdleSync();
        waitForFrameRateCategoryToSettle(mView);
        assertThat(mView.hasWindowFocus()).isEqualTo(windowFocused);

        sInstrumentation.runOnMainSync(() -> {
            if (viewFocused) {
                mView.requestFocus();
            } else {
                mView.clearFocusInternal(null, true, false);
            }
        });
        waitForAfterDraw();
        waitForFrameRateCategoryToSettle(mView);
    }

    static class InputView extends View {
        private final BlockingQueue<InputEvent> mEvents = new LinkedBlockingQueue<>();
        private final BlockingQueueEventVerifier mVerifier =
                new BlockingQueueEventVerifier(mEvents);

        InputView(Context context) {
            super(context);
        }

        @Override
        public boolean dispatchKeyEventPreIme(KeyEvent event) {
            mEvents.add(event.copy());
            return true /*handled*/;
        }

        public void assertReceivedKey(Matcher<KeyEvent> matcher) {
            mVerifier.assertReceivedKey(matcher);
        }

        public void assertNoEvents() {
            mVerifier.assertNoEvents();
        }
    }

    /**
     * Create a new view, and add it to window manager.
     * Run the precondition 'setup'.
     * Next, inject an event into this view, and check whether it is received.
     */
    private void checkKeyEvent(Runnable setup, boolean shouldReceiveKey) {
        final InputView view = new InputView(sContext);
        mView = view;

        attachViewToWindow(view);

        mViewRootImpl = view.getViewRootImpl();
        sInstrumentation.runOnMainSync(setup);
        sInstrumentation.waitForIdleSync();

        // Inject a key event, and wait for it to be processed
        sInstrumentation.runOnMainSync(() -> {
            KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A);
            mViewRootImpl.dispatchInputEvent(event);
        });
        sInstrumentation.waitForIdleSync();
        if (shouldReceiveKey) {
            view.assertReceivedKey(withKeyCode(KeyEvent.KEYCODE_A));
        } else {
            view.assertNoEvents();
        }
    }

    private void attachViewToWindow(View view) {
        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check

        sInstrumentation.runOnMainSync(() -> {
            WindowManager wm = sContext.getSystemService(WindowManager.class);
            wm.addView(view, wmlp);
        });
        sInstrumentation.waitForIdleSync();
    }

    private void runAfterDraw(@NonNull Runnable runnable) {
        mAfterDrawLatch = new CountDownLatch(1);
        ViewTreeObserver.OnDrawListener listener = new ViewTreeObserver.OnDrawListener() {
            @Override
            public void onDraw() {
                mView.getHandler().postAtFrontOfQueue(() -> {
                    mView.getViewTreeObserver().removeOnDrawListener(this);
                    try {
                        runnable.run();
                    } catch (Throwable t) {
                        mAfterDrawThrowable = t;
                    }
                    mAfterDrawLatch.countDown();
                });
            }
        };
        mView.getViewTreeObserver().addOnDrawListener(listener);
    }

    private void waitForAfterDraw() throws Throwable {
        assertTrue(mAfterDrawLatch.await(1, TimeUnit.SECONDS));
        if (mAfterDrawThrowable != null) {
            throw mAfterDrawThrowable;
        }
    }

    private void waitForFrameRateCategoryToSettle(View view) throws Throwable {
        for (int i = 0; i < 5 || mViewRootImpl.getIsFrameRateBoosting(); i++) {
            final CountDownLatch drawLatch = new CountDownLatch(1);

            // Now that it is small, any invalidation should have a normal category
            ViewTreeObserver.OnDrawListener listener = drawLatch::countDown;

            sInstrumentation.runOnMainSync(() -> {
                view.invalidate();
                view.getViewTreeObserver().addOnDrawListener(listener);
            });

            assertTrue(drawLatch.await(1, TimeUnit.SECONDS));
            sInstrumentation.runOnMainSync(
                    () -> view.getViewTreeObserver().removeOnDrawListener(listener));
        }
    }

    private void waitForSystemNightModeActivated(boolean active) {
        waitForSystemNightModeActivated(sContext, active);
    }

    private void waitForSystemNightModeActivated(Context context, boolean active) {
        ShellIdentityUtils.invokeWithShellPermissions(() ->
                sInstrumentation.runOnMainSync(() -> {
                    var uiModeManager = context.getSystemService(UiModeManager.class);
                    uiModeManager.setNightModeActivated(active);
                }));
        sInstrumentation.waitForIdleSync();
    }

    private void enableForceInvertColor(boolean enabled) {
        enableForceInvertColor(sContext, enabled);
    }

    private void enableForceInvertColor(Context context, boolean enabled) {
        ShellIdentityUtils.invokeWithShellPermissions(() -> {
            Settings.Secure.putInt(
                    context.getContentResolver(),
                    Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
                    enabled ? 1 : 0
            );
        });
    }

    private void setUpViewAttributes(boolean isLightTheme, boolean isForceDarkAllowed) {
        setUpViewAttributes(sContext, isLightTheme, isForceDarkAllowed);
    }

    private void setUpViewAttributes(Context context, boolean isLightTheme,
            boolean isForceDarkAllowed) {
        ShellIdentityUtils.invokeWithShellPermissions(() -> {
            int themeId;
            if (isForceDarkAllowed) {
                if (isLightTheme) {
                    themeId = R.style.ForceDarkAllowed_Light;
                } else {
                    themeId = R.style.ForceDarkAllowed_Dark;
                }
            } else {
                if (isLightTheme) {
                    themeId = R.style.ForceDarkAllowedFalse_Light;
                } else {
                    themeId = R.style.ForceDarkAllowedFalse_Dark;
                }
            }
            context.setTheme(themeId);
        });

        sInstrumentation.runOnMainSync(() -> {
            View view = new View(context);
            WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                    TYPE_APPLICATION_OVERLAY);
            layoutParams.token = new Binder();
            view.setLayoutParams(layoutParams);
            mViewRootImpl.setView(view, layoutParams, /* panelParentView= */ null);
            mViewRootImpl.updateConfiguration(context.getDisplayNoVerify().getDisplayId());
        });
    }
}
