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.server.wm;
18 
19 import static android.view.Display.DEFAULT_DISPLAY;
20 
21 import static junit.framework.Assert.assertTrue;
22 
23 import android.Manifest;
24 import android.app.Instrumentation;
25 import android.app.UiAutomation;
26 import android.graphics.Point;
27 import android.graphics.Rect;
28 import android.graphics.RectF;
29 import android.os.IBinder;
30 import android.os.SystemClock;
31 import android.os.SystemProperties;
32 import android.util.Log;
33 import android.view.View;
34 import android.view.ViewTreeObserver;
35 import android.view.Window;
36 import android.window.WindowInfosListenerForTest;
37 import android.window.WindowInfosListenerForTest.WindowInfo;
38 
39 import androidx.annotation.NonNull;
40 import androidx.annotation.Nullable;
41 import androidx.test.platform.app.InstrumentationRegistry;
42 
43 import com.android.compatibility.common.util.CtsTouchUtils;
44 import com.android.compatibility.common.util.PollingCheck;
45 import com.android.compatibility.common.util.SystemUtil;
46 import com.android.compatibility.common.util.ThrowingRunnable;
47 
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Set;
52 import java.util.Timer;
53 import java.util.TimerTask;
54 import java.util.concurrent.CountDownLatch;
55 import java.util.concurrent.TimeUnit;
56 import java.util.concurrent.atomic.AtomicBoolean;
57 import java.util.function.Consumer;
58 import java.util.function.Predicate;
59 import java.util.function.Supplier;
60 
61 public class CtsWindowInfoUtils {
62     private static final int HW_TIMEOUT_MULTIPLIER = SystemProperties.getInt(
63             "ro.hw_timeout_multiplier", 1);
64 
65     /**
66      * Calls the provided predicate each time window information changes.
67      *
68      * <p>
69      * <strong>Note:</strong>The caller must have
70      * android.permission.ACCESS_SURFACE_FLINGER permissions.
71      * </p>
72      *
73      * @param predicate The predicate tested each time window infos change.
74      * @param timeout   The amount of time to wait for the predicate to be satisfied.
75      * @param unit      The units associated with timeout.
76      * @param uiAutomation Pass in a uiAutomation to use. If null is passed in, the default will
77      *                     be used. Passing non null is only needed if the test has a custom version
78      *                     of uiAutomtation since retrieving a uiAutomation could overwrite it.
79      * @return True if the provided predicate is true for any invocation before
80      * the timeout is reached. False otherwise.
81      */
waitForWindowInfos(@onNull Predicate<List<WindowInfo>> predicate, long timeout, @NonNull TimeUnit unit, @Nullable UiAutomation uiAutomation)82     public static boolean waitForWindowInfos(@NonNull Predicate<List<WindowInfo>> predicate,
83             long timeout, @NonNull TimeUnit unit, @Nullable UiAutomation uiAutomation)
84             throws InterruptedException {
85         var latch = new CountDownLatch(1);
86         var satisfied = new AtomicBoolean();
87 
88         Consumer<List<WindowInfo>> checkPredicate = windowInfos -> {
89             if (satisfied.get()) {
90                 return;
91             }
92             if (predicate.test(windowInfos)) {
93                 satisfied.set(true);
94                 latch.countDown();
95             }
96         };
97 
98         var waitForWindow = new ThrowingRunnable() {
99             @Override
100             public void run() throws InterruptedException {
101                 var listener = new WindowInfosListenerForTest();
102                 try {
103                     listener.addWindowInfosListener(checkPredicate);
104                     latch.await(timeout, unit);
105                 } finally {
106                     listener.removeWindowInfosListener(checkPredicate);
107                 }
108             }
109         };
110 
111         if (uiAutomation == null) {
112             uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
113         }
114         Set<String> shellPermissions = uiAutomation.getAdoptedShellPermissions();
115         if (shellPermissions.isEmpty()) {
116             SystemUtil.runWithShellPermissionIdentity(uiAutomation, waitForWindow,
117                     Manifest.permission.ACCESS_SURFACE_FLINGER);
118         } else if (shellPermissions.contains(Manifest.permission.ACCESS_SURFACE_FLINGER)) {
119             waitForWindow.run();
120         } else {
121             throw new IllegalStateException(
122                     "waitForWindowOnTop called with adopted shell permissions that don't include "
123                             + "ACCESS_SURFACE_FLINGER");
124         }
125 
126         return satisfied.get();
127     }
128 
129     /**
130      * Same as {@link #waitForWindowInfos(Predicate, long, TimeUnit, UiAutomation)}, but passes in
131      * a null uiAutomation object. This should be used in most cases unless there's a custom
132      * uiAutomation object used in the test.
133      *
134      * @param predicate The predicate tested each time window infos change.
135      * @param timeout   The amount of time to wait for the predicate to be satisfied.
136      * @param unit      The units associated with timeout.
137      * @return True if the provided predicate is true for any invocation before
138      * the timeout is reached. False otherwise.
139      */
waitForWindowInfos(@onNull Predicate<List<WindowInfo>> predicate, long timeout, @NonNull TimeUnit unit)140     public static boolean waitForWindowInfos(@NonNull Predicate<List<WindowInfo>> predicate,
141             long timeout, @NonNull TimeUnit unit) throws InterruptedException {
142         return waitForWindowInfos(predicate, timeout, unit, null /* uiAutomation */);
143     }
144 
145     /**
146      * Calls the provided predicate each time window information changes if a visible
147      * window is found that matches the supplied window token.
148      *
149      * <p>
150      * <strong>Note:</strong>The caller must have the
151      * android.permission.ACCESS_SURFACE_FLINGER permissions.
152      * </p>
153      *
154      * @param predicate           The predicate tested each time window infos change.
155      * @param timeout             The amount of time to wait for the predicate to be satisfied.
156      * @param unit                The units associated with timeout.
157      * @param windowTokenSupplier Supplies the window token for the window to
158      *                            call the predicate on. The supplier is called each time window
159      *                            info change. If the supplier returns null, the predicate is
160      *                            assumed false for the current invocation.
161      * @param displayId           The id of the display on which to wait for the window of interest
162      * @return True if the provided predicate is true for any invocation before the timeout is
163      * reached. False otherwise.
164      * @hide
165      */
waitForWindowInfo(@onNull Predicate<WindowInfo> predicate, long timeout, @NonNull TimeUnit unit, @NonNull Supplier<IBinder> windowTokenSupplier, int displayId)166     public static boolean waitForWindowInfo(@NonNull Predicate<WindowInfo> predicate, long timeout,
167             @NonNull TimeUnit unit, @NonNull Supplier<IBinder> windowTokenSupplier, int displayId)
168             throws InterruptedException {
169         Predicate<List<WindowInfo>> wrappedPredicate = windowInfos -> {
170             IBinder windowToken = windowTokenSupplier.get();
171             if (windowToken == null) {
172                 return false;
173             }
174 
175             for (var windowInfo : windowInfos) {
176                 if (!windowInfo.isVisible) {
177                     continue;
178                 }
179                 // only wait for requested display.
180                 if (windowInfo.windowToken == windowToken
181                         && windowInfo.displayId == displayId) {
182                     return predicate.test(windowInfo);
183                 }
184             }
185 
186             return false;
187         };
188         return waitForWindowInfos(wrappedPredicate, timeout, unit);
189     }
190 
191     /**
192      * Waits for the window associated with the view to be present.
193      */
waitForWindowVisible(@onNull View view)194     public static boolean waitForWindowVisible(@NonNull View view) throws InterruptedException {
195         // Wait until view is attached to a display
196         PollingCheck.waitFor(() -> view.getDisplay() != null, "View not attached to a display");
197         return waitForWindowInfo(windowInfo -> true, HW_TIMEOUT_MULTIPLIER * 5L, TimeUnit.SECONDS,
198                 view::getWindowToken, view.getDisplay().getDisplayId());
199     }
200 
waitForWindowVisible(@onNull IBinder windowToken)201     public static boolean waitForWindowVisible(@NonNull IBinder windowToken)
202             throws InterruptedException {
203         return waitForWindowInfo(windowInfo -> true, HW_TIMEOUT_MULTIPLIER * 5L, TimeUnit.SECONDS,
204                 () -> windowToken, DEFAULT_DISPLAY);
205     }
206 
207     /**
208      * Calls {@link CtsWindowInfoUtils#waitForWindowOnTop(int, TimeUnit, Supplier)}. Adopts
209      * required permissions and waits at least five seconds before timing out.
210      *
211      * @param window The window to wait on.
212      * @return True if the window satisfies the visibility requirements before the timeout is
213      * reached. False otherwise.
214      */
waitForWindowOnTop(@onNull Window window)215     public static boolean waitForWindowOnTop(@NonNull Window window) throws InterruptedException {
216         return waitForWindowOnTop(HW_TIMEOUT_MULTIPLIER * 5, TimeUnit.SECONDS,
217                 () -> window.getDecorView().getWindowToken());
218     }
219 
220     /**
221      * Waits until the window specified by the predicate is present, not occluded, and hasn't
222      * had geometry changes for 200ms.
223      *
224      * The window is considered occluded if any part of another window is above it, excluding
225      * trusted overlays.
226      *
227      * <p>
228      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
229      * android.permission.ACCESS_SURFACE_FLINGER.
230      * </p>
231      *
232      * @param timeout             The amount of time to wait for the window to be visible.
233      * @param unit                The units associated with timeout.
234      * @param windowTokenSupplier Supplies the window token for the window to wait on. The
235      *                            supplier is called each time window infos change. If the
236      *                            supplier returns null, the window is assumed not visible
237      *                            yet.
238      * @return True if the window satisfies the visibility requirements before the timeout is
239      * reached. False otherwise.
240      */
waitForWindowOnTop(long timeout, @NonNull TimeUnit unit, @NonNull Predicate<WindowInfo> predicate)241     public static boolean waitForWindowOnTop(long timeout, @NonNull TimeUnit unit,
242                                              @NonNull Predicate<WindowInfo> predicate)
243             throws InterruptedException {
244         var latch = new CountDownLatch(1);
245         var satisfied = new AtomicBoolean();
246 
247         var windowNotOccluded = new Consumer<List<WindowInfo>>() {
248             private Timer mTimer = new Timer();
249             private TimerTask mTask = null;
250             private Rect mPreviousBounds = new Rect(0, 0, -1, -1);
251 
252             private void resetState() {
253                 if (mTask != null) {
254                     mTask.cancel();
255                     mTask = null;
256                 }
257                 mPreviousBounds.set(0, 0, -1, -1);
258             }
259 
260             @Override
261             public void accept(List<WindowInfo> windowInfos) {
262                 if (satisfied.get()) {
263                     return;
264                 }
265 
266                 WindowInfo targetWindowInfo = null;
267                 ArrayList<WindowInfo> aboveWindowInfos = new ArrayList<>();
268                 for (var windowInfo : windowInfos) {
269                     if (predicate.test(windowInfo)) {
270                         targetWindowInfo = windowInfo;
271                         break;
272                     }
273                     if (windowInfo.isTrustedOverlay || !windowInfo.isVisible) {
274                         continue;
275                     }
276                     aboveWindowInfos.add(windowInfo);
277                 }
278 
279                 if (targetWindowInfo == null) {
280                     // The window isn't present. If we have an active timer, we need to cancel it
281                     // as it's possible the window was previously present and has since disappeared.
282                     resetState();
283                     return;
284                 }
285 
286                 for (var windowInfo : aboveWindowInfos) {
287                     if (targetWindowInfo.displayId == windowInfo.displayId
288                             && Rect.intersects(targetWindowInfo.bounds, windowInfo.bounds)) {
289                         // The window is occluded. If we have an active timer, we need to cancel it
290                         // as it's possible the window was previously not occluded and now is
291                         // occluded.
292                         resetState();
293                         return;
294                     }
295                 }
296 
297                 if (targetWindowInfo.bounds.equals(mPreviousBounds)) {
298                     // The window matches previously found bounds. Let the active timer continue.
299                     return;
300                 }
301 
302                 // The window is present and not occluded but has different bounds than
303                 // previously seen or this is the first time we've detected the window. If
304                 // there's an active timer, cancel it. Schedule a task to toggle the latch in 200ms.
305                 resetState();
306                 mPreviousBounds.set(targetWindowInfo.bounds);
307                 mTask = new TimerTask() {
308                     @Override
309                     public void run() {
310                         satisfied.set(true);
311                         latch.countDown();
312                     }
313                 };
314                 mTimer.schedule(mTask, 200 * HW_TIMEOUT_MULTIPLIER);
315             }
316         };
317 
318         runWithSurfaceFlingerPermission(() -> {
319             var listener = new WindowInfosListenerForTest();
320             try {
321                 listener.addWindowInfosListener(windowNotOccluded);
322                 latch.await(timeout, unit);
323             } finally {
324                 listener.removeWindowInfosListener(windowNotOccluded);
325             }
326         });
327 
328         return satisfied.get();
329     }
330 
331     private interface InterruptableRunnable {
run()332         void run() throws InterruptedException;
333     };
334 
runWithSurfaceFlingerPermission(@onNull InterruptableRunnable runnable)335     private static void runWithSurfaceFlingerPermission(@NonNull InterruptableRunnable runnable)
336             throws InterruptedException {
337         Set<String> shellPermissions =
338                 InstrumentationRegistry.getInstrumentation().getUiAutomation()
339                         .getAdoptedShellPermissions();
340         if (shellPermissions.isEmpty()) {
341             SystemUtil.runWithShellPermissionIdentity(runnable::run,
342                     Manifest.permission.ACCESS_SURFACE_FLINGER);
343         } else if (shellPermissions.contains(Manifest.permission.ACCESS_SURFACE_FLINGER)) {
344             runnable.run();
345         } else {
346             throw new IllegalStateException(
347                     "waitForWindowOnTop called with adopted shell permissions that don't include "
348                             + "ACCESS_SURFACE_FLINGER");
349         }
350     }
351 
352     /**
353      * Waits until the window specified by windowTokenSupplier is present, not occluded, and hasn't
354      * had geometry changes for 200ms.
355      *
356      * The window is considered occluded if any part of another window is above it, excluding
357      * trusted overlays.
358      *
359      * <p>
360      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
361      * android.permission.ACCESS_SURFACE_FLINGER.
362      * </p>
363      *
364      * @param timeout             The amount of time to wait for the window to be visible.
365      * @param unit                The units associated with timeout.
366      * @param windowTokenSupplier Supplies the window token for the window to wait on. The
367      *                            supplier is called each time window infos change. If the
368      *                            supplier returns null, the window is assumed not visible
369      *                            yet.
370      * @return True if the window satisfies the visibility requirements before the timeout is
371      * reached. False otherwise.
372      */
waitForWindowOnTop(long timeout, @NonNull TimeUnit unit, @NonNull Supplier<IBinder> windowTokenSupplier)373     public static boolean waitForWindowOnTop(long timeout, @NonNull TimeUnit unit,
374             @NonNull Supplier<IBinder> windowTokenSupplier)
375             throws InterruptedException {
376         return waitForWindowOnTop(timeout, unit, windowInfo -> {
377             IBinder windowToken = windowTokenSupplier.get();
378             return windowToken != null && windowInfo.windowToken == windowToken;
379         });
380     }
381 
382     /**
383      * Waits until the set of windows and their geometries are unchanged for 200ms.
384      *
385      * <p>
386      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
387      * android.permission.ACCESS_SURFACE_FLINGER.
388      * </p>
389      *
390      * @param timeout The amount of time to wait for the window to be visible.
391      * @param unit    The units associated with timeout.
392      * @return True if window geometry becomes stable before the timeout is reached. False
393      * otherwise.
394      */
waitForStableWindowGeometry(long timeout, @NonNull TimeUnit unit)395     public static boolean waitForStableWindowGeometry(long timeout, @NonNull TimeUnit unit)
396             throws InterruptedException {
397         var latch = new CountDownLatch(1);
398         var satisfied = new AtomicBoolean();
399 
400         var timer = new Timer();
401         TimerTask[] task = {null};
402 
403         var previousBounds = new HashMap<IBinder, Rect>();
404         var currentBounds = new HashMap<IBinder, Rect>();
405 
406         Consumer<List<WindowInfo>> consumer = windowInfos -> {
407             if (satisfied.get()) {
408                 return;
409             }
410 
411             currentBounds.clear();
412             for (var windowInfo : windowInfos) {
413                 currentBounds.put(windowInfo.windowToken, windowInfo.bounds);
414             }
415 
416             if (currentBounds.equals(previousBounds)) {
417                 // No changes detected. Let the previously scheduled timer task continue.
418                 return;
419             }
420 
421             previousBounds.clear();
422             previousBounds.putAll(currentBounds);
423 
424             // Something has changed. Cancel the previous timer task and schedule a new task
425             // to countdown the latch in 200ms.
426             if (task[0] != null) {
427                 task[0].cancel();
428             }
429             task[0] = new TimerTask() {
430                 @Override
431                 public void run() {
432                     satisfied.set(true);
433                     latch.countDown();
434                 }
435             };
436             timer.schedule(task[0], 200 * HW_TIMEOUT_MULTIPLIER);
437         };
438 
439         runWithSurfaceFlingerPermission(() -> {
440             var listener = new WindowInfosListenerForTest();
441             try {
442                 listener.addWindowInfosListener(consumer);
443                 latch.await(timeout, unit);
444             } finally {
445                 listener.removeWindowInfosListener(consumer);
446             }
447         });
448 
449         return satisfied.get();
450     }
451 
452     /**
453      * Tap on the center coordinates of the specified window and sends back the coordinates tapped
454      * </p>
455      *
456      * @param instrumentation     Instrumentation object to use for tap.
457      * @param windowTokenSupplier Supplies the window token for the window to wait on. The supplier
458      *                            is called each time window infos change. If the supplier returns
459      *                            null, the window is assumed not visible yet.
460      * @param outCoords           If non null, the tapped coordinates will be set in the object.
461      * @return true if successfully tapped on the coordinates, false otherwise.
462      * @throws InterruptedException if failed to wait for WindowInfo
463      */
tapOnWindowCenter(Instrumentation instrumentation, @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point outCoords)464     public static boolean tapOnWindowCenter(Instrumentation instrumentation,
465             @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point outCoords)
466             throws InterruptedException {
467         Rect bounds = getWindowBoundsInDisplaySpace(windowTokenSupplier);
468         if (bounds == null) {
469             return false;
470         }
471 
472         final Point coord = new Point(bounds.left + bounds.width() / 2,
473                 bounds.top + bounds.height() / 2);
474         sendTap(instrumentation, coord);
475         if (outCoords != null) {
476             outCoords.set(coord.x, coord.y);
477         }
478         return true;
479     }
480 
481     /**
482      * Tap on the coordinates of the specified window, offset by the value passed in.
483      * </p>
484      *
485      * @param instrumentation     Instrumentation object to use for tap.
486      * @param windowTokenSupplier Supplies the window token for the window to wait on. The supplier
487      *                            is called each time window infos change. If the supplier returns
488      *                            null, the window is assumed not visible yet.
489      * @param offset              The offset from 0,0 of the window to tap on. If null, it will be
490      *                            ignored and 0,0 will be tapped.
491      * @return true if successfully tapped on the coordinates, false otherwise.
492      * @throws InterruptedException if failed to wait for WindowInfo
493      */
tapOnWindow(Instrumentation instrumentation, @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point offset)494     public static boolean tapOnWindow(Instrumentation instrumentation,
495             @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point offset)
496             throws InterruptedException {
497         Rect bounds = getWindowBoundsInDisplaySpace(windowTokenSupplier);
498         if (bounds == null) {
499             return false;
500         }
501 
502         final Point coord = new Point(bounds.left + (offset != null ? offset.x : 0),
503                 bounds.top + (offset != null ? offset.y : 0));
504         sendTap(instrumentation, coord);
505         return true;
506     }
507 
getWindowBoundsInWindowSpace(@onNull Supplier<IBinder> windowTokenSupplier)508     public static Rect getWindowBoundsInWindowSpace(@NonNull Supplier<IBinder> windowTokenSupplier)
509             throws InterruptedException {
510         Rect bounds = new Rect();
511         Predicate<WindowInfo> predicate = windowInfo -> {
512             if (!windowInfo.bounds.isEmpty()) {
513                 if (!windowInfo.transform.isIdentity()) {
514                     RectF rectF = new RectF(windowInfo.bounds);
515                     windowInfo.transform.mapRect(rectF);
516                     bounds.set((int) rectF.left, (int) rectF.top, (int) rectF.right,
517                             (int) rectF.bottom);
518                 } else {
519                     bounds.set(windowInfo.bounds);
520                 }
521                 return true;
522             }
523 
524             return false;
525         };
526 
527         if (!waitForWindowInfo(predicate, 5L * HW_TIMEOUT_MULTIPLIER, TimeUnit.SECONDS,
528                 windowTokenSupplier, DEFAULT_DISPLAY)) {
529             return null;
530         }
531         return bounds;
532     }
533 
getWindowBoundsInDisplaySpace(@onNull Supplier<IBinder> windowTokenSupplier)534     public static Rect getWindowBoundsInDisplaySpace(@NonNull Supplier<IBinder> windowTokenSupplier)
535             throws InterruptedException {
536         Rect bounds = new Rect();
537         Predicate<WindowInfo> predicate = windowInfo -> {
538             if (!windowInfo.bounds.isEmpty()) {
539                 bounds.set(windowInfo.bounds);
540                 return true;
541             }
542 
543             return false;
544         };
545 
546         if (!waitForWindowInfo(predicate, 5L * HW_TIMEOUT_MULTIPLIER, TimeUnit.SECONDS,
547                 windowTokenSupplier, DEFAULT_DISPLAY)) {
548             return null;
549         }
550         return bounds;
551     }
552 
553     /**
554      * Get the center coordinates of the specified window
555      *
556      * @param windowTokenSupplier Supplies the window token for the window to wait on. The supplier
557      *                            is called each time window infos change. If the supplier returns
558      *                            null, the window is assumed not visible yet.
559      * @return Point of the window center
560      * @throws InterruptedException if failed to wait for WindowInfo
561      */
getWindowCenter(@onNull Supplier<IBinder> windowTokenSupplier)562     public static Point getWindowCenter(@NonNull Supplier<IBinder> windowTokenSupplier)
563             throws InterruptedException {
564         final Rect bounds = getWindowBoundsInDisplaySpace(windowTokenSupplier);
565         if (bounds == null) {
566             throw new IllegalArgumentException("Could not get the bounds for window");
567         }
568         return new Point(bounds.left + bounds.width() / 2, bounds.top + bounds.height() / 2);
569     }
570 
571     /**
572      * Sends tap to the specified coordinates.
573      * </p>
574      *
575      * @param instrumentation    Instrumentation object to use for tap.
576      * @param coord              The coordinates to tap on in display space.
577      * @throws InterruptedException if failed to wait for WindowInfo
578      */
sendTap(Instrumentation instrumentation, Point coord)579     public static void sendTap(Instrumentation instrumentation, Point coord) {
580         // Get anchor coordinates on the screen
581         final long downTime = SystemClock.uptimeMillis();
582 
583         CtsTouchUtils ctsTouchUtils = new CtsTouchUtils(instrumentation.getTargetContext());
584         ctsTouchUtils.injectDownEvent(instrumentation, downTime, coord.x, coord.y,
585                 /* eventInjectionListener= */ null);
586         ctsTouchUtils.injectUpEvent(instrumentation, downTime, false, coord.x, coord.y, null);
587 
588         instrumentation.waitForIdleSync();
589     }
590 
waitForWindowFocus(final View view, boolean hasWindowFocus)591     public static boolean waitForWindowFocus(final View view, boolean hasWindowFocus) {
592         final CountDownLatch latch = new CountDownLatch(1);
593 
594         view.getHandler().post(() -> {
595             if (view.hasWindowFocus() == hasWindowFocus) {
596                 latch.countDown();
597                 return;
598             }
599             view.getViewTreeObserver().addOnWindowFocusChangeListener(
600                     new ViewTreeObserver.OnWindowFocusChangeListener() {
601                         @Override
602                         public void onWindowFocusChanged(boolean newFocusState) {
603                             if (hasWindowFocus == newFocusState) {
604                                 view.getViewTreeObserver()
605                                         .removeOnWindowFocusChangeListener(this);
606                                 latch.countDown();
607                             }
608                         }
609                     });
610 
611             view.invalidate();
612         });
613 
614         try {
615             if (!latch.await(HW_TIMEOUT_MULTIPLIER * 10L, TimeUnit.SECONDS)) {
616                 return false;
617             }
618         } catch (InterruptedException e) {
619             return false;
620         }
621         return true;
622     }
623 
dumpWindowsOnScreen(String tag, String message)624     public static void dumpWindowsOnScreen(String tag, String message)
625             throws InterruptedException {
626         waitForWindowInfos(windowInfos -> {
627             if (windowInfos.isEmpty()) {
628                 return false;
629             }
630             Log.d(tag, "Dumping windows on screen: " + message);
631             for (var windowInfo : windowInfos) {
632                 Log.d(tag, "     " + windowInfo);
633             }
634             return true;
635         }, 5L * HW_TIMEOUT_MULTIPLIER, TimeUnit.SECONDS);
636     }
637 
638     /**
639      * Assert the condition and dump the window states if the condition fails.
640      */
assertAndDumpWindowState(String tag, String message, boolean condition)641     public static void assertAndDumpWindowState(String tag, String message, boolean condition)
642             throws InterruptedException {
643         if (!condition) {
644             dumpWindowsOnScreen(tag, message);
645         }
646 
647         assertTrue(message, condition);
648     }
649 }
650