Attached choreographer tests
Creates an attached Choreographer and
listens for the vsync with a 10Hz and 30Hz refresh rate
Verify callbacks on the attached choreographer
and no callbacks when released.
Test: atest ChoreographerTests
BUG: 258235754
Change-Id: I2f981e6929ff9a5406730d53aa96a7299eb830b8
diff --git a/tests/ChoreographerTests/Android.bp b/tests/ChoreographerTests/Android.bp
new file mode 100644
index 0000000..ff252f7
--- /dev/null
+++ b/tests/ChoreographerTests/Android.bp
@@ -0,0 +1,46 @@
+// Copyright 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "ChoreographerTests",
+ srcs: ["src/**/*.java"],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "compatibility-device-util-axt",
+ "com.google.android.material_material",
+ "truth-prebuilt",
+ ],
+ resource_dirs: ["src/main/res"],
+ certificate: "platform",
+ platform_apis: true,
+ test_suites: ["device-tests"],
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/tests/ChoreographerTests/AndroidManifest.xml b/tests/ChoreographerTests/AndroidManifest.xml
new file mode 100644
index 0000000..3283c90
--- /dev/null
+++ b/tests/ChoreographerTests/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.view.choreographertests">
+
+ <application android:debuggable="true" android:testOnly="true">
+ <uses-library android:name="android.test.runner"/>
+ <activity
+ android:name=".GraphicsActivity"
+ android:exported="false">
+ </activity>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.view.choreographertests"
+ android:label="Tests of android.view.ChoreographerTests">
+ </instrumentation>
+</manifest>
\ No newline at end of file
diff --git a/tests/ChoreographerTests/AndroidTest.xml b/tests/ChoreographerTests/AndroidTest.xml
new file mode 100644
index 0000000..e717699
--- /dev/null
+++ b/tests/ChoreographerTests/AndroidTest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<configuration description="Config for ChoreographerTests cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="test-tag" value="ChoreographerTests"/>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="install-arg" value="-t" />
+ <option name="test-file-name" value="ChoreographerTests.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.view.choreographertests" />
+ <option name="hidden-api-checks" value="false" />
+ <option name="isolated-storage" value="false" />
+ </test>
+</configuration>
\ No newline at end of file
diff --git a/tests/ChoreographerTests/OWNERS b/tests/ChoreographerTests/OWNERS
new file mode 100644
index 0000000..2b7de25
--- /dev/null
+++ b/tests/ChoreographerTests/OWNERS
@@ -0,0 +1,2 @@
+include platform/frameworks/base:/graphics/java/android/graphics/OWNERS
+include platform/frameworks/native:/services/surfaceflinger/OWNERS
\ No newline at end of file
diff --git a/tests/ChoreographerTests/TEST_MAPPING b/tests/ChoreographerTests/TEST_MAPPING
new file mode 100644
index 0000000..16a48ea
--- /dev/null
+++ b/tests/ChoreographerTests/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "ChoreographerTests"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java
new file mode 100644
index 0000000..44112fc
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.choreographertests;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.Manifest;
+import android.app.compat.CompatChanges;
+import android.hardware.display.DisplayManager;
+import android.os.Looper;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+import android.view.Choreographer;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class AttachedChoreographerTest {
+ private static final String TAG = "AttachedChoreographerTest";
+ private static final long DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID = 170503758;
+ private static final int THRESHOLD_MS = 10;
+ private static final int CALLBACK_TIME_10_FPS = 100;
+ private static final int CALLBACK_TIME_30_FPS = 33;
+ private static final int FRAME_ITERATIONS = 21;
+ private static final int CALLBACK_MISSED_THRESHOLD = 2;
+
+ private final CountDownLatch mTestCompleteSignal = new CountDownLatch(2);
+ private final CountDownLatch mSurfaceCreationCountDown = new CountDownLatch(1);
+ private final CountDownLatch mNoCallbackSignal = new CountDownLatch(1);
+ private final CountDownLatch mFramesSignal = new CountDownLatch(FRAME_ITERATIONS);
+
+ private ActivityScenario<GraphicsActivity> mScenario;
+ private int mInitialMatchContentFrameRate;
+ private DisplayManager mDisplayManager;
+ private SurfaceView mSurfaceView;
+ private SurfaceHolder mSurfaceHolder;
+ private boolean mIsFirstCallback = true;
+ private int mCallbackMissedCounter = 0;
+
+ @Before
+ public void setUp() throws Exception {
+ mScenario = ActivityScenario.launch(GraphicsActivity.class);
+ mScenario.moveToState(Lifecycle.State.CREATED);
+ mCallbackMissedCounter = 0;
+ mScenario.onActivity(activity -> {
+ mSurfaceView = activity.findViewById(R.id.surface);
+ mSurfaceHolder = mSurfaceView.getHolder();
+ mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurfaceCreationCountDown.countDown();
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ }
+ });
+ });
+
+
+ UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ uiDevice.wakeUp();
+ uiDevice.executeShellCommand("wm dismiss-keyguard");
+ mScenario.moveToState(Lifecycle.State.RESUMED);
+
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE,
+ Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS,
+ Manifest.permission.MANAGE_GAME_MODE);
+ mScenario.onActivity(activity -> {
+ mDisplayManager = activity.getSystemService(DisplayManager.class);
+ mInitialMatchContentFrameRate = toSwitchingType(
+ mDisplayManager.getMatchContentFrameRateUserPreference());
+ mDisplayManager.setRefreshRateSwitchingType(
+ DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY);
+ mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true);
+ boolean changeIsEnabled =
+ CompatChanges.isChangeEnabled(
+ DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID);
+ Log.i(TAG, "DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGE_ID is "
+ + (changeIsEnabled ? "enabled" : "disabled"));
+ });
+ }
+
+ @After
+ public void tearDown() {
+ mDisplayManager.setRefreshRateSwitchingType(mInitialMatchContentFrameRate);
+ mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false);
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void test_create_choreographer() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ mTestCompleteSignal.countDown();
+ SurfaceControl sc1 = new SurfaceControl(sc, "AttachedChoreographerTests");
+ // Create attached choreographer with getChoreographer
+ sc1.getChoreographer();
+ assertTrue(sc1.hasChoreographer());
+ assertTrue(sc1.isValid());
+ sc1.release();
+
+ SurfaceControl sc2 = new SurfaceControl(sc, "AttachedChoreographerTests");
+ // Create attached choreographer with Looper.myLooper
+ sc2.getChoreographer(Looper.myLooper());
+ assertTrue(sc2.hasChoreographer());
+ assertTrue(sc2.isValid());
+ sc2.release();
+
+ SurfaceControl sc3 = new SurfaceControl(sc, "AttachedChoreographerTests");
+ // Create attached choreographer with Looper.myLooper
+ sc3.getChoreographer(Looper.getMainLooper());
+ assertTrue(sc3.hasChoreographer());
+ assertTrue(sc3.isValid());
+ sc3.release();
+ mTestCompleteSignal.countDown();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void test_copy_surface_control() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ // Create attached choreographer
+ sc.getChoreographer();
+ assertTrue(sc.hasChoreographer());
+
+ // Use copy constructor
+ SurfaceControl copyConstructorSc = new SurfaceControl(sc, "AttachedChoreographerTests");
+ //Choreographer isn't copied over.
+ assertFalse(copyConstructorSc.hasChoreographer());
+ copyConstructorSc.getChoreographer();
+ assertTrue(copyConstructorSc.hasChoreographer());
+ mTestCompleteSignal.countDown();
+
+ // Use copyFrom
+ SurfaceControl copyFromSc = new SurfaceControl();
+ copyFromSc.copyFrom(sc, "AttachedChoreographerTests");
+ //Choreographer isn't copied over.
+ assertFalse(copyFromSc.hasChoreographer());
+ copyFromSc.getChoreographer();
+ assertTrue(copyFromSc.hasChoreographer());
+ mTestCompleteSignal.countDown();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void test_mirror_surface_control() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ // Create attached choreographer
+ sc.getChoreographer();
+ assertTrue(sc.hasChoreographer());
+ mTestCompleteSignal.countDown();
+
+ // Use mirrorSurface
+ SurfaceControl mirrorSc = SurfaceControl.mirrorSurface(sc);
+ //Choreographer isn't copied over.
+ assertFalse(mirrorSc.hasChoreographer());
+ mirrorSc.getChoreographer();
+ assertTrue(mirrorSc.hasChoreographer());
+ // make SurfaceControl invalid by releasing it.
+ mirrorSc.release();
+
+ assertTrue(sc.isValid());
+ assertFalse(mirrorSc.isValid());
+ assertFalse(mirrorSc.hasChoreographer());
+ assertThrows(NullPointerException.class, mirrorSc::getChoreographer);
+ mTestCompleteSignal.countDown();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void test_postFrameCallback() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ sc.getChoreographer().postFrameCallback(
+ frameTimeNanos -> mTestCompleteSignal.countDown());
+
+ SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
+ Choreographer copyChoreographer = copySc.getChoreographer();
+ // make SurfaceControl invalid by releasing it.
+ copySc.release();
+
+ assertTrue(sc.isValid());
+ assertFalse(copySc.isValid());
+ copyChoreographer.postFrameCallback(frameTimeNanos -> mNoCallbackSignal.countDown());
+ assertDoesReceiveCallback();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void test_postFrameCallbackDelayed() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ sc.getChoreographer(Looper.getMainLooper()).postFrameCallbackDelayed(
+ callback -> mTestCompleteSignal.countDown(),
+ /* delayMillis */ 5);
+
+ SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
+ Choreographer copyChoreographer = copySc.getChoreographer();
+ // make SurfaceControl invalid by releasing it.
+ copySc.release();
+
+ assertTrue(sc.isValid());
+ assertFalse(copySc.isValid());
+ copyChoreographer.postFrameCallbackDelayed(
+ frameTimeNanos -> mNoCallbackSignal.countDown(), /* delayMillis */5);
+ assertDoesReceiveCallback();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void test_postCallback() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ sc.getChoreographer().postCallback(Choreographer.CALLBACK_COMMIT,
+ mTestCompleteSignal::countDown, /* token */ this);
+
+ SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
+ Choreographer copyChoreographer = copySc.getChoreographer();
+ // make SurfaceControl invalid by releasing it.
+ copySc.release();
+
+ assertTrue(sc.isValid());
+ assertFalse(copySc.isValid());
+ copyChoreographer.postCallback(Choreographer.CALLBACK_COMMIT,
+ mNoCallbackSignal::countDown, /* token */ this);
+ assertDoesReceiveCallback();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void test_postCallbackDelayed() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ sc.getChoreographer().postCallbackDelayed(Choreographer.CALLBACK_COMMIT,
+ mTestCompleteSignal::countDown, /* token */ this, /* delayMillis */ 5);
+
+ SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
+ Choreographer copyChoreographer = copySc.getChoreographer();
+ // make SurfaceControl invalid by releasing it.
+ copySc.release();
+
+ assertTrue(sc.isValid());
+ assertFalse(copySc.isValid());
+ copyChoreographer.postCallbackDelayed(Choreographer.CALLBACK_COMMIT,
+ mNoCallbackSignal::countDown, /* token */ this, /* delayMillis */ 5);
+ assertDoesReceiveCallback();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void test_postVsyncCallback() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeout */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ sc.getChoreographer().postVsyncCallback(data -> mTestCompleteSignal.countDown());
+
+ SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
+ Choreographer copyChoreographer = copySc.getChoreographer();
+ // make SurfaceControl invalid by releasing it.
+ copySc.release();
+
+ assertTrue(sc.isValid());
+ assertFalse(copySc.isValid());
+ copyChoreographer.postVsyncCallback(data -> mNoCallbackSignal.countDown());
+ assertDoesReceiveCallback();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void test_choreographer_10Hz_refreshRate() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ Choreographer choreographer = sc.getChoreographer();
+ SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+ transaction.setFrameRate(sc, 10.0f, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
+ .addTransactionCommittedListener(Runnable::run,
+ () -> verifyVsyncCallbacks(choreographer,
+ CALLBACK_TIME_10_FPS))
+ .apply();
+ mTestCompleteSignal.countDown();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 5L)) {
+ fail("Test not finished in 5 Seconds");
+ }
+ }
+
+ @Test
+ public void test_choreographer_30Hz_refreshRate() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ Choreographer choreographer = sc.getChoreographer();
+ SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+ transaction.setFrameRate(sc, 30.0f, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
+ .addTransactionCommittedListener(Runnable::run,
+ () -> verifyVsyncCallbacks(choreographer,
+ CALLBACK_TIME_30_FPS))
+ .apply();
+ mTestCompleteSignal.countDown();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 5L)) {
+ fail("Test not finished in 5 Seconds");
+ }
+ }
+
+ private void verifyVsyncCallbacks(Choreographer choreographer, int callbackDurationMs) {
+ long callbackRequestedTimeNs = System.nanoTime();
+ choreographer.postVsyncCallback(frameData -> {
+ mFramesSignal.countDown();
+ final long frameCount = mFramesSignal.getCount();
+ if (frameCount > 0) {
+ if (!mIsFirstCallback) {
+ // Skip the first callback as it takes 1 frame
+ // to reflect the new refresh rate
+ long callbackDurationDiffMs = getCallbackDurationDiffInMs(
+ frameData.getFrameTimeNanos(),
+ callbackRequestedTimeNs, callbackDurationMs);
+ if (callbackDurationDiffMs < 0 || callbackDurationDiffMs > THRESHOLD_MS) {
+ mCallbackMissedCounter++;
+ Log.e(TAG, "Frame #" + Math.abs(frameCount - FRAME_ITERATIONS)
+ + " vsync callback failed, expected callback in "
+ + callbackDurationMs
+ + " With threshold of " + THRESHOLD_MS
+ + " but actual duration difference is " + callbackDurationDiffMs);
+ }
+ }
+ mIsFirstCallback = false;
+ verifyVsyncCallbacks(choreographer, callbackDurationMs);
+ } else {
+ assertTrue("Missed timeline for " + mCallbackMissedCounter + " callbacks, while "
+ + CALLBACK_MISSED_THRESHOLD + " missed callbacks are allowed",
+ mCallbackMissedCounter <= CALLBACK_MISSED_THRESHOLD);
+ mTestCompleteSignal.countDown();
+ }
+ });
+ }
+
+ private long getCallbackDurationDiffInMs(long callbackTimeNs, long requestedTimeNs,
+ int expectedCallbackMs) {
+ long actualTimeMs = TimeUnit.NANOSECONDS.toMillis(callbackTimeNs)
+ - TimeUnit.NANOSECONDS.toMillis(requestedTimeNs);
+ return Math.abs(expectedCallbackMs - actualTimeMs);
+ }
+
+ private boolean waitForCountDown(CountDownLatch countDownLatch, long timeoutInSeconds) {
+ try {
+ return !countDownLatch.await(timeoutInSeconds, TimeUnit.SECONDS);
+ } catch (InterruptedException ex) {
+ throw new AssertionError("Test interrupted", ex);
+ }
+ }
+
+ private int toSwitchingType(int matchContentFrameRateUserPreference) {
+ switch (matchContentFrameRateUserPreference) {
+ case DisplayManager.MATCH_CONTENT_FRAMERATE_NEVER:
+ return DisplayManager.SWITCHING_TYPE_NONE;
+ case DisplayManager.MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY:
+ return DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS;
+ case DisplayManager.MATCH_CONTENT_FRAMERATE_ALWAYS:
+ return DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS;
+ default:
+ return -1;
+ }
+ }
+
+ private void assertDoesReceiveCallback() {
+ try {
+ if (mNoCallbackSignal.await(/* timeout */ 50L, TimeUnit.MILLISECONDS)) {
+ fail("Callback not supposed to be generated");
+ } else {
+ mTestCompleteSignal.countDown();
+ }
+ } catch (InterruptedException e) {
+ fail("Callback wait is interrupted " + e);
+ }
+ }
+}
diff --git a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/GraphicsActivity.java b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/GraphicsActivity.java
new file mode 100644
index 0000000..50a6850
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/GraphicsActivity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.choreographertests;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class GraphicsActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_attached_choreographer);
+ }
+}
diff --git a/tests/ChoreographerTests/src/main/res/layout/activity_attached_choreographer.xml b/tests/ChoreographerTests/src/main/res/layout/activity_attached_choreographer.xml
new file mode 100644
index 0000000..d6c8212
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/res/layout/activity_attached_choreographer.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <SurfaceView
+ android:id="@+id/surface"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/tests/ChoreographerTests/src/main/res/mipmap-hdpi/ic_launcher.png b/tests/ChoreographerTests/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/ChoreographerTests/src/main/res/mipmap-mdpi/ic_launcher.png b/tests/ChoreographerTests/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/tests/ChoreographerTests/src/main/res/mipmap-xhdpi/ic_launcher.png b/tests/ChoreographerTests/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/ChoreographerTests/src/main/res/mipmap-xxhdpi/ic_launcher.png b/tests/ChoreographerTests/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/ChoreographerTests/src/main/res/values/strings.xml b/tests/ChoreographerTests/src/main/res/values/strings.xml
new file mode 100644
index 0000000..e66b001
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <string name="app_name">ChoreographerTests</string>
+</resources>