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
This commit is contained in:
ramindani
2022-12-02 08:33:49 -08:00
committed by Rachel Lee
parent b13f16cfcd
commit 8cfe762269
13 changed files with 669 additions and 0 deletions

View File

@@ -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,
},
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,2 @@
include platform/frameworks/base:/graphics/java/android/graphics/OWNERS
include platform/frameworks/native:/services/surfaceflinger/OWNERS

View File

@@ -0,0 +1,7 @@
{
"presubmit": [
{
"name": "ChoreographerTests"
}
]
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -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>