1 /*
2  * Copyright 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.surfacecontroltests;
18 
19 import static org.junit.Assert.assertNotNull;
20 import static org.junit.Assert.assertNotSame;
21 import static org.junit.Assert.assertTrue;
22 import static org.junit.Assert.fail;
23 import static org.junit.Assume.assumeTrue;
24 
25 import android.app.Activity;
26 import android.graphics.Canvas;
27 import android.graphics.Color;
28 import android.graphics.Rect;
29 import android.hardware.display.DisplayManager;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.Looper;
33 import android.sysprop.SurfaceFlingerProperties;
34 import android.util.Log;
35 import android.view.Display;
36 import android.view.Surface;
37 import android.view.SurfaceControl;
38 import android.view.SurfaceHolder;
39 import android.view.SurfaceView;
40 import android.view.ViewGroup;
41 
42 import java.io.PrintWriter;
43 import java.io.StringWriter;
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.List;
47 import java.util.stream.Collectors;
48 
49 /**
50  * An Activity to help with frame rate testing.
51  */
52 public class GraphicsActivity extends Activity {
53     private static final String TAG = "GraphicsActivity";
54     private static final long FRAME_RATE_SWITCH_GRACE_PERIOD_SECONDS = 2;
55     private static final long STABLE_FRAME_RATE_WAIT_SECONDS = 1;
56     private static final long POST_BUFFER_INTERVAL_MILLIS = 500;
57     private static final int PRECONDITION_WAIT_MAX_ATTEMPTS = 5;
58     private static final long PRECONDITION_WAIT_TIMEOUT_SECONDS = 20;
59     private static final long PRECONDITION_VIOLATION_WAIT_TIMEOUT_SECONDS = 3;
60     private static final float FRAME_RATE_TOLERANCE = 0.01f;
61 
62     static class FpsRange {
63         // The max difference between refresh rates in order to be considered equal.
64         private static final double FPS_THRESHOLD = 0.001;
65         double mMin;
66         double mMax;
FpsRange(double min, double max)67         FpsRange(double min, double max) {
68             mMin = min;
69             mMax = max;
70         }
includes(double fps)71         public boolean includes(double fps) {
72             return fps >= mMin - FPS_THRESHOLD && mMax + FPS_THRESHOLD >= fps;
73         }
74     }
75 
76     // TODO(b/293651105): Unhardcode category fps range mapping
77     private static final FpsRange FRAME_RATE_CATEGORY_HIGH = new FpsRange(90, 120);
78     private static final FpsRange FRAME_RATE_CATEGORY_NORMAL = new FpsRange(60, 90);
79     private static final FpsRange FRAME_RATE_CATEGORY_LOW = new FpsRange(30, 30);
80 
81     private DisplayManager mDisplayManager;
82     private SurfaceView mSurfaceView;
83     private Handler mHandler = new Handler(Looper.getMainLooper());
84     private final Object mLock = new Object();
85     private Surface mSurface = null;
86     private float mDisplayModeRefreshRate;
87     private float mDisplayRefreshRate;
88     private ModeChangedEvents mModeChangedEvents = new ModeChangedEvents();
89 
90     private enum ActivityState { RUNNING, PAUSED, DESTROYED }
91 
92     private ActivityState mActivityState;
93 
94     SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
95         @Override
96         public void surfaceCreated(SurfaceHolder holder) {
97             synchronized (mLock) {
98                 mSurface = holder.getSurface();
99                 mLock.notify();
100             }
101         }
102 
103         @Override
104         public void surfaceDestroyed(SurfaceHolder holder) {
105             synchronized (mLock) {
106                 mSurface = null;
107                 mLock.notify();
108             }
109         }
110 
111         @Override
112         public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
113     };
114 
115     DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() {
116         @Override
117         public void onDisplayAdded(int displayId) {}
118 
119         @Override
120         public void onDisplayChanged(int displayId) {
121             if (displayId != Display.DEFAULT_DISPLAY) {
122                 return;
123             }
124             synchronized (mLock) {
125                 Display display = mDisplayManager.getDisplay(displayId);
126                 Display.Mode mode = display.getMode();
127                 mModeChangedEvents.add(mode);
128                 float displayModeRefreshRate = mode.getRefreshRate();
129                 float displayRefreshRate = display.getRefreshRate();
130                 if (displayModeRefreshRate != mDisplayModeRefreshRate
131                         || displayRefreshRate != mDisplayRefreshRate) {
132                     Log.i(TAG,
133                             String.format("Refresh rate changed: (mode) %.2f --> %.2f, "
134                                             + "(display) %.2f --> %.2f",
135                                     mDisplayModeRefreshRate, displayModeRefreshRate,
136                                     mDisplayRefreshRate, displayRefreshRate));
137                     mDisplayModeRefreshRate = displayModeRefreshRate;
138                     mDisplayRefreshRate = displayRefreshRate;
139                     mLock.notify();
140                 }
141             }
142         }
143 
144         @Override
145         public void onDisplayRemoved(int displayId) {}
146     };
147 
148     // Wrapper around ArrayList for which the only allowed mutable operation is add().
149     // We use this to store all mode change events during a test. When we need to iterate over
150     // all mode changes during a certain operation, we use the number of events in the beginning
151     // and in the end. It's important to never clear or modify the elements in this list hence the
152     // wrapper.
153     private static class ModeChangedEvents {
154         private List<Display.Mode> mEvents = new ArrayList<>();
155 
add(Display.Mode mode)156         public void add(Display.Mode mode) {
157             mEvents.add(mode);
158         }
159 
get(int i)160         public Display.Mode get(int i) {
161             return mEvents.get(i);
162         }
163 
size()164         public int size() {
165             return mEvents.size();
166         }
167     }
168 
169     private static class PreconditionViolatedException extends RuntimeException {
PreconditionViolatedException()170         PreconditionViolatedException() {}
171     }
172 
173     private static class FrameRateTimeoutException extends RuntimeException {
FrameRateTimeoutException(float expectedFrameRate, float deviceFrameRate)174         FrameRateTimeoutException(float expectedFrameRate, float deviceFrameRate) {
175             this.expectedFrameRate = expectedFrameRate;
176             this.deviceFrameRate = deviceFrameRate;
177         }
178 
FrameRateTimeoutException(List<Float> expectedFrameRates, float deviceFrameRate)179         FrameRateTimeoutException(List<Float> expectedFrameRates, float deviceFrameRate) {
180             this.expectedFrameRates = expectedFrameRates;
181             this.deviceFrameRate = deviceFrameRate;
182         }
183 
184         public float expectedFrameRate;
185         public float deviceFrameRate;
186         public List<Float> expectedFrameRates;
187     }
188 
189     public enum Api {
190         // Much of the code is copied from the SetFrameRate cts test. Add APIs as support grows.
191         SURFACE_CONTROL("SurfaceControl");
192 
193         private final String mName;
Api(String name)194         Api(String name) {
195             mName = name;
196         }
197 
toString()198         public String toString() {
199             return mName;
200         }
201     }
202 
203     private static class TestSurface {
204         private String mName;
205         private SurfaceControl mSurfaceControl;
206         private Surface mSurface;
207         private int mColor;
208         private boolean mLastBufferPostTimeValid;
209         private long mLastBufferPostTime;
210 
TestSurface(SurfaceControl parentSurfaceControl, Surface parentSurface, String name, Rect destFrame, boolean visible, int color)211         TestSurface(SurfaceControl parentSurfaceControl, Surface parentSurface, String name,
212                 Rect destFrame, boolean visible, int color) {
213             mName = name;
214             mColor = color;
215 
216             assertNotNull("No parent surface", parentSurfaceControl);
217             mSurfaceControl = new SurfaceControl.Builder()
218                                       .setParent(parentSurfaceControl)
219                                       .setName(mName)
220                                       .setBufferSize(destFrame.right - destFrame.left,
221                                               destFrame.bottom - destFrame.top)
222                                       .build();
223             try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
224                 transaction.setGeometry(mSurfaceControl, null, destFrame, Surface.ROTATION_0)
225                         .apply();
226             }
227             mSurface = new Surface(mSurfaceControl);
228 
229             setVisibility(visible);
230             postBuffer();
231         }
232 
getSurface()233         Surface getSurface() {
234             return mSurface;
235         }
236 
getSurfaceControl()237         SurfaceControl getSurfaceControl() {
238             return mSurfaceControl;
239         }
240 
setFrameRate(float frameRate)241         public int setFrameRate(float frameRate) {
242             return setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
243         }
244 
setFrameRate( float frameRate, @Surface.FrameRateCompatibility int compatibility)245         public int setFrameRate(
246                 float frameRate, @Surface.FrameRateCompatibility int compatibility) {
247             Log.i(TAG,
248                     String.format("Setting frame rate for %s: frameRate=%.2f", mName, frameRate));
249 
250             int rc = 0;
251             try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
252                 transaction.setFrameRate(mSurfaceControl, frameRate, compatibility);
253                 transaction.apply();
254             }
255             return rc;
256         }
257 
setFrameRateCategory(int category)258         public int setFrameRateCategory(int category) {
259             Log.i(TAG,
260                     String.format(
261                             "Setting frame rate category for %s: category=%d", mName, category));
262 
263             int rc = 0;
264             try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
265                 transaction.setFrameRateCategory(mSurfaceControl, category, false);
266                 transaction.apply();
267             }
268             return rc;
269         }
270 
setFrameRateSelectionStrategy(int strategy)271         public int setFrameRateSelectionStrategy(int strategy) {
272             Log.i(TAG,
273                     String.format("Setting frame rate selection strategy for %s: strategy=%d",
274                             mName, strategy));
275 
276             int rc = 0;
277             try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
278                 transaction.setFrameRateSelectionStrategy(mSurfaceControl, strategy);
279                 transaction.apply();
280             }
281             return rc;
282         }
283 
setVisibility(boolean visible)284         public void setVisibility(boolean visible) {
285             Log.i(TAG,
286                     String.format("Setting visibility for %s: %s", mName,
287                             visible ? "visible" : "hidden"));
288             try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
289                 transaction.setVisibility(mSurfaceControl, visible).apply();
290             }
291         }
292 
postBuffer()293         public void postBuffer() {
294             mLastBufferPostTimeValid = true;
295             mLastBufferPostTime = System.nanoTime();
296             Canvas canvas = mSurface.lockHardwareCanvas();
297             canvas.drawColor(mColor);
298             mSurface.unlockCanvasAndPost(canvas);
299         }
300 
getLastBufferPostTime()301         public long getLastBufferPostTime() {
302             assertTrue("No buffer posted yet", mLastBufferPostTimeValid);
303             return mLastBufferPostTime;
304         }
305 
release()306         public void release() {
307             if (mSurface != null) {
308                 mSurface.release();
309                 mSurface = null;
310             }
311             if (mSurfaceControl != null) {
312                 try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
313                     transaction.reparent(mSurfaceControl, null).apply();
314                 }
315                 mSurfaceControl.release();
316                 mSurfaceControl = null;
317             }
318         }
319 
320         @Override
finalize()321         protected void finalize() throws Throwable {
322             try {
323                 release();
324             } finally {
325                 super.finalize();
326             }
327         }
328     }
329 
330     @Override
onCreate(Bundle savedInstanceState)331     protected void onCreate(Bundle savedInstanceState) {
332         super.onCreate(savedInstanceState);
333         synchronized (mLock) {
334             mDisplayManager = getSystemService(DisplayManager.class);
335             Display display = getDisplay();
336             Display.Mode mode = display.getMode();
337             mDisplayModeRefreshRate = mode.getRefreshRate();
338             mDisplayRefreshRate = display.getRefreshRate();
339             // Insert the initial mode so we have the full display mode history.
340             mModeChangedEvents.add(mode);
341             mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
342             mSurfaceView = new SurfaceView(this);
343             mSurfaceView.setWillNotDraw(false);
344             mSurfaceView.setZOrderOnTop(true);
345             setContentView(mSurfaceView,
346                     new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
347                             ViewGroup.LayoutParams.MATCH_PARENT));
348             mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
349         }
350     }
351 
352     @Override
onDestroy()353     protected void onDestroy() {
354         super.onDestroy();
355         mDisplayManager.unregisterDisplayListener(mDisplayListener);
356         synchronized (mLock) {
357             mActivityState = ActivityState.DESTROYED;
358             mLock.notify();
359         }
360     }
361 
362     @Override
onPause()363     public void onPause() {
364         super.onPause();
365         synchronized (mLock) {
366             mActivityState = ActivityState.PAUSED;
367             mLock.notify();
368         }
369     }
370 
371     @Override
onResume()372     public void onResume() {
373         super.onResume();
374         synchronized (mLock) {
375             mActivityState = ActivityState.RUNNING;
376             mLock.notify();
377         }
378     }
379 
380     // Returns the refresh rates with the same resolution as "mode".
getRefreshRates(Display.Mode mode, Display display)381     private ArrayList<Float> getRefreshRates(Display.Mode mode, Display display) {
382         Display.Mode[] modes = display.getSupportedModes();
383         ArrayList<Float> frameRates = new ArrayList<>();
384         for (Display.Mode supportedMode : modes) {
385             if (hasSameResolution(supportedMode, mode)) {
386                 frameRates.add(supportedMode.getRefreshRate());
387             }
388         }
389         Collections.sort(frameRates);
390         ArrayList<Float> uniqueFrameRates = new ArrayList<>();
391         for (float frameRate : frameRates) {
392             if (uniqueFrameRates.isEmpty()
393                     || frameRate - uniqueFrameRates.get(uniqueFrameRates.size() - 1)
394                             >= FRAME_RATE_TOLERANCE) {
395                 uniqueFrameRates.add(frameRate);
396             }
397         }
398         Log.i(TAG,
399                 "**** Available display refresh rates: "
400                         + uniqueFrameRates.stream()
401                                   .map(Object::toString)
402                                   .collect(Collectors.joining(", ")));
403         return uniqueFrameRates;
404     }
405 
hasSameResolution(Display.Mode mode1, Display.Mode mode2)406     private boolean hasSameResolution(Display.Mode mode1, Display.Mode mode2) {
407         return mode1.getPhysicalHeight() == mode2.getPhysicalHeight()
408                 && mode1.getPhysicalWidth() == mode2.getPhysicalWidth();
409     }
410 
isFrameRateMultiple(float higherFrameRate, float lowerFrameRate)411     private boolean isFrameRateMultiple(float higherFrameRate, float lowerFrameRate) {
412         float multiple = higherFrameRate / lowerFrameRate;
413         int roundedMultiple = Math.round(multiple);
414         return roundedMultiple > 0
415                 && Math.abs(roundedMultiple * lowerFrameRate - higherFrameRate)
416                 <= FRAME_RATE_TOLERANCE;
417     }
418 
frameRateEquals(float frameRate1, float frameRate2)419     private boolean frameRateEquals(float frameRate1, float frameRate2) {
420         return Math.abs(frameRate1 - frameRate2) <= FRAME_RATE_TOLERANCE;
421     }
422 
423     // Waits until our SurfaceHolder has a surface and the activity is resumed.
waitForPreconditions()424     private void waitForPreconditions() throws InterruptedException {
425         assertNotSame(
426                 "Activity was unexpectedly destroyed", mActivityState, ActivityState.DESTROYED);
427         if (mSurface == null || mActivityState != ActivityState.RUNNING) {
428             Log.i(TAG,
429                     String.format(
430                             "Waiting for preconditions. Have surface? %b. Activity resumed? %b.",
431                             mSurface != null, mActivityState == ActivityState.RUNNING));
432         }
433         long nowNanos = System.nanoTime();
434         long endTimeNanos = nowNanos + PRECONDITION_WAIT_TIMEOUT_SECONDS * 1_000_000_000L;
435         while (mSurface == null || mActivityState != ActivityState.RUNNING) {
436             long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000;
437             assertTrue(String.format("Timed out waiting for preconditions. Have surface? %b."
438                                        + " Activity resumed? %b.",
439                                mSurface != null, mActivityState == ActivityState.RUNNING),
440                     timeRemainingMillis > 0);
441             mLock.wait(timeRemainingMillis);
442             assertNotSame(
443                     "Activity was unexpectedly destroyed", mActivityState, ActivityState.DESTROYED);
444             nowNanos = System.nanoTime();
445         }
446         // Make sure any previous mode changes are completed.
447         waitForStableFrameRate();
448     }
449 
450     // Returns true if we encounter a precondition violation, false otherwise.
waitForPreconditionViolation()451     private boolean waitForPreconditionViolation() throws InterruptedException {
452         assertNotSame(
453                 "Activity was unexpectedly destroyed", mActivityState, ActivityState.DESTROYED);
454         long nowNanos = System.nanoTime();
455         long endTimeNanos = nowNanos + PRECONDITION_VIOLATION_WAIT_TIMEOUT_SECONDS * 1_000_000_000L;
456         while (mSurface != null && mActivityState == ActivityState.RUNNING) {
457             long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000;
458             if (timeRemainingMillis <= 0) {
459                 break;
460             }
461             mLock.wait(timeRemainingMillis);
462             assertNotSame(
463                     "Activity was unexpectedly destroyed", mActivityState, ActivityState.DESTROYED);
464             nowNanos = System.nanoTime();
465         }
466         return mSurface == null || mActivityState != ActivityState.RUNNING;
467     }
468 
verifyPreconditions()469     private void verifyPreconditions() {
470         if (mSurface == null || mActivityState != ActivityState.RUNNING) {
471             throw new PreconditionViolatedException();
472         }
473     }
474 
475     // Returns true if we reached waitUntilNanos, false if some other event occurred.
waitForEvents(long waitUntilNanos, TestSurface[] surfaces)476     private boolean waitForEvents(long waitUntilNanos, TestSurface[] surfaces)
477             throws InterruptedException {
478         int numModeChangedEvents = mModeChangedEvents.size();
479         long nowNanos = System.nanoTime();
480         while (nowNanos < waitUntilNanos) {
481             long surfacePostTime = Long.MAX_VALUE;
482             for (TestSurface surface : surfaces) {
483                 surfacePostTime = Math.min(surfacePostTime,
484                         surface.getLastBufferPostTime()
485                                 + (POST_BUFFER_INTERVAL_MILLIS * 1_000_000L));
486             }
487             long timeoutNs = Math.min(waitUntilNanos, surfacePostTime) - nowNanos;
488             long timeoutMs = timeoutNs / 1_000_000L;
489             int remainderNs = (int) (timeoutNs % 1_000_000L);
490             // Don't call wait(0, 0) - it blocks indefinitely.
491             if (timeoutMs > 0 || remainderNs > 0) {
492                 mLock.wait(timeoutMs, remainderNs);
493             }
494             nowNanos = System.nanoTime();
495             verifyPreconditions();
496             if (mModeChangedEvents.size() > numModeChangedEvents) {
497                 return false;
498             }
499             if (nowNanos >= surfacePostTime) {
500                 for (TestSurface surface : surfaces) {
501                     surface.postBuffer();
502                 }
503             }
504         }
505         return true;
506     }
507 
waitForStableFrameRate(TestSurface... surfaces)508     private void waitForStableFrameRate(TestSurface... surfaces) throws InterruptedException {
509         verifyFrameRates(List.of(), surfaces);
510     }
511 
verifyExactAndStableFrameRate( float expectedFrameRate, TestSurface... surfaces)512     private void verifyExactAndStableFrameRate(
513             float expectedFrameRate,
514             TestSurface... surfaces) throws InterruptedException {
515         verifyFrameRate(List.of(expectedFrameRate), false, surfaces);
516     }
517 
verifyCompatibleAndStableFrameRate( float expectedFrameRate, TestSurface... surfaces)518     private void verifyCompatibleAndStableFrameRate(
519             float expectedFrameRate,
520             TestSurface... surfaces) throws InterruptedException {
521         verifyFrameRate(List.of(expectedFrameRate), true, surfaces);
522     }
523 
524     /** Verify stable frame rate at one of the expectedFrameRates. */
verifyFrameRates(List<Float> expectedFrameRates, TestSurface... surfaces)525     private void verifyFrameRates(List<Float> expectedFrameRates, TestSurface... surfaces)
526             throws InterruptedException {
527         verifyFrameRate(expectedFrameRates, true, surfaces);
528     }
529 
530     // Set expectedFrameRates to empty to verify only stable frame rate.
verifyFrameRate(List<Float> expectedFrameRates, boolean multiplesAllowed, TestSurface... surfaces)531     private void verifyFrameRate(List<Float> expectedFrameRates, boolean multiplesAllowed,
532             TestSurface... surfaces) throws InterruptedException {
533         Log.i(TAG, "Verifying compatible and stable frame rate");
534         long nowNanos = System.nanoTime();
535         long gracePeriodEndTimeNanos =
536                 nowNanos + FRAME_RATE_SWITCH_GRACE_PERIOD_SECONDS * 1_000_000_000L;
537         while (true) {
538             if (expectedFrameRates.size() == 1) {
539                 float expectedFrameRate = expectedFrameRates.get(0);
540                 // Wait until we switch to a compatible frame rate.
541                 Log.i(TAG,
542                         String.format(
543                                 "Verifying expected frame rate: actual=%.2f, expected=%.2f",
544                                 multiplesAllowed ? mDisplayModeRefreshRate : mDisplayRefreshRate,
545                                 expectedFrameRate));
546                 if (multiplesAllowed) {
547                     while (!isFrameRateMultiple(mDisplayModeRefreshRate, expectedFrameRate)
548                             && !waitForEvents(gracePeriodEndTimeNanos, surfaces)) {
549                         // Empty
550                     }
551                 } else {
552                     while (!frameRateEquals(mDisplayRefreshRate, expectedFrameRate)
553                             && !waitForEvents(gracePeriodEndTimeNanos, surfaces)) {
554                         // Empty
555                     }
556                 }
557                 nowNanos = System.nanoTime();
558                 if (nowNanos >= gracePeriodEndTimeNanos) {
559                     throw new FrameRateTimeoutException(expectedFrameRate,
560                             multiplesAllowed ? mDisplayModeRefreshRate : mDisplayRefreshRate);
561                 }
562             }
563 
564             // We've switched to a compatible frame rate. Now wait for a while to see if we stay at
565             // that frame rate.
566             long endTimeNanos = nowNanos + STABLE_FRAME_RATE_WAIT_SECONDS * 1_000_000_000L;
567             while (endTimeNanos > nowNanos) {
568                 int numModeChangedEvents = mModeChangedEvents.size();
569                 if (waitForEvents(endTimeNanos, surfaces)) {
570                     // Verify any expected frame rate since there are multiple that will suffice.
571                     // Mainly to account for running tests on real devices, where other non-test
572                     // layers may affect the outcome.
573                     if (expectedFrameRates.size() > 1) {
574                         for (float expectedFrameRate : expectedFrameRates) {
575                             if (isFrameRateMultiple(mDisplayModeRefreshRate, expectedFrameRate)) {
576                                 return;
577                             }
578                         }
579                         // The frame rate is stable but it is not one of the expected frame rates.
580                         throw new FrameRateTimeoutException(
581                                 expectedFrameRates, mDisplayModeRefreshRate);
582                     } else {
583                         Log.i(TAG,
584                                 String.format("Stable frame rate %.2f verified",
585                                         multiplesAllowed ? mDisplayModeRefreshRate
586                                                          : mDisplayRefreshRate));
587                         return;
588                     }
589                 }
590                 nowNanos = System.nanoTime();
591                 if (mModeChangedEvents.size() > numModeChangedEvents) {
592                     break;
593                 }
594             }
595         }
596     }
597 
verifyModeSwitchesDontChangeResolution(int fromId, int toId)598     private void verifyModeSwitchesDontChangeResolution(int fromId, int toId) {
599         assertTrue(fromId <= toId);
600         for (int eventId = fromId; eventId < toId; eventId++) {
601             Display.Mode fromMode = mModeChangedEvents.get(eventId - 1);
602             Display.Mode toMode = mModeChangedEvents.get(eventId);
603             assertTrue("Resolution change was not expected, but there was such from " + fromMode
604                             + " to " + toMode + ".",
605                     hasSameResolution(fromMode, toMode));
606         }
607     }
608 
609     // Unfortunately, we can't just use Consumer<Api> for this, because we need to declare that it
610     // throws InterruptedException.
611     private interface TestInterface {
run()612         void run() throws InterruptedException;
613     }
614 
615     private interface OneSurfaceTestInterface {
run(TestSurface surface)616         void run(TestSurface surface) throws InterruptedException;
617     }
618 
619     // Runs the given test for each api, waiting for the preconditions to be satisfied before
620     // running the test. Includes retry logic when the test fails because the preconditions are
621     // violated. E.g. if we lose the SurfaceHolder's surface, or the activity is paused/resumed,
622     // we'll retry the test. The activity being intermittently paused/resumed has been observed to
623     // cause test failures in practice.
runTestsWithPreconditions(TestInterface test, String testName)624     private void runTestsWithPreconditions(TestInterface test, String testName)
625             throws InterruptedException {
626         synchronized (mLock) {
627             for (Api api : Api.values()) {
628                 Log.i(TAG, String.format("Testing %s %s", api, testName));
629                 int attempts = 0;
630                 boolean testPassed = false;
631                 try {
632                     while (!testPassed) {
633                         waitForPreconditions();
634                         try {
635                             test.run();
636                             testPassed = true;
637                         } catch (PreconditionViolatedException exc) {
638                             // The logic below will retry if we're below max attempts.
639                         } catch (FrameRateTimeoutException exc) {
640                             StringWriter stringWriter = new StringWriter();
641                             PrintWriter printWriter = new PrintWriter(stringWriter);
642                             exc.printStackTrace(printWriter);
643                             String stackTrace = stringWriter.toString();
644 
645                             // Sometimes we get a test timeout failure before we get the
646                             // notification that the activity was paused, and it was the pause that
647                             // caused the timeout failure. Wait for a bit to see if we get notified
648                             // of a precondition violation, and if so, retry the test. Otherwise
649                             // fail.
650                             if (exc.expectedFrameRates.isEmpty()) {
651                                 assertTrue(
652                                         String.format(
653                                                 "Timed out waiting for a stable and compatible"
654                                                         + " frame rate."
655                                                         + " expected=%.2f received=%.2f."
656                                                         + " Stack trace: " + stackTrace,
657                                                 exc.expectedFrameRate, exc.deviceFrameRate),
658                                         waitForPreconditionViolation());
659                             } else {
660                                 assertTrue(
661                                         String.format(
662                                                 "Timed out waiting for a stable and compatible"
663                                                         + " frame rate."
664                                                         + " expected={%.2f} received=%.2f."
665                                                         + " Stack trace: " + stackTrace,
666                                                 exc.expectedFrameRates.stream()
667                                                         .map(Object::toString)
668                                                         .collect(Collectors.joining(", ")),
669                                                 exc.deviceFrameRate),
670                                         waitForPreconditionViolation());
671                             }
672                         }
673 
674                         if (!testPassed) {
675                             Log.i(TAG,
676                                     String.format("Preconditions violated while running the test."
677                                                     + " Have surface? %b. Activity resumed? %b.",
678                                             mSurface != null,
679                                             mActivityState == ActivityState.RUNNING));
680                             attempts++;
681                             assertTrue(String.format(
682                                                "Exceeded %d precondition wait attempts. Giving up.",
683                                                PRECONDITION_WAIT_MAX_ATTEMPTS),
684                                     attempts < PRECONDITION_WAIT_MAX_ATTEMPTS);
685                         }
686                     }
687                 } finally {
688                     String passFailMessage = String.format(
689                             "%s %s %s", testPassed ? "Passed" : "Failed", api, testName);
690                     if (testPassed) {
691                         Log.i(TAG, passFailMessage);
692                     } else {
693                         Log.e(TAG, passFailMessage);
694                     }
695                 }
696             }
697         }
698     }
699 
700     private void runOneSurfaceTest(OneSurfaceTestInterface test) throws InterruptedException {
701         TestSurface surface = null;
702         try {
703             surface = new TestSurface(mSurfaceView.getSurfaceControl(), mSurface, "testSurface",
704                     mSurfaceView.getHolder().getSurfaceFrame(),
705                     /*visible=*/true, Color.RED);
706 
707             test.run(surface);
708         } finally {
709             if (surface != null) {
710                 surface.release();
711             }
712         }
713     }
714 
715     private void testSurfaceControlFrameRateCompatibilityInternal(
716             @Surface.FrameRateCompatibility int compatibility) throws InterruptedException {
717         runOneSurfaceTest((TestSurface surface) -> {
718             Log.i(TAG,
719                     "**** Running testSurfaceControlFrameRateCompatibility with compatibility "
720                             + compatibility);
721 
722             List<Float> expectedFrameRates = getExpectedFrameRateForCompatibility(compatibility);
723             Log.i(TAG,
724                     "Expected frame rates: "
725                             + expectedFrameRates.stream()
726                                       .map(Object::toString)
727                                       .collect(Collectors.joining(", ")));
728             int initialNumEvents = mModeChangedEvents.size();
729             surface.setFrameRate(70.f, compatibility);
730             verifyFrameRates(expectedFrameRates, surface);
731             verifyModeSwitchesDontChangeResolution(initialNumEvents, mModeChangedEvents.size());
732         });
733     }
734 
testSurfaceControlFrameRateCompatibility( @urface.FrameRateCompatibility int compatibility)735     public void testSurfaceControlFrameRateCompatibility(
736             @Surface.FrameRateCompatibility int compatibility) throws InterruptedException {
737         runTestsWithPreconditions(
738                 () -> testSurfaceControlFrameRateCompatibilityInternal(compatibility),
739                 "frame rate compatibility=" + compatibility);
740     }
741 
testSurfaceControlFrameRateCategoryInternal( @urface.FrameRateCategory int category)742     private void testSurfaceControlFrameRateCategoryInternal(
743             @Surface.FrameRateCategory int category) throws InterruptedException {
744         runOneSurfaceTest((TestSurface surface) -> {
745             Log.i(TAG, "**** Running testSurfaceControlFrameRateCategory for category " + category);
746 
747             List<Float> expectedFrameRates = getExpectedFrameRateForCategory(category);
748             int initialNumEvents = mModeChangedEvents.size();
749             surface.setFrameRateCategory(category);
750             verifyFrameRates(expectedFrameRates, surface);
751             verifyModeSwitchesDontChangeResolution(initialNumEvents, mModeChangedEvents.size());
752         });
753     }
754 
testSurfaceControlFrameRateCategory(@urface.FrameRateCategory int category)755     public void testSurfaceControlFrameRateCategory(@Surface.FrameRateCategory int category)
756             throws InterruptedException {
757         runTestsWithPreconditions(()
758                 -> testSurfaceControlFrameRateCategoryInternal(category),
759                 "frame rate category=" + category);
760     }
761 
testSurfaceControlFrameRateSelectionStrategyInternal(int parentStrategy)762     private void testSurfaceControlFrameRateSelectionStrategyInternal(int parentStrategy)
763             throws InterruptedException {
764         Log.i(TAG,
765                 "**** Running testSurfaceControlFrameRateSelectionStrategy for strategy "
766                         + parentStrategy);
767         TestSurface parent = null;
768         TestSurface child = null;
769         try {
770             parent = new TestSurface(mSurfaceView.getSurfaceControl(), mSurface,
771                     "testSurfaceParent", mSurfaceView.getHolder().getSurfaceFrame(),
772                     /*visible=*/true, Color.RED);
773             child = new TestSurface(parent.getSurfaceControl(), parent.getSurface(),
774                     "testSurfaceChild", mSurfaceView.getHolder().getSurfaceFrame(),
775                     /*visible=*/true, Color.BLUE);
776 
777             // Test
778             Display display = getDisplay();
779             List<Float> frameRates = getRefreshRates(display.getMode(), display);
780             assumeTrue("**** SKIPPED due to frame rate override disabled",
781                     SurfaceFlingerProperties.enable_frame_rate_override().orElse(true));
782             float childFrameRate = Collections.max(frameRates);
783             float parentFrameRate = childFrameRate / 2;
784             int initialNumEvents = mModeChangedEvents.size();
785             parent.setFrameRateSelectionStrategy(parentStrategy);
786 
787             // For Self case, we want to test that child gets default behavior
788             if (parentStrategy == SurfaceControl.FRAME_RATE_SELECTION_STRATEGY_SELF) {
789                 parent.setFrameRateCategory(Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE);
790             } else {
791                 parent.setFrameRate(parentFrameRate);
792                 child.setFrameRate(childFrameRate);
793             }
794 
795             // Verify
796             float expectedFrameRate =
797                     parentStrategy == SurfaceControl.FRAME_RATE_SELECTION_STRATEGY_OVERRIDE_CHILDREN
798                     ? parentFrameRate
799                     : childFrameRate;
800             verifyExactAndStableFrameRate(expectedFrameRate, parent, child);
801             verifyModeSwitchesDontChangeResolution(initialNumEvents, mModeChangedEvents.size());
802         } finally {
803             if (parent != null) {
804                 parent.release();
805             }
806             if (child != null) {
807                 child.release();
808             }
809         }
810     }
811 
testSurfaceControlFrameRateSelectionStrategy(int parentStrategy)812     public void testSurfaceControlFrameRateSelectionStrategy(int parentStrategy)
813             throws InterruptedException {
814         runTestsWithPreconditions(
815                 () -> testSurfaceControlFrameRateSelectionStrategyInternal(parentStrategy),
816                 "frame rate strategy=" + parentStrategy);
817     }
818 
getExpectedFrameRateForCompatibility(int compatibility)819     private List<Float> getExpectedFrameRateForCompatibility(int compatibility) {
820         assumeTrue("**** testSurfaceControlFrameRateCompatibility SKIPPED for compatibility "
821                         + compatibility,
822                 compatibility == Surface.FRAME_RATE_COMPATIBILITY_GTE);
823 
824         Display display = getDisplay();
825         List<Float> expectedFrameRates = getRefreshRates(display.getMode(), display)
826                                                  .stream()
827                                                  .filter(rate -> rate >= 70.f)
828                                                  .collect(Collectors.toList());
829 
830         assumeTrue("**** testSurfaceControlFrameRateCompatibility SKIPPED because no refresh rate "
831                         + "is >= 30",
832                 !expectedFrameRates.isEmpty());
833         return expectedFrameRates;
834     }
835 
getExpectedFrameRateForCategory(int category)836     private List<Float> getExpectedFrameRateForCategory(int category) {
837         Display display = getDisplay();
838         List<Float> frameRates = getRefreshRates(display.getMode(), display);
839 
840         if (category == Surface.FRAME_RATE_CATEGORY_DEFAULT) {
841             // Max due to default vote and no other frame rate specifications.
842             return List.of(Collections.max(frameRates));
843         } else if (category == Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE) {
844             return frameRates;
845         }
846 
847         FpsRange categoryRange = convertCategory(category);
848         List<Float> expectedFrameRates = frameRates.stream()
849                                                  .filter(fps -> categoryRange.includes(fps))
850                                                  .collect(Collectors.toList());
851         assumeTrue("**** testSurfaceControlFrameRateCategory SKIPPED for category " + category,
852                 !expectedFrameRates.isEmpty());
853         return expectedFrameRates;
854     }
855 
convertCategory(int category)856     private FpsRange convertCategory(int category) {
857         switch (category) {
858             case Surface.FRAME_RATE_CATEGORY_HIGH_HINT:
859             case Surface.FRAME_RATE_CATEGORY_HIGH:
860                 return FRAME_RATE_CATEGORY_HIGH;
861             case Surface.FRAME_RATE_CATEGORY_NORMAL:
862                 return FRAME_RATE_CATEGORY_NORMAL;
863             case Surface.FRAME_RATE_CATEGORY_LOW:
864                 return FRAME_RATE_CATEGORY_LOW;
865             case Surface.FRAME_RATE_CATEGORY_DEFAULT:
866             case Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE:
867                 fail("Should not get range for category=" + category);
868         }
869         return new FpsRange(0, 0);
870     }
871 }
872