xref: /aosp_15_r20/cts/tests/framework/base/windowmanager/util/src/android/server/wm/CtsWindowInfoUtils.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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.util.Pair;
34 import android.view.SurfaceView;
35 import android.view.View;
36 import android.view.ViewTreeObserver;
37 import android.view.Window;
38 import android.window.WindowInfosListenerForTest;
39 import android.window.WindowInfosListenerForTest.DisplayInfo;
40 import android.window.WindowInfosListenerForTest.WindowInfo;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 import androidx.test.platform.app.InstrumentationRegistry;
45 
46 import com.android.compatibility.common.util.CtsTouchUtils;
47 import com.android.compatibility.common.util.PollingCheck;
48 import com.android.compatibility.common.util.SystemUtil;
49 import com.android.compatibility.common.util.ThrowingRunnable;
50 
51 import java.time.Duration;
52 import java.util.ArrayList;
53 import java.util.HashMap;
54 import java.util.List;
55 import java.util.Set;
56 import java.util.Timer;
57 import java.util.TimerTask;
58 import java.util.concurrent.CountDownLatch;
59 import java.util.concurrent.TimeUnit;
60 import java.util.concurrent.atomic.AtomicBoolean;
61 import java.util.function.BiConsumer;
62 import java.util.function.Predicate;
63 import java.util.function.Supplier;
64 
65 public class CtsWindowInfoUtils {
66     private static final int HW_TIMEOUT_MULTIPLIER = SystemProperties.getInt(
67             "ro.hw_timeout_multiplier", 1);
68 
69     /**
70      * Calls the provided predicate each time window information changes.
71      *
72      * <p>
73      * <strong>Note:</strong>The caller must have
74      * android.permission.ACCESS_SURFACE_FLINGER permissions.
75      * </p>
76      *
77      * @param predicate The predicate tested each time window infos change.
78      * @param timeout   The amount of time to wait for the predicate to be satisfied.
79      * @param uiAutomation Pass in a uiAutomation to use. If null is passed in, the default will
80      *                     be used. Passing non null is only needed if the test has a custom version
81      *                     of uiAutomation since retrieving a uiAutomation could overwrite it.
82      * @return True if the provided predicate is true for any invocation before
83      * the timeout is reached. False otherwise.
84      */
waitForWindowInfos(@onNull Predicate<List<WindowInfo>> predicate, @NonNull Duration timeout, @Nullable UiAutomation uiAutomation)85     public static boolean waitForWindowInfos(@NonNull Predicate<List<WindowInfo>> predicate,
86             @NonNull Duration timeout, @Nullable UiAutomation uiAutomation)
87             throws InterruptedException {
88         var latch = new CountDownLatch(1);
89         var satisfied = new AtomicBoolean();
90 
91         BiConsumer<List<WindowInfo>, List<DisplayInfo>> checkPredicate =
92                 (windowInfos, displayInfos) -> {
93                     if (satisfied.get()) {
94                         return;
95                     }
96                     if (predicate.test(windowInfos)) {
97                         satisfied.set(true);
98                         latch.countDown();
99                     }
100                 };
101 
102         var waitForWindow = new ThrowingRunnable() {
103             @Override
104             public void run() throws InterruptedException {
105                 var listener = new WindowInfosListenerForTest();
106                 try {
107                     listener.addWindowInfosListener(checkPredicate);
108                     latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
109                 } finally {
110                     listener.removeWindowInfosListener(checkPredicate);
111                 }
112             }
113         };
114 
115         if (uiAutomation == null) {
116             uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
117         }
118         Set<String> shellPermissions = uiAutomation.getAdoptedShellPermissions();
119         if (shellPermissions.isEmpty()) {
120             SystemUtil.runWithShellPermissionIdentity(uiAutomation, waitForWindow,
121                     Manifest.permission.ACCESS_SURFACE_FLINGER);
122         } else if (shellPermissions.contains(Manifest.permission.ACCESS_SURFACE_FLINGER)) {
123             waitForWindow.run();
124         } else {
125             throw new IllegalStateException(
126                     "waitForWindowOnTop called with adopted shell permissions that don't include "
127                             + "ACCESS_SURFACE_FLINGER");
128         }
129 
130         return satisfied.get();
131     }
132 
133     /**
134      * Same as {@link #waitForWindowInfos(Predicate, Duration, UiAutomation)}, but passes in
135      * a null uiAutomation object. This should be used in most cases unless there's a custom
136      * uiAutomation object used in the test.
137      *
138      * @param predicate The predicate tested each time window infos change.
139      * @param timeout   The amount of time to wait for the predicate to be satisfied.
140      * @return True if the provided predicate is true for any invocation before
141      * the timeout is reached. False otherwise.
142      */
waitForWindowInfos(@onNull Predicate<List<WindowInfo>> predicate, @NonNull Duration timeout)143     public static boolean waitForWindowInfos(@NonNull Predicate<List<WindowInfo>> predicate,
144             @NonNull Duration timeout) throws InterruptedException {
145         return waitForWindowInfos(predicate, timeout, null /* uiAutomation */);
146     }
147 
148     /**
149      * Calls the provided predicate each time window information changes if a visible
150      * window is found that matches the supplied window token.
151      *
152      * <p>
153      * <strong>Note:</strong>The caller must have the
154      * android.permission.ACCESS_SURFACE_FLINGER permissions.
155      * </p>
156      *
157      * @param predicate           The predicate tested each time window infos change.
158      * @param timeout             The amount of time to wait for the predicate to be satisfied.
159      * @param windowTokenSupplier Supplies the window token for the window to
160      *                            call the predicate on. The supplier is called each time window
161      *                            info change. If the supplier returns null, the predicate is
162      *                            assumed false for the current invocation.
163      * @param displayId           The id of the display on which to wait for the window of interest
164      * @return True if the provided predicate is true for any invocation before the timeout is
165      * reached. False otherwise.
166      * @hide
167      */
waitForWindowInfo(@onNull Predicate<WindowInfo> predicate, @NonNull Duration timeout, @NonNull Supplier<IBinder> windowTokenSupplier, int displayId)168     public static boolean waitForWindowInfo(@NonNull Predicate<WindowInfo> predicate,
169             @NonNull Duration timeout, @NonNull Supplier<IBinder> windowTokenSupplier,
170             int displayId) throws InterruptedException {
171         Predicate<List<WindowInfo>> wrappedPredicate = windowInfos -> {
172             IBinder windowToken = windowTokenSupplier.get();
173             if (windowToken == null) {
174                 return false;
175             }
176 
177             for (var windowInfo : windowInfos) {
178                 if (!windowInfo.isVisible) {
179                     continue;
180                 }
181                 // only wait for requested display.
182                 if (windowInfo.windowToken == windowToken
183                         && windowInfo.displayId == displayId) {
184                     return predicate.test(windowInfo);
185                 }
186             }
187 
188             return false;
189         };
190         return waitForWindowInfos(wrappedPredicate, timeout);
191     }
192 
193     /**
194      * Waits for the SurfaceView to be invisible.
195      */
waitForSurfaceViewInvisible(@onNull SurfaceView view)196     public static boolean waitForSurfaceViewInvisible(@NonNull SurfaceView view)
197             throws InterruptedException {
198         Predicate<List<WindowInfo>> wrappedPredicate = windowInfos -> {
199             for (var windowInfo : windowInfos) {
200                 if (windowInfo.isVisible) {
201                     continue;
202                 }
203                 if (windowInfo.name.startsWith(getHashCode(view))) {
204                     return false;
205                 }
206             }
207 
208             return true;
209         };
210 
211         return waitForWindowInfos(wrappedPredicate, Duration.ofSeconds(HW_TIMEOUT_MULTIPLIER * 5L));
212     }
213 
214     /**
215      * Waits for the SurfaceView to be present.
216      */
waitForSurfaceViewVisible(@onNull SurfaceView view)217     public static boolean waitForSurfaceViewVisible(@NonNull SurfaceView view)
218             throws InterruptedException {
219         // Wait until view is attached to a display
220         PollingCheck.waitFor(() -> view.getDisplay() != null, "View not attached to a display");
221 
222         Predicate<List<WindowInfo>> wrappedPredicate = windowInfos -> {
223             for (var windowInfo : windowInfos) {
224                 if (!windowInfo.isVisible) {
225                     continue;
226                 }
227                 if (windowInfo.name.startsWith(getHashCode(view))
228                         && windowInfo.displayId == view.getDisplay().getDisplayId()) {
229                     return true;
230                 }
231             }
232 
233             return false;
234         };
235 
236         return waitForWindowInfos(wrappedPredicate, Duration.ofSeconds(HW_TIMEOUT_MULTIPLIER * 5L));
237     }
238 
239     /**
240      * Waits for a window to become visible.
241      *
242      * @param view The view of the window to wait for.
243      * @return {@code true} if the window becomes visible within the timeout period, {@code false}
244      *         otherwise.
245      * @throws InterruptedException If the thread is interrupted while waiting for the window
246      *         information.
247      */
waitForWindowVisible(@onNull View view)248     public static boolean waitForWindowVisible(@NonNull View view) throws InterruptedException {
249         // Wait until view is attached to a display
250         PollingCheck.waitFor(() -> view.getDisplay() != null, "View not attached to a display");
251         return waitForWindowInfo(windowInfo -> true, Duration.ofSeconds(HW_TIMEOUT_MULTIPLIER * 5L),
252                 view::getWindowToken, view.getDisplay().getDisplayId());
253     }
254 
255     /**
256      * Waits for a window to become visible.
257      *
258      * @param windowToken The token of the window to wait for.
259      * @return {@code true} if the window becomes visible within the timeout period, {@code false}
260      *         otherwise.
261      * @throws InterruptedException If the thread is interrupted while waiting for the window
262      *         information.
263      */
waitForWindowVisible(@onNull IBinder windowToken)264     public static boolean waitForWindowVisible(@NonNull IBinder windowToken)
265             throws InterruptedException {
266         return waitForWindowVisible(windowToken, DEFAULT_DISPLAY);
267     }
268 
269     /**
270      * Waits for a window to become visible.
271      *
272      * @param windowTokenSupplier Supplies the window token for the window to wait on. The
273      *                            supplier is called each time window infos change. If the
274      *                            supplier returns null, the window is assumed not visible
275      *                            yet.
276      * @return {@code true} if the window becomes visible within the timeout period, {@code false}
277      *         otherwise.
278      * @throws InterruptedException If the thread is interrupted while waiting for the window
279      *         information.
280      */
waitForWindowVisible(@onNull Supplier<IBinder> windowTokenSupplier)281     public static boolean waitForWindowVisible(@NonNull Supplier<IBinder> windowTokenSupplier)
282             throws InterruptedException {
283         return waitForWindowInfo(windowInfo -> true, Duration.ofSeconds(HW_TIMEOUT_MULTIPLIER * 5L),
284                 windowTokenSupplier, DEFAULT_DISPLAY);
285     }
286 
287     /**
288      * Waits for a window to become visible.
289      *
290      * @param windowToken The token of the window to wait for.
291      * @param displayId The ID of the display on which to check for the window's visibility.
292      * @return {@code true} if the window becomes visible within the timeout period, {@code false}
293      *         otherwise.
294      * @throws InterruptedException If the thread is interrupted while waiting for the window
295      *         information.
296      */
waitForWindowVisible(@onNull IBinder windowToken, int displayId)297     public static boolean waitForWindowVisible(@NonNull IBinder windowToken, int displayId)
298             throws InterruptedException {
299         return waitForWindowInfo(windowInfo -> true, Duration.ofSeconds(HW_TIMEOUT_MULTIPLIER * 5L),
300                 () -> windowToken, displayId);
301     }
302 
303     /**
304      * Waits for a window to become invisible.
305      *
306      * @param windowTokenSupplier Supplies the window token for the window to wait on.
307      * @param timeout The amount of time to wait for the window to be invisible.
308      * @return {@code true} if the window becomes invisible within the timeout period, {@code false}
309      *         otherwise.
310      * @throws InterruptedException If the thread is interrupted while waiting for the window
311      *         information.
312      */
waitForWindowInvisible(@onNull Supplier<IBinder> windowTokenSupplier, @NonNull Duration timeout)313     public static boolean waitForWindowInvisible(@NonNull Supplier<IBinder> windowTokenSupplier,
314                                                  @NonNull Duration timeout)
315             throws InterruptedException {
316         Predicate<List<WindowInfo>> wrappedPredicate = windowInfos -> {
317             IBinder windowToken = windowTokenSupplier.get();
318             if (windowToken == null) {
319                 return false;
320             }
321 
322             for (var windowInfo : windowInfos) {
323                 if (windowInfo.isVisible
324                         && windowInfo.windowToken == windowToken) {
325                     return false;
326                 }
327             }
328 
329             return true;
330         };
331         return waitForWindowInfos(wrappedPredicate, timeout);
332     }
333 
334     /**
335      * Calls {@link CtsWindowInfoUtils#waitForWindowOnTop(Duration, Supplier)}. Adopts
336      * required permissions and waits at least five seconds before timing out.
337      *
338      * @param window The window to wait on.
339      * @return True if the window satisfies the visibility requirements before the timeout is
340      * reached. False otherwise.
341      */
waitForWindowOnTop(@onNull Window window)342     public static boolean waitForWindowOnTop(@NonNull Window window) throws InterruptedException {
343         return waitForWindowOnTop(Duration.ofSeconds(HW_TIMEOUT_MULTIPLIER * 5L),
344                 () -> window.getDecorView().getWindowToken());
345     }
346 
347     /**
348      * Waits until the window specified by the predicate is present, not occluded, and hasn't
349      * had geometry changes for 200ms.
350      *
351      * The window is considered occluded if any part of another window is above it, excluding
352      * trusted overlays.
353      *
354      * <p>
355      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
356      * android.permission.ACCESS_SURFACE_FLINGER.
357      * </p>
358      *
359      * @param timeout             The amount of time to wait for the window to be visible.
360      * @param predicate           A predicate identifying the target window we are waiting for,
361      *                            will be tested each time window infos change.
362      * @return True if the window satisfies the visibility requirements before the timeout is
363      * reached. False otherwise.
364      */
waitForWindowOnTop(@onNull Duration timeout, @NonNull Predicate<WindowInfo> predicate)365     public static boolean waitForWindowOnTop(@NonNull Duration timeout,
366                                              @NonNull Predicate<WindowInfo> predicate)
367             throws InterruptedException {
368         return waitForNthWindowFromTop(timeout, predicate, 0);
369     }
370 
371     /**
372      * Waits until the window specified by {@code predicate} is present, at the expected level
373      * of the composition hierarchy, and hasn't had geometry changes for 200ms.
374      *
375      * The window is considered occluded if any part of another window is above it, excluding
376      * trusted overlays and bbq.
377      *
378      * <p>
379      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
380      * android.permission.ACCESS_SURFACE_FLINGER.
381      * </p>
382      *
383      * @param timeout       The amount of time to wait for the window to be visible.
384      * @param predicate     A predicate identifying the target window we are waiting, will be
385      *                      tested each time window infos change.
386      * @param expectedOrder The expected order of the surface control we are looking
387      *                      for.
388      * @return True if the window satisfies the visibility requirements before the timeout is
389      * reached. False otherwise.
390      */
waitForNthWindowFromTop(@onNull Duration timeout, @NonNull Predicate<WindowInfo> predicate, int expectedOrder)391     public static boolean waitForNthWindowFromTop(@NonNull Duration timeout,
392                                                   @NonNull Predicate<WindowInfo> predicate,
393                                                   int expectedOrder)
394             throws InterruptedException {
395         var latch = new CountDownLatch(1);
396         var satisfied = new AtomicBoolean();
397 
398         var windowNotOccluded = new BiConsumer<List<WindowInfo>, List<DisplayInfo>>() {
399             private Timer mTimer = new Timer();
400             private TimerTask mTask = null;
401             private Rect mPreviousBounds = new Rect(0, 0, -1, -1);
402 
403             private void resetState() {
404                 if (mTask != null) {
405                     mTask.cancel();
406                     mTask = null;
407                 }
408                 mPreviousBounds.set(0, 0, -1, -1);
409             }
410 
411             @Override
412             public void accept(List<WindowInfo> windowInfos, List<DisplayInfo> displayInfos) {
413                 if (satisfied.get()) {
414                     return;
415                 }
416 
417                 WindowInfo targetWindowInfo = null;
418                 ArrayList<WindowInfo> aboveWindowInfos = new ArrayList<>();
419                 for (var windowInfo : windowInfos) {
420                     if (predicate.test(windowInfo)) {
421                         targetWindowInfo = windowInfo;
422                         break;
423                     }
424                     if (windowInfo.isTrustedOverlay || !windowInfo.isVisible) {
425                         continue;
426                     }
427                     aboveWindowInfos.add(windowInfo);
428                 }
429 
430                 if (targetWindowInfo == null) {
431                     // The window isn't present. If we have an active timer, we need to cancel it
432                     // as it's possible the window was previously present and has since disappeared.
433                     resetState();
434                     return;
435                 }
436 
437                 int currentOrder = 0;
438                 for (var windowInfo : aboveWindowInfos) {
439                     if (targetWindowInfo.displayId == windowInfo.displayId
440                             && Rect.intersects(targetWindowInfo.bounds, windowInfo.bounds)) {
441                         if (currentOrder < expectedOrder) {
442                             currentOrder++;
443                             continue;
444                         }
445                         // The window is occluded. If we have an active timer, we need to cancel it
446                         // as it's possible the window was previously not occluded and now is
447                         // occluded.
448                         resetState();
449                         return;
450                     }
451                 }
452                 if (currentOrder != expectedOrder) {
453                     resetState();
454                     return;
455                 }
456 
457                 if (targetWindowInfo.bounds.equals(mPreviousBounds)) {
458                     // The window matches previously found bounds. Let the active timer continue.
459                     return;
460                 }
461 
462                 // The window is present and not occluded but has different bounds than
463                 // previously seen or this is the first time we've detected the window. If
464                 // there's an active timer, cancel it. Schedule a task to toggle the latch in 200ms.
465                 resetState();
466                 mPreviousBounds.set(targetWindowInfo.bounds);
467                 mTask = new TimerTask() {
468                     @Override
469                     public void run() {
470                         satisfied.set(true);
471                         latch.countDown();
472                     }
473                 };
474                 mTimer.schedule(mTask, 200L * HW_TIMEOUT_MULTIPLIER);
475             }
476         };
477 
478         runWithSurfaceFlingerPermission(() -> {
479             var listener = new WindowInfosListenerForTest();
480             try {
481                 listener.addWindowInfosListener(windowNotOccluded);
482                 latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
483             } finally {
484                 listener.removeWindowInfosListener(windowNotOccluded);
485             }
486         });
487 
488         return satisfied.get();
489     }
490 
491     private interface InterruptableRunnable {
run()492         void run() throws InterruptedException;
493     };
494 
runWithSurfaceFlingerPermission(@onNull InterruptableRunnable runnable)495     private static void runWithSurfaceFlingerPermission(@NonNull InterruptableRunnable runnable)
496             throws InterruptedException {
497         Set<String> shellPermissions =
498                 InstrumentationRegistry.getInstrumentation().getUiAutomation()
499                         .getAdoptedShellPermissions();
500         if (shellPermissions.isEmpty()) {
501             SystemUtil.runWithShellPermissionIdentity(runnable::run,
502                     Manifest.permission.ACCESS_SURFACE_FLINGER);
503         } else if (shellPermissions.contains(Manifest.permission.ACCESS_SURFACE_FLINGER)) {
504             runnable.run();
505         } else {
506             throw new IllegalStateException(
507                     "waitForWindowOnTop called with adopted shell permissions that don't include "
508                             + "ACCESS_SURFACE_FLINGER");
509         }
510     }
511 
512     /**
513      * Waits until the window specified by windowTokenSupplier is present, not occluded, and hasn't
514      * had geometry changes for 200ms.
515      *
516      * The window is considered occluded if any part of another window is above it, excluding
517      * trusted overlays.
518      *
519      * <p>
520      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
521      * android.permission.ACCESS_SURFACE_FLINGER.
522      * </p>
523      *
524      * @param timeout             The amount of time to wait for the window to be visible.
525      * @param windowTokenSupplier Supplies the window token for the window to wait on. The
526      *                            supplier is called each time window infos change. If the
527      *                            supplier returns null, the window is assumed not visible
528      *                            yet.
529      * @return True if the window satisfies the visibility requirements before the timeout is
530      * reached. False otherwise.
531      */
waitForWindowOnTop(@onNull Duration timeout, @NonNull Supplier<IBinder> windowTokenSupplier)532     public static boolean waitForWindowOnTop(@NonNull Duration timeout,
533             @NonNull Supplier<IBinder> windowTokenSupplier)
534             throws InterruptedException {
535         return waitForWindowOnTop(timeout, windowInfo -> {
536             IBinder windowToken = windowTokenSupplier.get();
537             return windowToken != null && windowInfo.windowToken == windowToken;
538         });
539     }
540 
541     /**
542      * Waits until the window specified by {@code predicate} is present, at the expected level
543      * of the composition hierarchy, and hasn't had geometry changes for 200ms.
544      *
545      * The window is considered occluded if any part of another window is above it, excluding
546      * trusted overlays and bbq.
547      *
548      * <p>
549      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
550      * android.permission.ACCESS_SURFACE_FLINGER.
551      * </p>
552      *
553      * @param timeout             The amount of time to wait for the window to be visible.
554      * @param windowTokenSupplier Supplies the window token for the window to wait on. The
555      *                            supplier is called each time window infos change. If the
556      *                            supplier returns null, the window is assumed not visible
557      *                            yet.
558      * @param expectedOrder       The expected order of the surface control we are looking
559      *                            for.
560      * @return True if the window satisfies the visibility requirements before the timeout is
561      * reached. False otherwise.
562      */
waitForNthWindowFromTop(@onNull Duration timeout, @NonNull Supplier<IBinder> windowTokenSupplier, int expectedOrder)563     public static boolean waitForNthWindowFromTop(@NonNull Duration timeout,
564                                                   @NonNull Supplier<IBinder> windowTokenSupplier,
565                                                   int expectedOrder)
566             throws InterruptedException {
567         return waitForNthWindowFromTop(timeout, windowInfo -> {
568             IBinder windowToken = windowTokenSupplier.get();
569             return windowToken != null && windowInfo.windowToken == windowToken;
570         }, expectedOrder);
571     }
572 
573     /**
574      * Waits until the set of windows and their geometries are unchanged for 200ms.
575      *
576      * <p>
577      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
578      * android.permission.ACCESS_SURFACE_FLINGER.
579      * </p>
580      *
581      * @param timeout The amount of time to wait for the window to be visible.
582      * @return True if window geometry becomes stable before the timeout is reached. False
583      * otherwise.
584      */
585     public static boolean waitForStableWindowGeometry(@NonNull Duration timeout)
586             throws InterruptedException {
587         var latch = new CountDownLatch(1);
588         var satisfied = new AtomicBoolean();
589 
590         var timer = new Timer();
591         TimerTask[] task = {null};
592 
593         var previousBounds = new HashMap<IBinder, Rect>();
594         var currentBounds = new HashMap<IBinder, Rect>();
595 
596         BiConsumer<List<WindowInfo>, List<DisplayInfo>> consumer =
597                 (windowInfos, displayInfos) -> {
598                     if (satisfied.get()) {
599                         return;
600                     }
601 
602                     currentBounds.clear();
603                     for (var windowInfo : windowInfos) {
604                         currentBounds.put(windowInfo.windowToken, windowInfo.bounds);
605                     }
606 
607                     if (currentBounds.equals(previousBounds)) {
608                         // No changes detected. Let the previously scheduled timer task continue.
609                         return;
610                     }
611 
612                     previousBounds.clear();
613                     previousBounds.putAll(currentBounds);
614 
615                     // Something has changed. Cancel the previous timer task and schedule a new task
616                     // to countdown the latch in 200ms.
617                     if (task[0] != null) {
618                         task[0].cancel();
619                     }
620                     task[0] =
621                             new TimerTask() {
622                                 @Override
623                                 public void run() {
624                                     satisfied.set(true);
625                                     latch.countDown();
626                                 }
627                             };
628                     timer.schedule(task[0], 200L * HW_TIMEOUT_MULTIPLIER);
629                 };
630 
631         runWithSurfaceFlingerPermission(() -> {
632             var listener = new WindowInfosListenerForTest();
633             try {
634                 listener.addWindowInfosListener(consumer);
635                 latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
636             } finally {
637                 listener.removeWindowInfosListener(consumer);
638             }
639         });
640 
641         return satisfied.get();
642     }
643 
644     /**
645      * Tap on the center coordinates of the specified window and sends back the coordinates tapped
646      * </p>
647      *
648      * @param instrumentation     Instrumentation object to use for tap.
649      * @param windowTokenSupplier Supplies the window token for the window to wait on. The supplier
650      *                            is called each time window infos change. If the supplier returns
651      *                            null, the window is assumed not visible yet.
652      * @param outCoords           If non null, the tapped coordinates will be set in the object.
653      * @return true if successfully tapped on the coordinates, false otherwise.
654      * @throws InterruptedException if failed to wait for WindowInfo
655      */
656     public static boolean tapOnWindowCenter(Instrumentation instrumentation,
657             @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point outCoords)
658             throws InterruptedException {
659         return tapOnWindowCenter(instrumentation, windowTokenSupplier, outCoords, DEFAULT_DISPLAY);
660     }
661 
662     /**
663      * Tap on the center coordinates of the specified window and sends back the coordinates tapped
664      * </p>
665      *
666      * @param instrumentation     Instrumentation object to use for tap.
667      * @param windowTokenSupplier Supplies the window token for the window to wait on. The supplier
668      *                            is called each time window infos change. If the supplier returns
669      *                            null, the window is assumed not visible yet.
670      * @param outCoords           If non null, the tapped coordinates will be set in the object.
671      * @param displayId           The ID of the display on which to tap the window center.
672      * @return true if successfully tapped on the coordinates, false otherwise.
673      * @throws InterruptedException if failed to wait for WindowInfo
674      */
675     public static boolean tapOnWindowCenter(Instrumentation instrumentation,
676             @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point outCoords,
677             int displayId) throws InterruptedException {
678         Rect bounds = getWindowBoundsInDisplaySpace(windowTokenSupplier, displayId);
679         if (bounds == null) {
680             return false;
681         }
682 
683         final Point coord = new Point(bounds.left + bounds.width() / 2,
684                 bounds.top + bounds.height() / 2);
685         sendTap(instrumentation, coord);
686         if (outCoords != null) {
687             outCoords.set(coord.x, coord.y);
688         }
689         return true;
690     }
691 
692     /**
693      * Tap on the coordinates of the specified window, offset by the value passed in.
694      * </p>
695      *
696      * @param instrumentation     Instrumentation object to use for tap.
697      * @param windowTokenSupplier Supplies the window token for the window to wait on. The supplier
698      *                            is called each time window infos change. If the supplier returns
699      *                            null, the window is assumed not visible yet.
700      * @param offset              The offset from 0,0 of the window to tap on. If null, it will be
701      *                            ignored and 0,0 will be tapped.
702      * @return true if successfully tapped on the coordinates, false otherwise.
703      * @throws InterruptedException if failed to wait for WindowInfo
704      */
705     public static boolean tapOnWindow(Instrumentation instrumentation,
706             @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point offset)
707             throws InterruptedException {
708         return tapOnWindow(instrumentation, windowTokenSupplier, offset, DEFAULT_DISPLAY);
709     }
710 
711     /**
712      * Tap on the coordinates of the specified window, offset by the value passed in.
713      * </p>
714      *
715      * @param instrumentation     Instrumentation object to use for tap.
716      * @param windowTokenSupplier Supplies the window token for the window to wait on. The supplier
717      *                            is called each time window infos change. If the supplier returns
718      *                            null, the window is assumed not visible yet.
719      * @param offset              The offset from 0,0 of the window to tap on. If null, it will be
720      *                            ignored and 0,0 will be tapped.
721      * @param displayId           The ID of the display on which to tap the window.
722      * @return true if successfully tapped on the coordinates, false otherwise.
723      * @throws InterruptedException if failed to wait for WindowInfo
724      */
725     public static boolean tapOnWindow(Instrumentation instrumentation,
726             @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point offset,
727             int displayId) throws InterruptedException {
728         Rect bounds = getWindowBoundsInDisplaySpace(windowTokenSupplier, displayId);
729         if (bounds == null) {
730             return false;
731         }
732 
733         final Point coord = new Point(bounds.left + (offset != null ? offset.x : 0),
734                 bounds.top + (offset != null ? offset.y : 0));
735         sendTap(instrumentation, coord);
736         return true;
737     }
738 
739     public static Rect getWindowBoundsInWindowSpace(@NonNull Supplier<IBinder> windowTokenSupplier)
740             throws InterruptedException {
741         return getWindowBoundsInWindowSpace(windowTokenSupplier, DEFAULT_DISPLAY);
742     }
743 
744     /**
745      * Get the bounds of a window in window space.
746      *
747      * @param windowTokenSupplier A supplier that provides the window token.
748      * @param displayId The ID of the display for which the window bounds are to be retrieved.
749      * @return A {@link Rect} representing the bounds of the window in window space,
750      *         or null if the window information is not available within the timeout period.
751      * @throws InterruptedException If the thread is interrupted while waiting for the window
752      *         information.
753      */
754     public static Rect getWindowBoundsInWindowSpace(@NonNull Supplier<IBinder> windowTokenSupplier,
755             int displayId) throws InterruptedException {
756         Rect bounds = new Rect();
757         Predicate<WindowInfo> predicate = windowInfo -> {
758             if (!windowInfo.bounds.isEmpty()) {
759                 if (!windowInfo.transform.isIdentity()) {
760                     RectF rectF = new RectF(windowInfo.bounds);
761                     windowInfo.transform.mapRect(rectF);
762                     bounds.set((int) rectF.left, (int) rectF.top, (int) rectF.right,
763                             (int) rectF.bottom);
764                 } else {
765                     bounds.set(windowInfo.bounds);
766                 }
767                 return true;
768             }
769 
770             return false;
771         };
772 
773         if (!waitForWindowInfo(predicate, Duration.ofSeconds(5L * HW_TIMEOUT_MULTIPLIER),
774                 windowTokenSupplier, displayId)) {
775             return null;
776         }
777         return bounds;
778     }
779 
780     public static Rect getWindowBoundsInDisplaySpace(@NonNull Supplier<IBinder> windowTokenSupplier)
781             throws InterruptedException {
782         return getWindowBoundsInDisplaySpace(windowTokenSupplier, DEFAULT_DISPLAY);
783     }
784 
785     /**
786      * Get the bounds of a window in display space for a specified display.
787      *
788      * @param windowTokenSupplier A supplier that provides the window token.
789      * @param displayId The ID of the display for which the window bounds are to be retrieved.
790      * @return A {@link Rect} representing the bounds of the window in display space, or null
791      *         if the window information is not available within the timeout period.
792      * @throws InterruptedException If the thread is interrupted while waiting for the
793      *         window information.
794      */
795     public static Rect getWindowBoundsInDisplaySpace(@NonNull Supplier<IBinder> windowTokenSupplier,
796              int displayId) throws InterruptedException {
797         Rect bounds = new Rect();
798         Predicate<WindowInfo> predicate = windowInfo -> {
799             if (!windowInfo.bounds.isEmpty()) {
800                 bounds.set(windowInfo.bounds);
801                 return true;
802             }
803 
804             return false;
805         };
806 
807         if (!waitForWindowInfo(predicate, Duration.ofSeconds(5L * HW_TIMEOUT_MULTIPLIER),
808                 windowTokenSupplier, displayId)) {
809             return null;
810         }
811         return bounds;
812     }
813 
814     /**
815      * Get the center coordinates of the specified window
816      *
817      * @param windowTokenSupplier Supplies the window token for the window to wait on. The supplier
818      *                            is called each time window infos change. If the supplier returns
819      *                            null, the window is assumed not visible yet.
820      * @param displayId The ID of the display on which the window is located.
821      * @return Point of the window center
822      * @throws InterruptedException if failed to wait for WindowInfo
823      */
824     public static Point getWindowCenter(@NonNull Supplier<IBinder> windowTokenSupplier,
825             int displayId) throws InterruptedException {
826         final Rect bounds = getWindowBoundsInDisplaySpace(windowTokenSupplier, displayId);
827         if (bounds == null) {
828             throw new IllegalArgumentException("Could not get the bounds for window");
829         }
830         return new Point(bounds.left + bounds.width() / 2, bounds.top + bounds.height() / 2);
831     }
832 
833     /**
834      * Sends tap to the specified coordinates.
835      * </p>
836      *
837      * @param instrumentation    Instrumentation object to use for tap.
838      * @param coord              The coordinates to tap on in display space.
839      * @throws InterruptedException if failed to wait for WindowInfo
840      */
841     public static void sendTap(Instrumentation instrumentation, Point coord) {
842         // Get anchor coordinates on the screen
843         final long downTime = SystemClock.uptimeMillis();
844 
845         CtsTouchUtils ctsTouchUtils = new CtsTouchUtils(instrumentation.getTargetContext());
846         ctsTouchUtils.injectDownEvent(instrumentation, downTime, coord.x, coord.y,
847                 /* eventInjectionListener= */ null);
848         ctsTouchUtils.injectUpEvent(instrumentation, downTime, false, coord.x, coord.y, null);
849 
850         instrumentation.waitForIdleSync();
851     }
852 
853     public static boolean waitForWindowFocus(final View view, boolean hasWindowFocus) {
854         final CountDownLatch latch = new CountDownLatch(1);
855 
856         view.getHandler().post(() -> {
857             if (view.hasWindowFocus() == hasWindowFocus) {
858                 latch.countDown();
859                 return;
860             }
861             view.getViewTreeObserver().addOnWindowFocusChangeListener(
862                     new ViewTreeObserver.OnWindowFocusChangeListener() {
863                         @Override
864                         public void onWindowFocusChanged(boolean newFocusState) {
865                             if (hasWindowFocus == newFocusState) {
866                                 view.getViewTreeObserver()
867                                         .removeOnWindowFocusChangeListener(this);
868                                 latch.countDown();
869                             }
870                         }
871                     });
872 
873             view.invalidate();
874         });
875 
876         try {
877             if (!latch.await(HW_TIMEOUT_MULTIPLIER * 10L, TimeUnit.SECONDS)) {
878                 return false;
879             }
880         } catch (InterruptedException e) {
881             return false;
882         }
883         return true;
884     }
885 
886     public static void dumpWindowsOnScreen(String tag, String message)
887             throws InterruptedException {
888         waitForWindowInfos(windowInfos -> {
889             if (windowInfos.isEmpty()) {
890                 return false;
891             }
892             Log.d(tag, "Dumping windows on screen: " + message);
893             for (var windowInfo : windowInfos) {
894                 Log.d(tag, "     " + windowInfo);
895             }
896             return true;
897         }, Duration.ofSeconds(5L * HW_TIMEOUT_MULTIPLIER));
898     }
899 
900     /**
901      * Assert the condition and dump the window states if the condition fails.
902      */
903     public static void assertAndDumpWindowState(String tag, String message, boolean condition)
904             throws InterruptedException {
905         if (!condition) {
906             dumpWindowsOnScreen(tag, message);
907         }
908 
909         assertTrue(message, condition);
910     }
911 
912     /**
913      * Get the current window and display state.
914      */
915     public static Pair<List<WindowInfo>, List<DisplayInfo>> getWindowAndDisplayState()
916             throws InterruptedException {
917         var consumer =
918                 new BiConsumer<List<WindowInfo>, List<DisplayInfo>>() {
919                     private CountDownLatch mLatch = new CountDownLatch(1);
920                     private boolean mComplete = false;
921 
922                     List<WindowInfo> mWindowInfos;
923                     List<DisplayInfo> mDisplayInfos;
924 
925                     @Override
926                     public void accept(List<WindowInfo> windows, List<DisplayInfo> displays) {
927                         if (mComplete || windows.isEmpty() || displays.isEmpty()) {
928                             return;
929                         }
930                         mComplete = true;
931                         mWindowInfos = windows;
932                         mDisplayInfos = displays;
933                         mLatch.countDown();
934                     }
935 
936                     void await() throws InterruptedException {
937                         mLatch.await(5L * HW_TIMEOUT_MULTIPLIER, TimeUnit.SECONDS);
938                     }
939 
940                     Pair<List<WindowInfo>, List<DisplayInfo>> getState() {
941                         return new Pair(mWindowInfos, mDisplayInfos);
942                     }
943                 };
944 
945         var waitForState =
946                 new ThrowingRunnable() {
947                     @Override
948                     public void run() throws InterruptedException {
949                         var listener = new WindowInfosListenerForTest();
950                         try {
951                             listener.addWindowInfosListener(consumer);
952                             consumer.await();
953                         } finally {
954                             listener.removeWindowInfosListener(consumer);
955                         }
956                     }
957                 };
958 
959         var uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
960         Set<String> shellPermissions = uiAutomation.getAdoptedShellPermissions();
961         if (shellPermissions.isEmpty()) {
962             SystemUtil.runWithShellPermissionIdentity(
963                     uiAutomation, waitForState, Manifest.permission.ACCESS_SURFACE_FLINGER);
964         } else if (shellPermissions.contains(Manifest.permission.ACCESS_SURFACE_FLINGER)) {
965             waitForState.run();
966         } else {
967             throw new IllegalStateException(
968                     "getWindowAndDisplayState called with adopted shell permissions that don't"
969                             + " include ACCESS_SURFACE_FLINGER");
970         }
971 
972         return consumer.getState();
973     }
974 
975     private static String getHashCode(Object obj) {
976         return Integer.toHexString(System.identityHashCode(obj));
977     }
978 }
979