diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 400a324b43113..34afd8a6a8e41 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3148,6 +3148,9 @@ package android.view { 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 91febcdcb0c44..3dc79cf13422f 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -270,10 +270,14 @@ public final class Choreographer { 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; @@ -312,6 +316,26 @@ public final class Choreographer { return sSfThreadInstance.get(); } + /** + * 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 @@ -333,6 +357,15 @@ public final class Choreographer { mDisplayEventReceiver.dispose(); } + /** + * Dispose the DisplayEventReceiver on the Choreographer. + * @hide + */ + @UnsupportedAppUsage + void invalidate() { + dispose(); + } + /** * The amount of time, in milliseconds, between each frame of the animation. *

@@ -1166,8 +1199,8 @@ public final class Choreographer { 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 ce7606a0cf70d..26fda3444c4ec 100644 --- a/core/java/android/view/DisplayEventReceiver.java +++ b/core/java/android/view/DisplayEventReceiver.java @@ -80,7 +80,7 @@ public abstract class DisplayEventReceiver { private MessageQueue mMessageQueue; private static native long nativeInit(WeakReference 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 @@ public abstract class DisplayEventReceiver { */ @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 @@ public abstract class DisplayEventReceiver { * @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(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 54e1a53ed7c99..b003659affd32 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -64,6 +64,7 @@ import android.opengl.EGLDisplay; 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 final class SurfaceControl implements Parcelable { 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") @@ -1268,6 +1273,59 @@ public final class SurfaceControl implements Parcelable { return other.mNativeHandle == mNativeHandle; } + /** + * 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 @@ public final class SurfaceControl implements Parcelable { 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 a8d8a431666a2..8855b7806cc18 100644 --- a/core/jni/android_view_DisplayEventReceiver.cpp +++ b/core/jni/android_view_DisplayEventReceiver.cpp @@ -65,7 +65,7 @@ class NativeDisplayEventReceiver : public DisplayEventDispatcher { public: NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak, const sp& messageQueue, jint vsyncSource, - jint eventRegistration); + jint eventRegistration, jlong layerHandle); void dispose(); @@ -88,11 +88,15 @@ private: NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak, const sp& messageQueue, - jint vsyncSource, jint eventRegistration) + jint vsyncSource, jint eventRegistration, + jlong layerHandle) : DisplayEventDispatcher(messageQueue->getLooper(), static_cast(vsyncSource), static_cast( - eventRegistration)), + eventRegistration), + layerHandle != 0 ? sp::fromExisting( + reinterpret_cast(layerHandle)) + : nullptr), mReceiverWeakGlobal(env->NewGlobalRef(receiverWeak)), mMessageQueue(messageQueue) { ALOGV("receiver %p ~ Initializing display event receiver.", this); @@ -214,7 +218,7 @@ void NativeDisplayEventReceiver::dispatchFrameRateOverrides( } static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject messageQueueObj, - jint vsyncSource, jint eventRegistration) { + jint vsyncSource, jint eventRegistration, jlong layerHandle) { sp messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj); if (messageQueue == NULL) { jniThrowRuntimeException(env, "MessageQueue is not initialized."); @@ -223,7 +227,7 @@ static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject sp receiver = new NativeDisplayEventReceiver(env, receiverWeak, messageQueue, vsyncSource, - eventRegistration); + eventRegistration, layerHandle); status_t status = receiver->initialize(); if (status) { String8 message; @@ -268,7 +272,7 @@ static jobject nativeGetLatestVsyncEventData(JNIEnv* env, jclass clazz, jlong re 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 0000000000000..ff252f717591b --- /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 0000000000000..3283c90bd2e55 --- /dev/null +++ b/tests/ChoreographerTests/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/ChoreographerTests/AndroidTest.xml b/tests/ChoreographerTests/AndroidTest.xml new file mode 100644 index 0000000000000..e7176996a49a8 --- /dev/null +++ b/tests/ChoreographerTests/AndroidTest.xml @@ -0,0 +1,30 @@ + + + + \ No newline at end of file diff --git a/tests/ChoreographerTests/OWNERS b/tests/ChoreographerTests/OWNERS new file mode 100644 index 0000000000000..2b7de25555879 --- /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 0000000000000..16a48eadc0cd8 --- /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 0000000000000..44112fc3957ce --- /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 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 0000000000000..50a68508334e0 --- /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 0000000000000..d6c821228c9a1 --- /dev/null +++ b/tests/ChoreographerTests/src/main/res/layout/activity_attached_choreographer.xml @@ -0,0 +1,31 @@ + + + + + + + + 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 0000000000000..cde69bcccec65 Binary files /dev/null and b/tests/ChoreographerTests/src/main/res/mipmap-hdpi/ic_launcher.png 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 0000000000000..c133a0cbd379f Binary files /dev/null and b/tests/ChoreographerTests/src/main/res/mipmap-mdpi/ic_launcher.png 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 0000000000000..bfa42f0e7b91d Binary files /dev/null and b/tests/ChoreographerTests/src/main/res/mipmap-xhdpi/ic_launcher.png 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 0000000000000..324e72cdd7480 Binary files /dev/null and b/tests/ChoreographerTests/src/main/res/mipmap-xxhdpi/ic_launcher.png 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 0000000000000..e66b001e105ea --- /dev/null +++ b/tests/ChoreographerTests/src/main/res/values/strings.xml @@ -0,0 +1,18 @@ + + + + ChoreographerTests +