• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      * Waits for a window to become invisible.
336      *
337      * @param name A name associated with the target window we are waiting for.
338      * @param timeout The amount of time to wait for the window to be invisible.
339      * @return {@code true} if the window becomes invisible within the timeout period, {@code false}
340      *     otherwise.
341      * @throws InterruptedException If the thread is interrupted while waiting for the window
342      *     information.
343      */
waitForWindowInvisible(@onNull String name, @NonNull Duration timeout)344     public static boolean waitForWindowInvisible(@NonNull String name, @NonNull Duration timeout)
345             throws InterruptedException {
346         return CtsWindowInfoUtils.waitForWindowInfos(
347                 windows -> windows.stream().noneMatch(window -> window.name.contains(name)),
348                 timeout);
349     }
350 
351     /**
352      * Calls {@link CtsWindowInfoUtils#waitForWindowOnTop(Duration, Supplier)}. Adopts
353      * required permissions and waits at least five seconds before timing out.
354      *
355      * @param window The window to wait on.
356      * @return True if the window satisfies the visibility requirements before the timeout is
357      * reached. False otherwise.
358      */
waitForWindowOnTop(@onNull Window window)359     public static boolean waitForWindowOnTop(@NonNull Window window) throws InterruptedException {
360         return waitForWindowOnTop(Duration.ofSeconds(HW_TIMEOUT_MULTIPLIER * 5L),
361                 () -> window.getDecorView().getWindowToken());
362     }
363 
364     /**
365      * Waits until the window specified by the predicate is present, not occluded, and hasn't
366      * had geometry changes for 200ms.
367      *
368      * The window is considered occluded if any part of another window is above it, excluding
369      * trusted overlays.
370      *
371      * <p>
372      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
373      * android.permission.ACCESS_SURFACE_FLINGER.
374      * </p>
375      *
376      * @param timeout             The amount of time to wait for the window to be visible.
377      * @param predicate           A predicate identifying the target window we are waiting for,
378      *                            will be tested each time window infos change.
379      * @return True if the window satisfies the visibility requirements before the timeout is
380      * reached. False otherwise.
381      */
waitForWindowOnTop(@onNull Duration timeout, @NonNull Predicate<WindowInfo> predicate)382     public static boolean waitForWindowOnTop(@NonNull Duration timeout,
383                                              @NonNull Predicate<WindowInfo> predicate)
384             throws InterruptedException {
385         return waitForNthWindowFromTop(timeout, predicate, 0);
386     }
387 
388     /**
389      * Waits until the window specified by {@code predicate} is present, at the expected level
390      * of the composition hierarchy, and hasn't had geometry changes for 200ms.
391      *
392      * @see #waitForNthWindowFromTop(Duration, Predicate, int, boolean)
393      */
waitForNthWindowFromTop(@onNull Duration timeout, @NonNull Predicate<WindowInfo> predicate, int expectedOrder)394     public static boolean waitForNthWindowFromTop(@NonNull Duration timeout,
395                                                   @NonNull Predicate<WindowInfo> predicate,
396                                                   int expectedOrder)
397             throws InterruptedException {
398         return waitForNthWindowFromTop(timeout, predicate, expectedOrder, /* stabilize= */ true);
399     }
400 
401     /**
402      * Waits until the window specified by {@code predicate} is present, at the expected level
403      * of the composition hierarchy.
404      *
405      * The window is considered occluded if any part of another window is above it, excluding
406      * trusted overlays and bbq.
407      *
408      * <p>
409      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
410      * android.permission.ACCESS_SURFACE_FLINGER.
411      * </p>
412      *
413      * @param timeout       The amount of time to wait for the window to be visible.
414      * @param predicate     A predicate identifying the target window we are waiting, will be
415      *                      tested each time window infos change.
416      * @param expectedOrder The expected order of the surface control we are looking
417      *                      for.
418      * @param stabilize     Whether to wait until the window geometry is stable before
419      *                      returning. If true, it waits until the window geometry is stable for
420      *                      200ms.
421      * @return True if the window satisfies the visibility requirements before the timeout is
422      * reached. False otherwise.
423      */
waitForNthWindowFromTop(@onNull Duration timeout, @NonNull Predicate<WindowInfo> predicate, int expectedOrder, boolean stabilize)424     private static boolean waitForNthWindowFromTop(@NonNull Duration timeout,
425                                                   @NonNull Predicate<WindowInfo> predicate,
426                                                   int expectedOrder, boolean stabilize)
427             throws InterruptedException {
428         var latch = new CountDownLatch(1);
429         var satisfied = new AtomicBoolean();
430 
431         var windowNotOccluded = new BiConsumer<List<WindowInfo>, List<DisplayInfo>>() {
432             private Timer mTimer = new Timer();
433             private TimerTask mTask = null;
434             private Rect mPreviousBounds = new Rect(0, 0, -1, -1);
435 
436             private void resetState() {
437                 if (mTask != null) {
438                     mTask.cancel();
439                     mTask = null;
440                 }
441                 mPreviousBounds.set(0, 0, -1, -1);
442             }
443 
444             @Override
445             public void accept(List<WindowInfo> windowInfos, List<DisplayInfo> displayInfos) {
446                 if (satisfied.get()) {
447                     return;
448                 }
449 
450                 WindowInfo targetWindowInfo = null;
451                 ArrayList<WindowInfo> aboveWindowInfos = new ArrayList<>();
452                 for (var windowInfo : windowInfos) {
453                     if (predicate.test(windowInfo)) {
454                         targetWindowInfo = windowInfo;
455                         break;
456                     }
457                     if (windowInfo.isTrustedOverlay || !windowInfo.isVisible) {
458                         continue;
459                     }
460                     aboveWindowInfos.add(windowInfo);
461                 }
462 
463                 if (targetWindowInfo == null) {
464                     // The window isn't present. If we have an active timer, we need to cancel it
465                     // as it's possible the window was previously present and has since disappeared.
466                     resetState();
467                     return;
468                 }
469 
470                 int currentOrder = 0;
471                 for (var windowInfo : aboveWindowInfos) {
472                     if (targetWindowInfo.displayId == windowInfo.displayId
473                             && Rect.intersects(targetWindowInfo.bounds, windowInfo.bounds)) {
474                         if (currentOrder < expectedOrder) {
475                             currentOrder++;
476                             continue;
477                         }
478                         // The window is occluded. If we have an active timer, we need to cancel it
479                         // as it's possible the window was previously not occluded and now is
480                         // occluded.
481                         resetState();
482                         return;
483                     }
484                 }
485                 if (currentOrder != expectedOrder) {
486                     resetState();
487                     return;
488                 }
489 
490                 if (targetWindowInfo.bounds.equals(mPreviousBounds)) {
491                     // The window matches previously found bounds. Let the active timer continue.
492                     return;
493                 }
494 
495                 // The window is present and not occluded but has different bounds than
496                 // previously seen or this is the first time we've detected the window. If
497                 // there's an active timer, cancel it. Schedule a task to toggle the latch in 200ms.
498                 resetState();
499                 mPreviousBounds.set(targetWindowInfo.bounds);
500                 mTask = new TimerTask() {
501                     @Override
502                     public void run() {
503                         satisfied.set(true);
504                         latch.countDown();
505                     }
506                 };
507                 mTimer.schedule(mTask, stabilize ? 200L * HW_TIMEOUT_MULTIPLIER : 0L);
508             }
509         };
510 
511         runWithSurfaceFlingerPermission(() -> {
512             var listener = new WindowInfosListenerForTest();
513             try {
514                 listener.addWindowInfosListener(windowNotOccluded);
515                 latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
516             } finally {
517                 listener.removeWindowInfosListener(windowNotOccluded);
518             }
519         });
520 
521         return satisfied.get();
522     }
523 
524     private interface InterruptableRunnable {
run()525         void run() throws InterruptedException;
526     };
527 
runWithSurfaceFlingerPermission(@onNull InterruptableRunnable runnable)528     private static void runWithSurfaceFlingerPermission(@NonNull InterruptableRunnable runnable)
529             throws InterruptedException {
530         Set<String> shellPermissions =
531                 InstrumentationRegistry.getInstrumentation().getUiAutomation()
532                         .getAdoptedShellPermissions();
533         if (shellPermissions.isEmpty()) {
534             SystemUtil.runWithShellPermissionIdentity(runnable::run,
535                     Manifest.permission.ACCESS_SURFACE_FLINGER);
536         } else if (shellPermissions.contains(Manifest.permission.ACCESS_SURFACE_FLINGER)) {
537             runnable.run();
538         } else {
539             throw new IllegalStateException(
540                     "waitForWindowOnTop called with adopted shell permissions that don't include "
541                             + "ACCESS_SURFACE_FLINGER");
542         }
543     }
544 
545     /**
546      * Waits until the window specified by windowTokenSupplier is present, not occluded, and hasn't
547      * had geometry changes for 200ms.
548      *
549      * The window is considered occluded if any part of another window is above it, excluding
550      * trusted overlays.
551      *
552      * <p>
553      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
554      * android.permission.ACCESS_SURFACE_FLINGER.
555      * </p>
556      *
557      * @param timeout             The amount of time to wait for the window to be visible.
558      * @param windowTokenSupplier Supplies the window token for the window to wait on. The
559      *                            supplier is called each time window infos change. If the
560      *                            supplier returns null, the window is assumed not visible
561      *                            yet.
562      * @return True if the window satisfies the visibility requirements before the timeout is
563      * reached. False otherwise.
564      */
waitForWindowOnTop(@onNull Duration timeout, @NonNull Supplier<IBinder> windowTokenSupplier)565     public static boolean waitForWindowOnTop(@NonNull Duration timeout,
566             @NonNull Supplier<IBinder> windowTokenSupplier)
567             throws InterruptedException {
568         return waitForWindowOnTop(timeout, windowInfo -> {
569             IBinder windowToken = windowTokenSupplier.get();
570             return windowToken != null && windowInfo.windowToken == windowToken;
571         });
572     }
573 
574     /**
575      * Waits until the given window is present and not occluded.
576      *
577      * Same as {@link #waitForWindowOnTop(Window)}, but doesn't wait for the window
578      * geometry to stabilize.
579      */
waitForWindowOnTopImmediate(@onNull Window window)580     public static boolean waitForWindowOnTopImmediate(@NonNull Window window)
581             throws InterruptedException {
582         return waitForNthWindowFromTop(Duration.ofSeconds(HW_TIMEOUT_MULTIPLIER * 5L),
583             windowInfo -> {
584                 IBinder windowToken = window.getDecorView().getWindowToken();
585                 return windowToken != null && windowInfo.windowToken == windowToken;
586             }, /* expectedOrder= */ 0, /* stabilize= */ false);
587     }
588 
589     /**
590      * Waits until the window specified by {@code predicate} is present, at the expected level
591      * of the composition hierarchy, and hasn't had geometry changes for 200ms.
592      *
593      * The window is considered occluded if any part of another window is above it, excluding
594      * trusted overlays and bbq.
595      *
596      * <p>
597      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
598      * android.permission.ACCESS_SURFACE_FLINGER.
599      * </p>
600      *
601      * @param timeout             The amount of time to wait for the window to be visible.
602      * @param windowTokenSupplier Supplies the window token for the window to wait on. The
603      *                            supplier is called each time window infos change. If the
604      *                            supplier returns null, the window is assumed not visible
605      *                            yet.
606      * @param expectedOrder       The expected order of the surface control we are looking
607      *                            for.
608      * @return True if the window satisfies the visibility requirements before the timeout is
609      * reached. False otherwise.
610      */
611     public static boolean waitForNthWindowFromTop(@NonNull Duration timeout,
612                                                   @NonNull Supplier<IBinder> windowTokenSupplier,
613                                                   int expectedOrder)
614             throws InterruptedException {
615         return waitForNthWindowFromTop(timeout, windowInfo -> {
616             IBinder windowToken = windowTokenSupplier.get();
617             return windowToken != null && windowInfo.windowToken == windowToken;
618         }, expectedOrder);
619     }
620 
621     /**
622      * Waits until the set of windows and their geometries are unchanged for 200ms.
623      *
624      * <p>
625      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
626      * android.permission.ACCESS_SURFACE_FLINGER.
627      * </p>
628      *
629      * @param timeout The amount of time to wait for the window to be visible.
630      * @return True if window geometry becomes stable before the timeout is reached. False
631      * otherwise.
632      */
633     public static boolean waitForStableWindowGeometry(@NonNull Duration timeout)
634             throws InterruptedException {
635         var latch = new CountDownLatch(1);
636         var satisfied = new AtomicBoolean();
637 
638         var timer = new Timer();
639         TimerTask[] task = {null};
640 
641         var previousBounds = new HashMap<IBinder, Rect>();
642         var currentBounds = new HashMap<IBinder, Rect>();
643 
644         BiConsumer<List<WindowInfo>, List<DisplayInfo>> consumer =
645                 (windowInfos, displayInfos) -> {
646                     if (satisfied.get()) {
647                         return;
648                     }
649 
650                     currentBounds.clear();
651                     for (var windowInfo : windowInfos) {
652                         currentBounds.put(windowInfo.windowToken, windowInfo.bounds);
653                     }
654 
655                     if (currentBounds.equals(previousBounds)) {
656                         // No changes detected. Let the previously scheduled timer task continue.
657                         return;
658                     }
659 
660                     previousBounds.clear();
661                     previousBounds.putAll(currentBounds);
662 
663                     // Something has changed. Cancel the previous timer task and schedule a new task
664                     // to countdown the latch in 200ms.
665                     if (task[0] != null) {
666                         task[0].cancel();
667                     }
668                     task[0] =
669                             new TimerTask() {
670                                 @Override
671                                 public void run() {
672                                     satisfied.set(true);
673                                     latch.countDown();
674                                 }
675                             };
676                     timer.schedule(task[0], 200L * HW_TIMEOUT_MULTIPLIER);
677                 };
678 
679         runWithSurfaceFlingerPermission(() -> {
680             var listener = new WindowInfosListenerForTest();
681             try {
682                 listener.addWindowInfosListener(consumer);
683                 latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
684             } finally {
685                 listener.removeWindowInfosListener(consumer);
686             }
687         });
688 
689         return satisfied.get();
690     }
691 
692     /**
693      * Tap on the center coordinates of the specified window and sends back the coordinates tapped
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 outCoords           If non null, the tapped coordinates will be set in the object.
701      * @return true if successfully tapped on the coordinates, false otherwise.
702      * @throws InterruptedException if failed to wait for WindowInfo
703      */
704     public static boolean tapOnWindowCenter(Instrumentation instrumentation,
705             @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point outCoords)
706             throws InterruptedException {
707         return tapOnWindowCenter(instrumentation, windowTokenSupplier, outCoords, DEFAULT_DISPLAY);
708     }
709 
710     /**
711      * Tap on the center coordinates of the specified window and sends back the coordinates tapped
712      * </p>
713      *
714      * @param instrumentation     Instrumentation object to use for tap.
715      * @param windowTokenSupplier Supplies the window token for the window to wait on. The supplier
716      *                            is called each time window infos change. If the supplier returns
717      *                            null, the window is assumed not visible yet.
718      * @param outCoords           If non null, the tapped coordinates will be set in the object.
719      * @param displayId           The ID of the display on which to tap the window center.
720      * @return true if successfully tapped on the coordinates, false otherwise.
721      * @throws InterruptedException if failed to wait for WindowInfo
722      */
723     public static boolean tapOnWindowCenter(Instrumentation instrumentation,
724             @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point outCoords,
725             int displayId) throws InterruptedException {
726         Rect bounds = getWindowBoundsInDisplaySpace(windowTokenSupplier, displayId);
727         if (bounds == null) {
728             return false;
729         }
730 
731         final Point coord = new Point(bounds.left + bounds.width() / 2,
732                 bounds.top + bounds.height() / 2);
733         sendTap(instrumentation, coord);
734         if (outCoords != null) {
735             outCoords.set(coord.x, coord.y);
736         }
737         return true;
738     }
739 
740     /**
741      * Tap on the coordinates of the specified window, offset by the value passed in.
742      * </p>
743      *
744      * @param instrumentation     Instrumentation object to use for tap.
745      * @param windowTokenSupplier Supplies the window token for the window to wait on. The supplier
746      *                            is called each time window infos change. If the supplier returns
747      *                            null, the window is assumed not visible yet.
748      * @param offset              The offset from 0,0 of the window to tap on. If null, it will be
749      *                            ignored and 0,0 will be tapped.
750      * @return true if successfully tapped on the coordinates, false otherwise.
751      * @throws InterruptedException if failed to wait for WindowInfo
752      */
753     public static boolean tapOnWindow(Instrumentation instrumentation,
754             @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point offset)
755             throws InterruptedException {
756         return tapOnWindow(instrumentation, windowTokenSupplier, offset, DEFAULT_DISPLAY);
757     }
758 
759     /**
760      * Tap on the coordinates of the specified window, offset by the value passed in.
761      * </p>
762      *
763      * @param instrumentation     Instrumentation object to use for tap.
764      * @param windowTokenSupplier Supplies the window token for the window to wait on. The supplier
765      *                            is called each time window infos change. If the supplier returns
766      *                            null, the window is assumed not visible yet.
767      * @param offset              The offset from 0,0 of the window to tap on. If null, it will be
768      *                            ignored and 0,0 will be tapped.
769      * @param displayId           The ID of the display on which to tap the window.
770      * @return true if successfully tapped on the coordinates, false otherwise.
771      * @throws InterruptedException if failed to wait for WindowInfo
772      */
773     public static boolean tapOnWindow(Instrumentation instrumentation,
774             @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point offset,
775             int displayId) throws InterruptedException {
776         Rect bounds = getWindowBoundsInDisplaySpace(windowTokenSupplier, displayId);
777         if (bounds == null) {
778             return false;
779         }
780 
781         final Point coord = new Point(bounds.left + (offset != null ? offset.x : 0),
782                 bounds.top + (offset != null ? offset.y : 0));
783         sendTap(instrumentation, coord);
784         return true;
785     }
786 
787     public static Rect getWindowBoundsInWindowSpace(@NonNull Supplier<IBinder> windowTokenSupplier)
788             throws InterruptedException {
789         return getWindowBoundsInWindowSpace(windowTokenSupplier, DEFAULT_DISPLAY);
790     }
791 
792     /**
793      * Get the bounds of a window in window space.
794      *
795      * @param windowTokenSupplier A supplier that provides the window token.
796      * @param displayId The ID of the display for which the window bounds are to be retrieved.
797      * @return A {@link Rect} representing the bounds of the window in window space,
798      *         or null if the window information is not available within the timeout period.
799      * @throws InterruptedException If the thread is interrupted while waiting for the window
800      *         information.
801      */
802     public static Rect getWindowBoundsInWindowSpace(@NonNull Supplier<IBinder> windowTokenSupplier,
803             int displayId) throws InterruptedException {
804         Rect bounds = new Rect();
805         Predicate<WindowInfo> predicate = windowInfo -> {
806             if (!windowInfo.bounds.isEmpty()) {
807                 if (!windowInfo.transform.isIdentity()) {
808                     RectF rectF = new RectF(windowInfo.bounds);
809                     windowInfo.transform.mapRect(rectF);
810                     bounds.set((int) rectF.left, (int) rectF.top, (int) rectF.right,
811                             (int) rectF.bottom);
812                 } else {
813                     bounds.set(windowInfo.bounds);
814                 }
815                 return true;
816             }
817 
818             return false;
819         };
820 
821         if (!waitForWindowInfo(predicate, Duration.ofSeconds(5L * HW_TIMEOUT_MULTIPLIER),
822                 windowTokenSupplier, displayId)) {
823             return null;
824         }
825         return bounds;
826     }
827 
828     public static Rect getWindowBoundsInDisplaySpace(@NonNull Supplier<IBinder> windowTokenSupplier)
829             throws InterruptedException {
830         return getWindowBoundsInDisplaySpace(windowTokenSupplier, DEFAULT_DISPLAY);
831     }
832 
833     /**
834      * Get the bounds of a window in display space for a specified display.
835      *
836      * @param windowTokenSupplier A supplier that provides the window token.
837      * @param displayId The ID of the display for which the window bounds are to be retrieved.
838      * @return A {@link Rect} representing the bounds of the window in display space, or null
839      *         if the window information is not available within the timeout period.
840      * @throws InterruptedException If the thread is interrupted while waiting for the
841      *         window information.
842      */
843     public static Rect getWindowBoundsInDisplaySpace(@NonNull Supplier<IBinder> windowTokenSupplier,
844              int displayId) throws InterruptedException {
845         Rect bounds = new Rect();
846         Predicate<WindowInfo> predicate = windowInfo -> {
847             if (!windowInfo.bounds.isEmpty()) {
848                 bounds.set(windowInfo.bounds);
849                 return true;
850             }
851 
852             return false;
853         };
854 
855         if (!waitForWindowInfo(predicate, Duration.ofSeconds(5L * HW_TIMEOUT_MULTIPLIER),
856                 windowTokenSupplier, displayId)) {
857             return null;
858         }
859         return bounds;
860     }
861 
862     /**
863      * Get the center coordinates of the specified window
864      *
865      * @param windowTokenSupplier Supplies the window token for the window to wait on. The supplier
866      *                            is called each time window infos change. If the supplier returns
867      *                            null, the window is assumed not visible yet.
868      * @param displayId The ID of the display on which the window is located.
869      * @return Point of the window center
870      * @throws InterruptedException if failed to wait for WindowInfo
871      */
872     public static Point getWindowCenter(@NonNull Supplier<IBinder> windowTokenSupplier,
873             int displayId) throws InterruptedException {
874         final Rect bounds = getWindowBoundsInDisplaySpace(windowTokenSupplier, displayId);
875         if (bounds == null) {
876             throw new IllegalArgumentException("Could not get the bounds for window");
877         }
878         return new Point(bounds.left + bounds.width() / 2, bounds.top + bounds.height() / 2);
879     }
880 
881     /**
882      * Sends tap to the specified coordinates.
883      * </p>
884      *
885      * @param instrumentation    Instrumentation object to use for tap.
886      * @param coord              The coordinates to tap on in display space.
887      * @throws InterruptedException if failed to wait for WindowInfo
888      */
889     public static void sendTap(Instrumentation instrumentation, Point coord) {
890         // Get anchor coordinates on the screen
891         final long downTime = SystemClock.uptimeMillis();
892 
893         CtsTouchUtils ctsTouchUtils = new CtsTouchUtils(instrumentation.getTargetContext());
894         ctsTouchUtils.injectDownEvent(instrumentation, downTime, coord.x, coord.y,
895                 /* eventInjectionListener= */ null);
896         ctsTouchUtils.injectUpEvent(instrumentation, downTime, false, coord.x, coord.y, null);
897 
898         instrumentation.waitForIdleSync();
899     }
900 
901     public static boolean waitForWindowFocus(final View view, boolean hasWindowFocus) {
902         final CountDownLatch latch = new CountDownLatch(1);
903 
904         view.getHandler().post(() -> {
905             if (view.hasWindowFocus() == hasWindowFocus) {
906                 latch.countDown();
907                 return;
908             }
909             view.getViewTreeObserver().addOnWindowFocusChangeListener(
910                     new ViewTreeObserver.OnWindowFocusChangeListener() {
911                         @Override
912                         public void onWindowFocusChanged(boolean newFocusState) {
913                             if (hasWindowFocus == newFocusState) {
914                                 view.getViewTreeObserver()
915                                         .removeOnWindowFocusChangeListener(this);
916                                 latch.countDown();
917                             }
918                         }
919                     });
920 
921             view.invalidate();
922         });
923 
924         try {
925             if (!latch.await(HW_TIMEOUT_MULTIPLIER * 10L, TimeUnit.SECONDS)) {
926                 return false;
927             }
928         } catch (InterruptedException e) {
929             return false;
930         }
931         return true;
932     }
933 
934     public static void dumpWindowsOnScreen(String tag, String message)
935             throws InterruptedException {
936         waitForWindowInfos(windowInfos -> {
937             if (windowInfos.isEmpty()) {
938                 return false;
939             }
940             Log.d(tag, "Dumping windows on screen: " + message);
941             for (var windowInfo : windowInfos) {
942                 Log.d(tag, "     " + windowInfo);
943             }
944             return true;
945         }, Duration.ofSeconds(5L * HW_TIMEOUT_MULTIPLIER));
946     }
947 
948     /**
949      * Assert the condition and dump the window states if the condition fails.
950      */
951     public static void assertAndDumpWindowState(String tag, String message, boolean condition)
952             throws InterruptedException {
953         if (!condition) {
954             dumpWindowsOnScreen(tag, message);
955         }
956 
957         assertTrue(message, condition);
958     }
959 
960     /**
961      * Get the current window and display state.
962      */
963     public static Pair<List<WindowInfo>, List<DisplayInfo>> getWindowAndDisplayState()
964             throws InterruptedException {
965         var consumer =
966                 new BiConsumer<List<WindowInfo>, List<DisplayInfo>>() {
967                     private CountDownLatch mLatch = new CountDownLatch(1);
968                     private boolean mComplete = false;
969 
970                     List<WindowInfo> mWindowInfos;
971                     List<DisplayInfo> mDisplayInfos;
972 
973                     @Override
974                     public void accept(List<WindowInfo> windows, List<DisplayInfo> displays) {
975                         if (mComplete || windows.isEmpty() || displays.isEmpty()) {
976                             return;
977                         }
978                         mComplete = true;
979                         mWindowInfos = windows;
980                         mDisplayInfos = displays;
981                         mLatch.countDown();
982                     }
983 
984                     void await() throws InterruptedException {
985                         mLatch.await(5L * HW_TIMEOUT_MULTIPLIER, TimeUnit.SECONDS);
986                     }
987 
988                     Pair<List<WindowInfo>, List<DisplayInfo>> getState() {
989                         return new Pair(mWindowInfos, mDisplayInfos);
990                     }
991                 };
992 
993         var waitForState =
994                 new ThrowingRunnable() {
995                     @Override
996                     public void run() throws InterruptedException {
997                         var listener = new WindowInfosListenerForTest();
998                         try {
999                             listener.addWindowInfosListener(consumer);
1000                             consumer.await();
1001                         } finally {
1002                             listener.removeWindowInfosListener(consumer);
1003                         }
1004                     }
1005                 };
1006 
1007         var uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
1008         Set<String> shellPermissions = uiAutomation.getAdoptedShellPermissions();
1009         if (shellPermissions.isEmpty()) {
1010             SystemUtil.runWithShellPermissionIdentity(
1011                     uiAutomation, waitForState, Manifest.permission.ACCESS_SURFACE_FLINGER);
1012         } else if (shellPermissions.contains(Manifest.permission.ACCESS_SURFACE_FLINGER)) {
1013             waitForState.run();
1014         } else {
1015             throw new IllegalStateException(
1016                     "getWindowAndDisplayState called with adopted shell permissions that don't"
1017                             + " include ACCESS_SURFACE_FLINGER");
1018         }
1019 
1020         return consumer.getState();
1021     }
1022 
1023     private static String getHashCode(Object obj) {
1024         return Integer.toHexString(System.identityHashCode(obj));
1025     }
1026 }
1027