• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 package android.view.surfacecontrol.cts;
17 
18 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
20 import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER;
21 import static android.server.wm.CtsWindowInfoUtils.waitForWindowOnTop;
22 import static android.view.SurfaceControl.JankData.JANK_APPLICATION;
23 import static android.view.cts.surfacevalidator.BitmapPixelChecker.validateScreenshot;
24 
25 import static androidx.test.core.app.ActivityScenario.launch;
26 
27 import static com.google.common.truth.Truth.assertThat;
28 import static com.google.common.truth.Truth.assertWithMessage;
29 
30 import static org.junit.Assert.assertTrue;
31 import static org.junit.Assert.fail;
32 import static org.junit.Assume.assumeFalse;
33 
34 import android.app.Activity;
35 import android.content.Context;
36 import android.content.pm.ActivityInfo;
37 import android.content.pm.PackageManager;
38 import android.graphics.Canvas;
39 import android.graphics.Color;
40 import android.graphics.Insets;
41 import android.graphics.Rect;
42 import android.platform.test.annotations.RequiresFlagsEnabled;
43 import android.platform.test.flag.junit.CheckFlagsRule;
44 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
45 import android.server.wm.IgnoreOrientationRequestSession;
46 import android.server.wm.WindowManagerStateHelper;
47 import android.util.Log;
48 import android.view.AttachedSurfaceControl;
49 import android.view.Gravity;
50 import android.view.Surface;
51 import android.view.SurfaceControl;
52 import android.view.SurfaceControlViewHost;
53 import android.view.SurfaceHolder;
54 import android.view.SurfaceView;
55 import android.view.View;
56 import android.view.ViewTreeObserver;
57 import android.view.cts.surfacevalidator.BitmapPixelChecker;
58 import android.widget.FrameLayout;
59 
60 import androidx.annotation.NonNull;
61 import androidx.test.core.app.ActivityScenario;
62 import androidx.test.filters.SmallTest;
63 import androidx.test.platform.app.InstrumentationRegistry;
64 
65 import com.android.window.flags.Flags;
66 
67 import org.junit.After;
68 import org.junit.Assert;
69 import org.junit.Assume;
70 import org.junit.Before;
71 import org.junit.Rule;
72 import org.junit.Test;
73 import org.junit.rules.TestName;
74 
75 import java.util.concurrent.CountDownLatch;
76 import java.util.concurrent.TimeUnit;
77 import java.util.function.IntConsumer;
78 
79 @SmallTest
80 public class AttachedSurfaceControlTest {
81     private static final String TAG = "AttachedSurfaceControlTest";
82     private IgnoreOrientationRequestSession mOrientationSession;
83     private WindowManagerStateHelper mWmState;
84 
85     private static final long WAIT_TIMEOUT_S = 5L * HW_TIMEOUT_MULTIPLIER;
86 
87     @Rule
88     public TestName mName = new TestName();
89 
90     @Rule
91     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
92 
93 
94     private static class TransformHintListener implements
95             AttachedSurfaceControl.OnBufferTransformHintChangedListener {
96         Activity activity;
97         int expectedOrientation;
98         CountDownLatch latch = new CountDownLatch(1);
99         IntConsumer hintConsumer;
100 
TransformHintListener(Activity activity, int expectedOrientation, IntConsumer hintConsumer)101         TransformHintListener(Activity activity, int expectedOrientation,
102                 IntConsumer hintConsumer) {
103             this.activity = activity;
104             this.expectedOrientation = expectedOrientation;
105             this.hintConsumer = hintConsumer;
106         }
107 
108         @Override
onBufferTransformHintChanged(int hint)109         public void onBufferTransformHintChanged(int hint) {
110             int orientation = activity.getResources().getConfiguration().orientation;
111             Log.d(TAG, "onBufferTransformHintChanged: orientation actual=" + orientation
112                     + " expected=" + expectedOrientation + " transformHint=" + hint);
113             Assert.assertEquals("Failed to switch orientation hint=" + hint, orientation,
114                     expectedOrientation);
115 
116             // Check the callback value matches the call to get the transform hint.
117             int actualTransformHint =
118                     activity.getWindow().getRootSurfaceControl().getBufferTransformHint();
119             Assert.assertEquals(
120                     "Callback " + hint + " doesn't match transform hint=" + actualTransformHint,
121                     hint,
122                     actualTransformHint);
123             hintConsumer.accept(hint);
124             latch.countDown();
125             activity.getWindow().getRootSurfaceControl()
126                     .removeOnBufferTransformHintChangedListener(this);
127         }
128     }
129 
130     @Before
setup()131     public void setup() throws InterruptedException {
132         mOrientationSession = new IgnoreOrientationRequestSession(false /* enable */);
133         mWmState = new WindowManagerStateHelper();
134     }
135 
supportRotationCheck()136     private void supportRotationCheck() {
137         PackageManager pm =
138                 InstrumentationRegistry.getInstrumentation().getContext().getPackageManager();
139         boolean supportsRotation = pm.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT)
140                 && pm.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE);
141         mWmState.computeState();
142         final boolean isFixedToUserRotation = mWmState.isFixedToUserRotation();
143         Assume.assumeTrue(supportsRotation && !isFixedToUserRotation);
144     }
145 
146     @After
teardown()147     public void teardown() {
148         if (mOrientationSession != null) {
149             mOrientationSession.close();
150         }
151     }
152 
153     @Test
testOnBufferTransformHintChangedListener()154     public void testOnBufferTransformHintChangedListener() throws InterruptedException {
155         supportRotationCheck();
156 
157         try (ActivityScenario<?> scenario = launch(HandleConfigurationActivity.class)) {
158             Activity activity = awaitActivityStart(scenario);
159 
160             final int[] transformHintResult = new int[2];
161             final CountDownLatch[] firstCallback = new CountDownLatch[1];
162             final CountDownLatch[] secondCallback = new CountDownLatch[1];
163             mWmState.computeState();
164             assumeFalse("Skipping test: display area is ignoring orientation request",
165                     mWmState.isTaskDisplayAreaIgnoringOrientationRequest(
166                             activity.getComponentName()));
167             int requestedOrientation = getRequestedOrientation(activity);
168             TransformHintListener listener = new TransformHintListener(activity,
169                     requestedOrientation, hint -> transformHintResult[0] = hint);
170             firstCallback[0] = listener.latch;
171             activity.getWindow().getRootSurfaceControl()
172                     .addOnBufferTransformHintChangedListener(listener);
173             setRequestedOrientation(activity, requestedOrientation);
174             // Check we get a callback since the orientation has changed and we expect transform
175             // hint to change.
176             Assert.assertTrue(firstCallback[0].await(10, TimeUnit.SECONDS));
177 
178             requestedOrientation = getRequestedOrientation(activity);
179             TransformHintListener secondListener = new TransformHintListener(activity,
180                     requestedOrientation, hint -> transformHintResult[1] = hint);
181             secondCallback[0] = secondListener.latch;
182             activity.getWindow().getRootSurfaceControl()
183                     .addOnBufferTransformHintChangedListener(secondListener);
184             setRequestedOrientation(activity, requestedOrientation);
185             // Check we get a callback since the orientation has changed and we expect transform
186             // hint to change.
187             Assert.assertTrue(secondCallback[0].await(10, TimeUnit.SECONDS));
188 
189             // If the app orientation was changed, we should get a different transform hint
190             Assert.assertNotEquals(transformHintResult[0], transformHintResult[1]);
191         }
192     }
193 
getRequestedOrientation(Activity activity)194     private int getRequestedOrientation(Activity activity) {
195         int currentOrientation = activity.getResources().getConfiguration().orientation;
196         return currentOrientation == ORIENTATION_LANDSCAPE ? ORIENTATION_PORTRAIT
197                 : ORIENTATION_LANDSCAPE;
198     }
199 
setRequestedOrientation(Activity activity, int requestedOrientation)200     private void setRequestedOrientation(Activity activity,
201             /* @Configuration.Orientation */ int requestedOrientation) {
202         /* @ActivityInfo.ScreenOrientation */
203         Log.d(TAG, "setRequestedOrientation: requestedOrientation=" + requestedOrientation);
204         int screenOrientation =
205                 requestedOrientation == ORIENTATION_LANDSCAPE
206                         ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
207                         : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
208         activity.setRequestedOrientation(screenOrientation);
209     }
210 
211     @Test
testOnBufferTransformHintChangesFromLandToSea()212     public void testOnBufferTransformHintChangesFromLandToSea() throws InterruptedException {
213         supportRotationCheck();
214 
215         try (ActivityScenario<?> scenario = launch(HandleConfigurationActivity.class)) {
216             Activity activity = awaitActivityStart(scenario);
217 
218             final int[] transformHintResult = new int[2];
219             final CountDownLatch[] firstCallback = new CountDownLatch[1];
220             final CountDownLatch[] secondCallback = new CountDownLatch[1];
221             mWmState.computeState();
222             assumeFalse("Skipping test: display area is ignoring orientation request",
223                     mWmState.isTaskDisplayAreaIgnoringOrientationRequest(
224                             activity.getComponentName()));
225             if (activity.getResources().getConfiguration().orientation
226                     != ORIENTATION_LANDSCAPE) {
227                 Log.d(TAG, "Request landscape orientation");
228                 TransformHintListener listener = new TransformHintListener(activity,
229                         ORIENTATION_LANDSCAPE, hint -> {
230                     transformHintResult[0] = hint;
231                     Log.d(TAG, "firstListener fired with hint =" + hint);
232                 });
233                 firstCallback[0] = listener.latch;
234                 activity.getWindow().getRootSurfaceControl()
235                         .addOnBufferTransformHintChangedListener(listener);
236                 setRequestedOrientation(activity, ORIENTATION_LANDSCAPE);
237                 Assert.assertTrue(firstCallback[0].await(10, TimeUnit.SECONDS));
238             } else {
239                 transformHintResult[0] =
240                         activity.getWindow().getRootSurfaceControl().getBufferTransformHint();
241                 Log.d(TAG, "Skipped request landscape orientation: hint=" + transformHintResult[0]);
242             }
243 
244             TransformHintListener secondListener = new TransformHintListener(activity,
245                     ORIENTATION_LANDSCAPE, hint -> {
246                 transformHintResult[1] = hint;
247                 Log.d(TAG, "secondListener fired with hint =" + hint);
248             });
249             secondCallback[0] = secondListener.latch;
250             activity.getWindow().getRootSurfaceControl()
251                     .addOnBufferTransformHintChangedListener(secondListener);
252             Log.d(TAG, "Requesting reverse landscape");
253             activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
254 
255             Assert.assertTrue(secondCallback[0].await(10, TimeUnit.SECONDS));
256             Assert.assertNotEquals(transformHintResult[0], transformHintResult[1]);
257         }
258     }
259 
260     private static class GreenAnchorViewWithInsets extends View {
261         SurfaceControl mSurfaceControl;
262         final Surface mSurface;
263 
264         private final Rect mChildBoundingInsets;
265 
266         private final CountDownLatch mDrawCompleteLatch = new CountDownLatch(1);
267 
268         private boolean mChildScAttached;
269 
GreenAnchorViewWithInsets(Context c, Rect insets)270         GreenAnchorViewWithInsets(Context c, Rect insets) {
271             super(c, null, 0, 0);
272             mSurfaceControl = new SurfaceControl.Builder()
273                     .setName("SurfaceAnchorView")
274                     .setBufferSize(100, 100)
275                     .build();
276             mSurface = new Surface(mSurfaceControl);
277             Canvas canvas = mSurface.lockHardwareCanvas();
278             canvas.drawColor(Color.GREEN);
279             mSurface.unlockCanvasAndPost(canvas);
280             mChildBoundingInsets = insets;
281 
282             getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
283                 @Override
284                 public boolean onPreDraw() {
285                     attachChildSc();
286                     getViewTreeObserver().removeOnPreDrawListener(this);
287                     return true;
288                 }
289             });
290         }
291 
292         @Override
onAttachedToWindow()293         protected void onAttachedToWindow() {
294             super.onAttachedToWindow();
295             attachChildSc();
296         }
297 
attachChildSc()298         private void attachChildSc() {
299             if (mChildScAttached) {
300                 return;
301             }
302             // This should be called even if buildReparentTransaction fails the first time since
303             // the second call will come from preDrawListener which is called after bounding insets
304             // are updated in VRI.
305             getRootSurfaceControl().setChildBoundingInsets(mChildBoundingInsets);
306 
307             SurfaceControl.Transaction t =
308                     getRootSurfaceControl().buildReparentTransaction(mSurfaceControl);
309 
310             if (t == null) {
311                 // TODO (b/286406553) SurfaceControl was not yet setup. Wait until the draw request
312                 // to attach since the SurfaceControl will be created by that point. This can be
313                 // cleaned up when the bug is fixed.
314                 return;
315             }
316 
317             t.setLayer(mSurfaceControl, 1).setVisibility(mSurfaceControl, true);
318             t.addTransactionCommittedListener(Runnable::run, mDrawCompleteLatch::countDown);
319             getRootSurfaceControl().applyTransactionOnDraw(t);
320             mChildScAttached = true;
321         }
322 
323         @Override
onDetachedFromWindow()324         protected void onDetachedFromWindow() {
325             new SurfaceControl.Transaction().reparent(mSurfaceControl, null).apply();
326             mSurfaceControl.release();
327             mSurface.release();
328             mChildScAttached = false;
329 
330             super.onDetachedFromWindow();
331         }
332 
waitForDrawn()333         public void waitForDrawn() {
334             try {
335                 assertTrue("Failed to wait for frame to draw",
336                         mDrawCompleteLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
337             } catch (InterruptedException e) {
338                 fail();
339             }
340         }
341     }
342 
343     @Test
testCropWithChildBoundingInsets()344     public void testCropWithChildBoundingInsets() throws Throwable {
345         try (ActivityScenario<TestActivity> scenario = launch(TestActivity.class)) {
346             final GreenAnchorViewWithInsets[] view = new GreenAnchorViewWithInsets[1];
347             Activity activity = awaitActivityStart(scenario, a -> {
348                 FrameLayout parentLayout = a.getParentLayout();
349                 GreenAnchorViewWithInsets anchorView = new GreenAnchorViewWithInsets(a,
350                         new Rect(0, 10, 0, 0));
351                 parentLayout.addView(anchorView,
352                         new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP));
353 
354                 view[0] = anchorView;
355             });
356 
357             view[0].waitForDrawn();
358             // Do not include system insets because the child SC is not laid out in the system
359             // insets
360             validateScreenshot(mName, activity,
361                     new BitmapPixelChecker(Color.GREEN, new Rect(0, 10, 100, 100)),
362                     9000 /* expectedMatchingPixels */, Insets.NONE);
363         }
364     }
365 
366     private static class ScvhSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
367         CountDownLatch mReadyLatch = new CountDownLatch(1);
368         SurfaceControlViewHost mScvh;
369         final View mView;
370 
ScvhSurfaceView(Context context, View view)371         ScvhSurfaceView(Context context, View view) {
372             super(context);
373             getHolder().addCallback(this);
374             mView = view;
375         }
376 
377         @Override
surfaceCreated(@onNull SurfaceHolder surfaceHolder)378         public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
379             mView.getViewTreeObserver().addOnWindowAttachListener(
380                     new ViewTreeObserver.OnWindowAttachListener() {
381                         @Override
382                         public void onWindowAttached() {
383                             mReadyLatch.countDown();
384                         }
385 
386                         @Override
387                         public void onWindowDetached() {
388                         }
389                     });
390             mScvh = new SurfaceControlViewHost(getContext(), getDisplay(), getHostToken());
391             mScvh.setView(mView, getWidth(), getHeight());
392             setChildSurfacePackage(mScvh.getSurfacePackage());
393         }
394 
395         @Override
surfaceChanged(@onNull SurfaceHolder surfaceHolder, int i, int i1, int i2)396         public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {
397 
398         }
399 
400         @Override
surfaceDestroyed(@onNull SurfaceHolder surfaceHolder)401         public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
402 
403         }
404 
waitForReady()405         public void waitForReady() throws InterruptedException {
406             assertTrue("Failed to wait for ScvhSurfaceView to get added",
407                     mReadyLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
408         }
409     }
410 
411     @Test
412     @RequiresFlagsEnabled(Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER)
testGetHostToken()413     public void testGetHostToken() throws Throwable {
414         try (ActivityScenario<TestActivity> scenario = launch(TestActivity.class)) {
415             final ScvhSurfaceView[] scvhSurfaceView = new ScvhSurfaceView[1];
416             final View[] view = new View[1];
417             Activity activity = awaitActivityStart(scenario, a -> {
418                 view[0] = new View(a);
419                 FrameLayout parentLayout = a.getParentLayout();
420                 scvhSurfaceView[0] = new ScvhSurfaceView(a, view[0]);
421                 parentLayout.addView(scvhSurfaceView[0]);
422             });
423 
424             final AttachedSurfaceControl attachedSurfaceControl =
425                     scvhSurfaceView[0].getRootSurfaceControl();
426             assertThat(attachedSurfaceControl.getInputTransferToken())
427                     .isNotEqualTo(null);
428         }
429     }
430 
431     /**
432      * Ensure the synced transaction is applied even if the view isn't visible and won't draw a
433      * frame.
434      */
435     @Test
testSyncTransactionViewNotVisible()436     public void testSyncTransactionViewNotVisible() throws Throwable {
437         try (ActivityScenario<TestActivity> scenario = launch(TestActivity.class)) {
438             final ScvhSurfaceView[] scvhSurfaceView = new ScvhSurfaceView[1];
439             final View[] view = new View[1];
440             Activity activity = awaitActivityStart(scenario, a -> {
441                 view[0] = new View(a);
442                 FrameLayout parentLayout = a.getParentLayout();
443                 scvhSurfaceView[0] = new ScvhSurfaceView(a, view[0]);
444                 parentLayout.addView(scvhSurfaceView[0],
445                         new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP));
446             });
447 
448             scvhSurfaceView[0].waitForReady();
449 
450             CountDownLatch committedLatch = new CountDownLatch(1);
451             activity.runOnUiThread(() -> {
452                 view[0].setVisibility(View.INVISIBLE);
453                 SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
454                 transaction.addTransactionCommittedListener(Runnable::run,
455                         committedLatch::countDown);
456                 view[0].getRootSurfaceControl().applyTransactionOnDraw(transaction);
457             });
458 
459             assertTrue("Failed to receive transaction committed callback for scvh with no view",
460                     committedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
461         }
462     }
463 
464     /**
465      * Ensure the synced transaction is applied even if there was nothing new to draw
466      */
467     @Test
testSyncTransactionNothingToDraw()468     public void testSyncTransactionNothingToDraw() throws Throwable {
469         try (ActivityScenario<TestActivity> scenario = launch(TestActivity.class)) {
470             final View[] view = new View[1];
471             Activity activity = awaitActivityStart(scenario, a -> {
472                 view[0] = new View(a);
473                 FrameLayout parentLayout = a.getParentLayout();
474                 parentLayout.addView(view[0],
475                         new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP));
476             });
477 
478             CountDownLatch committedLatch = new CountDownLatch(1);
479             activity.runOnUiThread(() -> {
480                 SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
481                 transaction.addTransactionCommittedListener(Runnable::run,
482                         committedLatch::countDown);
483                 view[0].getRootSurfaceControl().applyTransactionOnDraw(transaction);
484                 view[0].requestLayout();
485             });
486 
487             assertTrue("Failed to receive transaction committed callback for scvh with no view",
488                     committedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
489         }
490     }
491 
492     /**
493      * Tests the jank classification API.
494      */
495     @Test
496     @RequiresFlagsEnabled(Flags.FLAG_JANK_API)
testRegisterOnJankDataListener()497     public void testRegisterOnJankDataListener() throws Throwable {
498         try (ActivityScenario<TestActivity> scenario = launch(TestActivity.class)) {
499             TestActivity activity = awaitActivityStart(scenario);
500 
501             final AttachedSurfaceControl asc = activity.getWindow().getRootSurfaceControl();
502             final CountDownLatch animEnd = new CountDownLatch(1);
503             final CountDownLatch dataReceived = new CountDownLatch(1);
504             final int[] jankCount = new int[] { 0 };
505 
506             SurfaceControl.OnJankDataListenerRegistration listenerRegistration =
507                     asc.registerOnJankDataListener(activity.getMainExecutor(), data -> {
508                         for (SurfaceControl.JankData frame : data) {
509                             assertWithMessage("durations should be positive")
510                                     .that(frame.getScheduledAppFrameTimeNanos())
511                                     .isGreaterThan(0);
512                             assertWithMessage("durations should be positive")
513                                     .that(frame.getActualAppFrameTimeNanos())
514                                     .isGreaterThan(0);
515                             if ((frame.getJankType() & JANK_APPLICATION) != 0) {
516                                 assertWithMessage("missed frame timeline mismatch")
517                                         .that(frame.getActualAppFrameTimeNanos())
518                                         .isGreaterThan(frame.getScheduledAppFrameTimeNanos());
519                                 jankCount[0]++;
520                             }
521                         }
522                         dataReceived.countDown();
523                     });
524 
525             activity.runOnUiThread(() -> activity.startJankyAnimation(animEnd));
526 
527             assertWithMessage("Failed to wait for animation to end")
528                     .that(animEnd.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
529                     .isTrue();
530 
531             listenerRegistration.flush();
532 
533             assertWithMessage("Failed to receive any jank data callbacks")
534                     .that(dataReceived.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
535                     .isTrue();
536 
537             listenerRegistration.removeAfter(0);
538 
539             assertWithMessage("No frames were marked as janky")
540                     .that(jankCount[0])
541                     .isGreaterThan(0);
542         }
543     }
544 
awaitActivityStart(ActivityScenario<A> scenario)545     private static <A extends Activity> A awaitActivityStart(ActivityScenario<A> scenario)
546             throws InterruptedException {
547         return awaitActivityStart(scenario, null);
548     }
549 
awaitActivityStart(ActivityScenario<A> scenario, ActivityScenario.ActivityAction<A> action)550     private static <A extends Activity> A awaitActivityStart(ActivityScenario<A> scenario,
551             ActivityScenario.ActivityAction<A> action) throws InterruptedException {
552         CountDownLatch activityReady = new CountDownLatch(1);
553         Activity[] activity = new Activity[1];
554         scenario.onActivity(a -> {
555             activity[0] = a;
556             activityReady.countDown();
557         });
558 
559         assertWithMessage("Failed to wait for activity to start")
560                 .that(activityReady.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
561                 .isTrue();
562 
563         waitForWindowOnTop(activity[0].getWindow());
564 
565         if (action != null) {
566             scenario.onActivity(action);
567         }
568 
569         return (A) activity[0];
570     }
571 }
572