Merge changes from topic "psc1"

* changes:
  Attached choreographer tests
  Attach the Choreographer instance with SurfaceControl
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 400a324..34afd8a 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3148,6 +3148,9 @@
 
   public final class SurfaceControl implements android.os.Parcelable {
     ctor public SurfaceControl(@NonNull android.view.SurfaceControl, @NonNull String);
+    method @NonNull public android.view.Choreographer getChoreographer();
+    method @NonNull public android.view.Choreographer getChoreographer(@NonNull android.os.Looper);
+    method public boolean hasChoreographer();
     method public boolean isSameSurface(@NonNull android.view.SurfaceControl);
   }
 
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index 91febcd..3dc79cf 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -270,10 +270,14 @@
     private static final int CALLBACK_LAST = CALLBACK_COMMIT;
 
     private Choreographer(Looper looper, int vsyncSource) {
+        this(looper, vsyncSource, /* layerHandle */ 0L);
+    }
+
+    private Choreographer(Looper looper, int vsyncSource, long layerHandle) {
         mLooper = looper;
         mHandler = new FrameHandler(looper);
         mDisplayEventReceiver = USE_VSYNC
-                ? new FrameDisplayEventReceiver(looper, vsyncSource)
+                ? new FrameDisplayEventReceiver(looper, vsyncSource, layerHandle)
                 : null;
         mLastFrameTimeNanos = Long.MIN_VALUE;
 
@@ -313,6 +317,26 @@
     }
 
     /**
+     * Gets the choreographer associated with the SurfaceControl.
+     *
+     * @param layerHandle to which the choreographer will be attached.
+     * @param looper      the choreographer is attached on this looper.
+     *
+     * @return The choreographer for the looper which is attached
+     * to the sourced SurfaceControl::mNativeHandle.
+     * @throws IllegalStateException if the looper sourced is null.
+     * @hide
+     */
+    @NonNull
+    static Choreographer getInstanceForSurfaceControl(long layerHandle,
+            @NonNull Looper looper) {
+        if (looper == null) {
+            throw new IllegalStateException("The current thread must have a looper!");
+        }
+        return new Choreographer(looper, VSYNC_SOURCE_APP, layerHandle);
+    }
+
+    /**
      * @return The Choreographer of the main thread, if it exists, or {@code null} otherwise.
      * @hide
      */
@@ -334,6 +358,15 @@
     }
 
     /**
+     * Dispose the DisplayEventReceiver on the Choreographer.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    void invalidate() {
+        dispose();
+    }
+
+    /**
      * The amount of time, in milliseconds, between each frame of the animation.
      * <p>
      * This is a requested time that the animation will attempt to honor, but the actual delay
@@ -1166,8 +1199,8 @@
         private int mFrame;
         private VsyncEventData mLastVsyncEventData = new VsyncEventData();
 
-        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
-            super(looper, vsyncSource, 0);
+        FrameDisplayEventReceiver(Looper looper, int vsyncSource, long layerHandle) {
+            super(looper, vsyncSource, /* eventRegistration */ 0, layerHandle);
         }
 
         // TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java
index ce7606a0..26fda34 100644
--- a/core/java/android/view/DisplayEventReceiver.java
+++ b/core/java/android/view/DisplayEventReceiver.java
@@ -80,7 +80,7 @@
     private MessageQueue mMessageQueue;
 
     private static native long nativeInit(WeakReference<DisplayEventReceiver> receiver,
-            MessageQueue messageQueue, int vsyncSource, int eventRegistration);
+            MessageQueue messageQueue, int vsyncSource, int eventRegistration, long layerHandle);
     private static native void nativeDispose(long receiverPtr);
     @FastNative
     private static native void nativeScheduleVsync(long receiverPtr);
@@ -93,7 +93,11 @@
      */
     @UnsupportedAppUsage
     public DisplayEventReceiver(Looper looper) {
-        this(looper, VSYNC_SOURCE_APP, 0);
+        this(looper, VSYNC_SOURCE_APP, /* eventRegistration */ 0, /* layerHandle */ 0L);
+    }
+
+    public DisplayEventReceiver(Looper looper, int vsyncSource, int eventRegistration) {
+        this(looper, vsyncSource, eventRegistration, /* layerHandle */ 0L);
     }
 
     /**
@@ -103,15 +107,17 @@
      * @param vsyncSource The source of the vsync tick. Must be on of the VSYNC_SOURCE_* values.
      * @param eventRegistration Which events to dispatch. Must be a bitfield consist of the
      * EVENT_REGISTRATION_*_FLAG values.
+     * @param layerHandle Layer to which the current instance is attached to
      */
-    public DisplayEventReceiver(Looper looper, int vsyncSource, int eventRegistration) {
+    public DisplayEventReceiver(Looper looper, int vsyncSource, int eventRegistration,
+            long layerHandle) {
         if (looper == null) {
             throw new IllegalArgumentException("looper must not be null");
         }
 
         mMessageQueue = looper.getQueue();
         mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
-                vsyncSource, eventRegistration);
+                vsyncSource, eventRegistration, layerHandle);
     }
 
     @Override
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 54e1a53..b003659 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -64,6 +64,7 @@
 import android.opengl.EGLSync;
 import android.os.Build;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.RemoteException;
@@ -463,6 +464,10 @@
     public long mNativeObject;
     private long mNativeHandle;
 
+    private final Object mChoreographerLock = new Object();
+    @GuardedBy("mChoreographerLock")
+    private Choreographer mChoreographer;
+
     // TODO: Move width/height to native and fix locking through out.
     private final Object mLock = new Object();
     @GuardedBy("mLock")
@@ -1269,6 +1274,59 @@
     }
 
     /**
+     * Returns the associated {@link Choreographer} instance with the
+     * current instance of the SurfaceControl.
+     * Must be called from a thread that already has a {@link android.os.Looper}
+     * associated with it.
+     * If there is no {@link Choreographer} associated with the SurfaceControl then a new instance
+     * of the {@link Choreographer} is created.
+     *
+     * @hide
+     */
+    @TestApi
+    public @NonNull Choreographer getChoreographer() {
+        return getChoreographer(Looper.myLooper());
+    }
+
+    /**
+     * Returns the associated {@link Choreographer} instance with the
+     * current instance of the SurfaceControl.
+     * If there is no {@link Choreographer} associated with the SurfaceControl then a new instance
+     * of the {@link Choreographer} is created.
+     *
+     * @param looper the choreographer is attached on this looper
+     *
+     * @hide
+     */
+    @TestApi
+    public @NonNull Choreographer getChoreographer(@NonNull Looper looper) {
+        checkNotReleased();
+        synchronized (mChoreographerLock) {
+            if (mChoreographer != null) {
+                return mChoreographer;
+            }
+
+            mChoreographer = Choreographer.getInstanceForSurfaceControl(mNativeHandle, looper);
+            return mChoreographer;
+        }
+    }
+
+    /**
+     * Returns true if {@link Choreographer} is present otherwise false.
+     * To check the validity use {@link #isValid} on the SurfaceControl, a valid SurfaceControl with
+     * choreographer will have the valid Choreographer.
+     *
+     * @hide
+     */
+    @TestApi
+    @UnsupportedAppUsage
+    public boolean hasChoreographer() {
+        synchronized (mChoreographerLock) {
+            return mChoreographer != null;
+        }
+    }
+
+    /**
      * Write to a protocol buffer output stream. Protocol buffer message definition is at {@link
      * android.view.SurfaceControlProto}.
      *
@@ -1325,6 +1383,13 @@
             mNativeObject = 0;
             mNativeHandle = 0;
             mCloseGuard.close();
+            synchronized (mChoreographerLock) {
+                if (mChoreographer != null) {
+                    mChoreographer.invalidate();
+                    // TODO(b/266121235): Use NativeAllocationRegistry to clean up Choreographer.
+                    mChoreographer = null;
+                }
+            }
         }
     }
 
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index a8d8a43..8855b78 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -65,7 +65,7 @@
 public:
     NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak,
                                const sp<MessageQueue>& messageQueue, jint vsyncSource,
-                               jint eventRegistration);
+                               jint eventRegistration, jlong layerHandle);
 
     void dispose();
 
@@ -88,11 +88,15 @@
 
 NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak,
                                                        const sp<MessageQueue>& messageQueue,
-                                                       jint vsyncSource, jint eventRegistration)
+                                                       jint vsyncSource, jint eventRegistration,
+                                                       jlong layerHandle)
       : DisplayEventDispatcher(messageQueue->getLooper(),
                                static_cast<gui::ISurfaceComposer::VsyncSource>(vsyncSource),
                                static_cast<gui::ISurfaceComposer::EventRegistration>(
-                                       eventRegistration)),
+                                       eventRegistration),
+                               layerHandle != 0 ? sp<IBinder>::fromExisting(
+                                                          reinterpret_cast<IBinder*>(layerHandle))
+                                                : nullptr),
         mReceiverWeakGlobal(env->NewGlobalRef(receiverWeak)),
         mMessageQueue(messageQueue) {
     ALOGV("receiver %p ~ Initializing display event receiver.", this);
@@ -214,7 +218,7 @@
 }
 
 static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject messageQueueObj,
-                        jint vsyncSource, jint eventRegistration) {
+                        jint vsyncSource, jint eventRegistration, jlong layerHandle) {
     sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
     if (messageQueue == NULL) {
         jniThrowRuntimeException(env, "MessageQueue is not initialized.");
@@ -223,7 +227,7 @@
 
     sp<NativeDisplayEventReceiver> receiver =
             new NativeDisplayEventReceiver(env, receiverWeak, messageQueue, vsyncSource,
-                                           eventRegistration);
+                                           eventRegistration, layerHandle);
     status_t status = receiver->initialize();
     if (status) {
         String8 message;
@@ -268,7 +272,7 @@
 
 static const JNINativeMethod gMethods[] = {
         /* name, signature, funcPtr */
-        {"nativeInit", "(Ljava/lang/ref/WeakReference;Landroid/os/MessageQueue;II)J",
+        {"nativeInit", "(Ljava/lang/ref/WeakReference;Landroid/os/MessageQueue;IIJ)J",
          (void*)nativeInit},
         {"nativeDispose", "(J)V", (void*)nativeDispose},
         // @FastNative
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>