1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view.choreographertests;
18 
19 import static org.junit.Assert.assertTrue;
20 import static org.junit.Assert.fail;
21 
22 import android.Manifest;
23 import android.hardware.display.DisplayManager;
24 import android.support.test.uiautomator.UiDevice;
25 import android.util.Log;
26 import android.view.Surface;
27 import android.view.SurfaceControl;
28 import android.view.SurfaceHolder;
29 import android.view.SurfaceView;
30 
31 import androidx.lifecycle.Lifecycle;
32 import androidx.test.core.app.ActivityScenario;
33 import androidx.test.ext.junit.runners.AndroidJUnit4;
34 import androidx.test.platform.app.InstrumentationRegistry;
35 
36 import org.junit.After;
37 import org.junit.Before;
38 import org.junit.Test;
39 import org.junit.runner.RunWith;
40 
41 import java.util.concurrent.CountDownLatch;
42 import java.util.concurrent.TimeUnit;
43 
44 @RunWith(AndroidJUnit4.class)
45 public class AttachedChoreographerNativeTest {
46     private static final String TAG = "AttachedChoreographerNativeTest";
47 
48     static {
49         System.loadLibrary("choreographertests_jni");
50     }
51 
52     private final CountDownLatch mSurfaceCreationCountDown = new CountDownLatch(1);
53     private CountDownLatch mTestCompleteSignal;
54     private long mChoreographerPtr;
55     private SurfaceView mSurfaceView;
56     private SurfaceHolder mSurfaceHolder;
57     private ActivityScenario<GraphicsActivity> mScenario;
58     private int mInitialMatchContentFrameRate;
59     private DisplayManager mDisplayManager;
60 
nativeSurfaceControl_getChoreographer(SurfaceControl surfaceControl)61     private static native long nativeSurfaceControl_getChoreographer(SurfaceControl surfaceControl);
nativeTestPostVsyncCallbackAtFrameRate( long choreographerPtr, float expectedFrameRate)62     private native void nativeTestPostVsyncCallbackAtFrameRate(
63             long choreographerPtr, float expectedFrameRate);
64 
65     @Before
setup()66     public void setup() throws Exception {
67         mScenario = ActivityScenario.launch(GraphicsActivity.class);
68         mScenario.moveToState(Lifecycle.State.CREATED);
69         mScenario.onActivity(activity -> {
70             mSurfaceView = activity.findViewById(R.id.surface);
71             mSurfaceHolder = mSurfaceView.getHolder();
72             mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
73                 @Override
74                 public void surfaceChanged(
75                         SurfaceHolder holder, int format, int width, int height) {}
76 
77                 @Override
78                 public void surfaceCreated(SurfaceHolder holder) {
79                     mSurfaceCreationCountDown.countDown();
80                 }
81 
82                 @Override
83                 public void surfaceDestroyed(SurfaceHolder holder) {}
84             });
85         });
86 
87         mScenario.moveToState(Lifecycle.State.RESUMED);
88         UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
89         uiDevice.wakeUp();
90         uiDevice.executeShellCommand("wm dismiss-keyguard");
91 
92         InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
93                 android.Manifest.permission.LOG_COMPAT_CHANGE,
94                 android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
95                 android.Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE,
96                 android.Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS,
97                 Manifest.permission.MANAGE_GAME_MODE);
98         mScenario.onActivity(activity -> {
99             mDisplayManager = activity.getSystemService(DisplayManager.class);
100             mInitialMatchContentFrameRate =
101                     toSwitchingType(mDisplayManager.getMatchContentFrameRateUserPreference());
102             mDisplayManager.setRefreshRateSwitchingType(
103                     DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY);
104             mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true);
105         });
106     }
107 
108     @After
tearDown()109     public void tearDown() {
110         mDisplayManager.setRefreshRateSwitchingType(mInitialMatchContentFrameRate);
111         mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false);
112         InstrumentationRegistry.getInstrumentation()
113                 .getUiAutomation()
114                 .dropShellPermissionIdentity();
115     }
116 
117     @Test
test_choreographer_callbacksForVariousFrameRates()118     public void test_choreographer_callbacksForVariousFrameRates() {
119         for (int divisor : new int[] {2, 3}) {
120             mTestCompleteSignal = new CountDownLatch(1);
121             mScenario.onActivity(activity -> {
122                 if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds= */ 1L)) {
123                     fail("Unable to create surface within 1 Second");
124                 }
125 
126                 SurfaceControl surfaceControl = mSurfaceView.getSurfaceControl();
127                 mChoreographerPtr = nativeSurfaceControl_getChoreographer(surfaceControl);
128                 Log.i(TAG, "mChoreographerPtr value " + mChoreographerPtr);
129 
130                 float displayRefreshRate = activity.getDisplay().getMode().getRefreshRate();
131                 float expectedFrameRate = displayRefreshRate / divisor;
132 
133                 SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
134                 transaction
135                         .setFrameRate(surfaceControl, expectedFrameRate,
136                                 Surface.FRAME_RATE_COMPATIBILITY_DEFAULT)
137                         .addTransactionCommittedListener(Runnable::run,
138                                 () -> {
139                                     assertTrue(mChoreographerPtr != 0L);
140                                     Log.i(TAG, "Testing frame rate of " + expectedFrameRate);
141                                     nativeTestPostVsyncCallbackAtFrameRate(
142                                             mChoreographerPtr, expectedFrameRate);
143                                 })
144                         .apply();
145             });
146             // wait for the previous callbacks to finish before moving to the next divisor
147             if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds= */ 5L)) {
148                 fail("Test for divisor " + divisor + " not finished in 5 seconds");
149             }
150         }
151     }
152 
153     /** Call from native to trigger test completion. */
endTest()154     private void endTest() {
155         Log.i(TAG, "Signal test completion!");
156         mTestCompleteSignal.countDown();
157     }
158 
waitForCountDown(CountDownLatch countDownLatch, long timeoutInSeconds)159     private boolean waitForCountDown(CountDownLatch countDownLatch, long timeoutInSeconds) {
160         try {
161             return !countDownLatch.await(timeoutInSeconds, TimeUnit.SECONDS);
162         } catch (InterruptedException ex) {
163             throw new AssertionError("Test interrupted", ex);
164         }
165     }
166 
toSwitchingType(int matchContentFrameRateUserPreference)167     private int toSwitchingType(int matchContentFrameRateUserPreference) {
168         switch (matchContentFrameRateUserPreference) {
169             case DisplayManager.MATCH_CONTENT_FRAMERATE_NEVER:
170                 return DisplayManager.SWITCHING_TYPE_NONE;
171             case DisplayManager.MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY:
172                 return DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS;
173             case DisplayManager.MATCH_CONTENT_FRAMERATE_ALWAYS:
174                 return DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS;
175             default:
176                 return -1;
177         }
178     }
179 }
180