• 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 android.Manifest;
20 import android.app.Instrumentation;
21 import android.app.UiAutomation;
22 import android.graphics.Point;
23 import android.graphics.Rect;
24 import android.os.IBinder;
25 import android.os.SystemClock;
26 import android.os.SystemProperties;
27 import android.util.Log;
28 import android.view.View;
29 import android.view.ViewTreeObserver;
30 import android.view.Window;
31 import android.view.Display;
32 import android.window.WindowInfosListenerForTest;
33 import android.window.WindowInfosListenerForTest.WindowInfo;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.test.platform.app.InstrumentationRegistry;
38 
39 import com.android.compatibility.common.util.CtsTouchUtils;
40 import com.android.compatibility.common.util.SystemUtil;
41 import com.android.compatibility.common.util.ThrowingRunnable;
42 
43 import org.junit.rules.TestName;
44 
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.Set;
48 import java.util.Timer;
49 import java.util.TimerTask;
50 import java.util.concurrent.CountDownLatch;
51 import java.util.concurrent.TimeUnit;
52 import java.util.concurrent.atomic.AtomicBoolean;
53 import java.util.function.Consumer;
54 import java.util.function.Predicate;
55 import java.util.function.Supplier;
56 
57 public class CtsWindowInfoUtils {
58     private static final int HW_TIMEOUT_MULTIPLIER = SystemProperties.getInt(
59             "ro.hw_timeout_multiplier", 1);
60 
61     /**
62      * Calls the provided predicate each time window information changes.
63      *
64      * <p>
65      * <strong>Note:</strong>The caller must have
66      * android.permission.ACCESS_SURFACE_FLINGER permissions.
67      * </p>
68      *
69      * @param predicate The predicate tested each time window infos change.
70      * @param timeout   The amount of time to wait for the predicate to be satisfied.
71      * @param unit      The units associated with timeout.
72      * @param uiAutomation Pass in a uiAutomation to use. If null is passed in, the default will
73      *                     be used. Passing non null is only needed if the test has a custom version
74      *                     of uiAutomtation since retrieving a uiAutomation could overwrite it.
75      * @return True if the provided predicate is true for any invocation before
76      * the timeout is reached. False otherwise.
77      */
waitForWindowInfos(@onNull Predicate<List<WindowInfo>> predicate, long timeout, @NonNull TimeUnit unit, @Nullable UiAutomation uiAutomation)78     public static boolean waitForWindowInfos(@NonNull Predicate<List<WindowInfo>> predicate,
79             long timeout, @NonNull TimeUnit unit, @Nullable UiAutomation uiAutomation)
80             throws InterruptedException {
81         var latch = new CountDownLatch(1);
82         var satisfied = new AtomicBoolean();
83 
84         Consumer<List<WindowInfo>> checkPredicate = windowInfos -> {
85             if (satisfied.get()) {
86                 return;
87             }
88             if (predicate.test(windowInfos)) {
89                 satisfied.set(true);
90                 latch.countDown();
91             }
92         };
93 
94         var waitForWindow = new ThrowingRunnable() {
95             @Override
96             public void run() throws InterruptedException {
97                 var listener = new WindowInfosListenerForTest();
98                 try {
99                     listener.addWindowInfosListener(checkPredicate);
100                     latch.await(timeout, unit);
101                 } finally {
102                     listener.removeWindowInfosListener(checkPredicate);
103                 }
104             }
105         };
106 
107         if (uiAutomation == null) {
108             uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
109         }
110         Set<String> shellPermissions = uiAutomation.getAdoptedShellPermissions();
111         if (shellPermissions.isEmpty()) {
112             SystemUtil.runWithShellPermissionIdentity(uiAutomation, waitForWindow,
113                     Manifest.permission.ACCESS_SURFACE_FLINGER);
114         } else if (shellPermissions.contains(Manifest.permission.ACCESS_SURFACE_FLINGER)) {
115             waitForWindow.run();
116         } else {
117             throw new IllegalStateException(
118                     "waitForWindowOnTop called with adopted shell permissions that don't include "
119                             + "ACCESS_SURFACE_FLINGER");
120         }
121 
122         return satisfied.get();
123     }
124 
125     /**
126      * Same as {@link #waitForWindowInfos(Predicate, long, TimeUnit, UiAutomation)}, but passes in
127      * a null uiAutomation object. This should be used in most cases unless there's a custom
128      * uiAutomation object used in the test.
129      *
130      * @param predicate The predicate tested each time window infos change.
131      * @param timeout   The amount of time to wait for the predicate to be satisfied.
132      * @param unit      The units associated with timeout.
133      * @return True if the provided predicate is true for any invocation before
134      * the timeout is reached. False otherwise.
135      */
waitForWindowInfos(@onNull Predicate<List<WindowInfo>> predicate, long timeout, @NonNull TimeUnit unit)136     public static boolean waitForWindowInfos(@NonNull Predicate<List<WindowInfo>> predicate,
137             long timeout, @NonNull TimeUnit unit) throws InterruptedException {
138         return waitForWindowInfos(predicate, timeout, unit, null /* uiAutomation */);
139     }
140 
141     /**
142      * Calls the provided predicate each time window information changes if a visible
143      * window is found that matches the supplied window token.
144      *
145      * <p>
146      * <strong>Note:</strong>The caller must have the
147      * android.permission.ACCESS_SURFACE_FLINGER permissions.
148      * </p>
149      *
150      * @param predicate           The predicate tested each time window infos change.
151      * @param timeout             The amount of time to wait for the predicate to be satisfied.
152      * @param unit                The units associated with timeout.
153      * @param windowTokenSupplier Supplies the window token for the window to
154      *                            call the predicate on. The supplier is called each time window
155      *                            info change. If the supplier returns null, the predicate is
156      *                            assumed false for the current invocation.
157      * @return True if the provided predicate is true for any invocation before the timeout is
158      * reached. False otherwise.
159      * @hide
160      */
waitForWindowInfo(@onNull Predicate<WindowInfo> predicate, long timeout, @NonNull TimeUnit unit, @NonNull Supplier<IBinder> windowTokenSupplier)161     public static boolean waitForWindowInfo(@NonNull Predicate<WindowInfo> predicate, long timeout,
162             @NonNull TimeUnit unit, @NonNull Supplier<IBinder> windowTokenSupplier)
163             throws InterruptedException {
164         Predicate<List<WindowInfo>> wrappedPredicate = windowInfos -> {
165             IBinder windowToken = windowTokenSupplier.get();
166             if (windowToken == null) {
167                 return false;
168             }
169 
170             for (var windowInfo : windowInfos) {
171                 if (!windowInfo.isVisible) {
172                     continue;
173                 }
174                 // only wait for default display.
175                 if (windowInfo.windowToken == windowToken
176                         && windowInfo.displayId == Display.DEFAULT_DISPLAY) {
177                     return predicate.test(windowInfo);
178                 }
179             }
180 
181             return false;
182         };
183         return waitForWindowInfos(wrappedPredicate, timeout, unit);
184     }
185 
186     /**
187      * Waits for the window associated with the view to be present.
188      */
waitForWindowVisible(@onNull View view)189     public static boolean waitForWindowVisible(@NonNull View view) throws InterruptedException {
190         return waitForWindowInfo(windowInfo -> true, HW_TIMEOUT_MULTIPLIER * 5L, TimeUnit.SECONDS,
191                 view::getWindowToken);
192     }
193 
waitForWindowVisible(@onNull IBinder windowToken)194     public static boolean waitForWindowVisible(@NonNull IBinder windowToken)
195             throws InterruptedException {
196         return waitForWindowInfo(windowInfo -> true, HW_TIMEOUT_MULTIPLIER * 5L, TimeUnit.SECONDS,
197                 () -> windowToken);
198     }
199 
200     /**
201      * Calls {@link CtsWindowInfoUtils#waitForWindowOnTop(int, TimeUnit, Supplier)}. Adopts
202      * required permissions and waits five seconds before timing out.
203      *
204      * @param window The window to wait on.
205      * @return True if the window satisfies the visibility requirements before the timeout is
206      * reached. False otherwise.
207      */
waitForWindowOnTop(@onNull Window window)208     public static boolean waitForWindowOnTop(@NonNull Window window) throws InterruptedException {
209         return waitForWindowOnTop(5, TimeUnit.SECONDS,
210                 () -> window.getDecorView().getWindowToken());
211     }
212 
213     /**
214      * Waits until the window specified by the predicate is present, not occluded, and hasn't
215      * had geometry changes for 200ms.
216      *
217      * The window is considered occluded if any part of another window is above it, excluding
218      * trusted overlays.
219      *
220      * <p>
221      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
222      * android.permission.ACCESS_SURFACE_FLINGER.
223      * </p>
224      *
225      * @param timeout             The amount of time to wait for the window to be visible.
226      * @param unit                The units associated with timeout.
227      * @param windowTokenSupplier Supplies the window token for the window to wait on. The
228      *                            supplier is called each time window infos change. If the
229      *                            supplier returns null, the window is assumed not visible
230      *                            yet.
231      * @return True if the window satisfies the visibility requirements before the timeout is
232      * reached. False otherwise.
233      */
waitForWindowOnTop(int timeout, @NonNull TimeUnit unit, @NonNull Predicate<WindowInfo> predicate)234     public static boolean waitForWindowOnTop(int timeout, @NonNull TimeUnit unit,
235                                              @NonNull Predicate<WindowInfo> predicate)
236             throws InterruptedException {
237         var latch = new CountDownLatch(1);
238         var satisfied = new AtomicBoolean();
239 
240         var windowNotOccluded = new Consumer<List<WindowInfo>>() {
241             private Timer mTimer = new Timer();
242             private TimerTask mTask = null;
243             private Rect mPreviousBounds = new Rect(0, 0, -1, -1);
244 
245             private void resetState() {
246                 if (mTask != null) {
247                     mTask.cancel();
248                     mTask = null;
249                 }
250                 mPreviousBounds.set(0, 0, -1, -1);
251             }
252 
253             @Override
254             public void accept(List<WindowInfo> windowInfos) {
255                 if (satisfied.get()) {
256                     return;
257                 }
258 
259                 WindowInfo targetWindowInfo = null;
260                 ArrayList<WindowInfo> aboveWindowInfos = new ArrayList<>();
261                 for (var windowInfo : windowInfos) {
262                     if (predicate.test(windowInfo)) {
263                         targetWindowInfo = windowInfo;
264                         break;
265                     }
266                     if (windowInfo.isTrustedOverlay || !windowInfo.isVisible) {
267                         continue;
268                     }
269                     aboveWindowInfos.add(windowInfo);
270                 }
271 
272                 if (targetWindowInfo == null) {
273                     // The window isn't present. If we have an active timer, we need to cancel it
274                     // as it's possible the window was previously present and has since disappeared.
275                     resetState();
276                     return;
277                 }
278 
279                 for (var windowInfo : aboveWindowInfos) {
280                     if (targetWindowInfo.displayId == windowInfo.displayId
281                             && Rect.intersects(targetWindowInfo.bounds, windowInfo.bounds)) {
282                         // The window is occluded. If we have an active timer, we need to cancel it
283                         // as it's possible the window was previously not occluded and now is
284                         // occluded.
285                         resetState();
286                         return;
287                     }
288                 }
289 
290                 if (targetWindowInfo.bounds.equals(mPreviousBounds)) {
291                     // The window matches previously found bounds. Let the active timer continue.
292                     return;
293                 }
294 
295                 // The window is present and not occluded but has different bounds than
296                 // previously seen or this is the first time we've detected the window. If
297                 // there's an active timer, cancel it. Schedule a task to toggle the latch in 200ms.
298                 resetState();
299                 mPreviousBounds.set(targetWindowInfo.bounds);
300                 mTask = new TimerTask() {
301                     @Override
302                     public void run() {
303                         satisfied.set(true);
304                         latch.countDown();
305                     }
306                 };
307                 mTimer.schedule(mTask, 200 * HW_TIMEOUT_MULTIPLIER);
308             }
309         };
310 
311         var waitForWindow = new ThrowingRunnable() {
312             @Override
313             public void run() throws InterruptedException {
314                 var listener = new WindowInfosListenerForTest();
315                 try {
316                     listener.addWindowInfosListener(windowNotOccluded);
317                     latch.await(timeout, unit);
318                 } finally {
319                     listener.removeWindowInfosListener(windowNotOccluded);
320                 }
321             }
322         };
323 
324         Set<String> shellPermissions =
325                 InstrumentationRegistry.getInstrumentation().getUiAutomation()
326                         .getAdoptedShellPermissions();
327         if (shellPermissions.isEmpty()) {
328             SystemUtil.runWithShellPermissionIdentity(waitForWindow,
329                     Manifest.permission.ACCESS_SURFACE_FLINGER);
330         } else if (shellPermissions.contains(Manifest.permission.ACCESS_SURFACE_FLINGER)) {
331             waitForWindow.run();
332         } else {
333             throw new IllegalStateException(
334                     "waitForWindowOnTop called with adopted shell permissions that don't include "
335                             + "ACCESS_SURFACE_FLINGER");
336         }
337 
338         return satisfied.get();
339     }
340 
341     /**
342      * Waits until the window specified by windowTokenSupplier is present, not occluded, and hasn't
343      * had geometry changes for 200ms.
344      *
345      * The window is considered occluded if any part of another window is above it, excluding
346      * trusted overlays.
347      *
348      * <p>
349      * <strong>Note:</strong>If the caller has any adopted shell permissions, they must include
350      * android.permission.ACCESS_SURFACE_FLINGER.
351      * </p>
352      *
353      * @param timeout             The amount of time to wait for the window to be visible.
354      * @param unit                The units associated with timeout.
355      * @param windowTokenSupplier Supplies the window token for the window to wait on. The
356      *                            supplier is called each time window infos change. If the
357      *                            supplier returns null, the window is assumed not visible
358      *                            yet.
359      * @return True if the window satisfies the visibility requirements before the timeout is
360      * reached. False otherwise.
361      */
waitForWindowOnTop(int timeout, @NonNull TimeUnit unit, @NonNull Supplier<IBinder> windowTokenSupplier)362     public static boolean waitForWindowOnTop(int timeout, @NonNull TimeUnit unit,
363             @NonNull Supplier<IBinder> windowTokenSupplier)
364             throws InterruptedException {
365         return waitForWindowOnTop(timeout, unit, windowInfo -> {
366             IBinder windowToken = windowTokenSupplier.get();
367             return windowToken != null && windowInfo.windowToken == windowToken;
368         });
369     }
370 
371     /**
372      * Tap on the center coordinates of the specified window.
373      * </p>
374      * @param instrumentation Instrumentation object to use for tap.
375      * @param windowTokenSupplier Supplies the window token for the window to wait on. The
376      *                            supplier is called each time window infos change. If the
377      *                            supplier returns null, the window is assumed not visible
378      *                            yet.
379      * @return true if successfully tapped on the coordinates, false otherwise.
380      *
381      * @throws InterruptedException if failed to wait for WindowInfo
382      */
tapOnWindowCenter(Instrumentation instrumentation, @NonNull Supplier<IBinder> windowTokenSupplier)383     public static boolean tapOnWindowCenter(Instrumentation instrumentation,
384             @NonNull Supplier<IBinder> windowTokenSupplier) throws InterruptedException {
385         Rect bounds = getWindowBounds(windowTokenSupplier);
386         if (bounds == null) {
387             return false;
388         }
389 
390         final Point coord = new Point(bounds.left + bounds.width() / 2,
391                 bounds.top + bounds.height() / 2);
392         sendTap(instrumentation, coord);
393         return true;
394     }
395 
396     /**
397      * Tap on the coordinates of the specified window, offset by the value passed in.
398      * </p>
399      * @param instrumentation Instrumentation object to use for tap.
400      * @param windowTokenSupplier Supplies the window token for the window to wait on. The
401      *                            supplier is called each time window infos change. If the
402      *                            supplier returns null, the window is assumed not visible
403      *                            yet.
404      * @param offset The offset from 0,0 of the window to tap on. If null, it will be ignored and
405      *               0,0 will be tapped.
406      * @return true if successfully tapped on the coordinates, false otherwise.
407      * @throws InterruptedException if failed to wait for WindowInfo
408      */
tapOnWindow(Instrumentation instrumentation, @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point offset)409     public static boolean tapOnWindow(Instrumentation instrumentation,
410             @NonNull Supplier<IBinder> windowTokenSupplier, @Nullable Point offset)
411             throws InterruptedException {
412         Rect bounds = getWindowBounds(windowTokenSupplier);
413         if (bounds == null) {
414             return false;
415         }
416 
417         final Point coord = new Point(bounds.left + (offset != null ? offset.x : 0),
418                 bounds.top + (offset != null ? offset.y : 0));
419         sendTap(instrumentation, coord);
420         return true;
421     }
422 
getWindowBounds(@onNull Supplier<IBinder> windowTokenSupplier)423     private static Rect getWindowBounds(@NonNull Supplier<IBinder> windowTokenSupplier)
424             throws InterruptedException {
425         Rect bounds = new Rect();
426         Predicate<WindowInfo> predicate = windowInfo -> {
427             if (!windowInfo.bounds.isEmpty()) {
428                 bounds.set(windowInfo.bounds);
429                 return true;
430             }
431             return false;
432         };
433 
434         if (!waitForWindowInfo(predicate, 5, TimeUnit.SECONDS, windowTokenSupplier)) {
435             return null;
436         }
437         return bounds;
438     }
439 
sendTap(Instrumentation instrumentation, Point coord)440     private static void sendTap(Instrumentation instrumentation, Point coord) {
441         // Get anchor coordinates on the screen
442         final long downTime = SystemClock.uptimeMillis();
443 
444         UiAutomation uiAutomation = instrumentation.getUiAutomation();
445         CtsTouchUtils ctsTouchUtils = new CtsTouchUtils(instrumentation.getTargetContext());
446         ctsTouchUtils.injectDownEvent(uiAutomation, downTime, coord.x, coord.y, true, null);
447         ctsTouchUtils.injectUpEvent(uiAutomation, downTime, false, coord.x, coord.y,
448                 true, null);
449 
450         instrumentation.waitForIdleSync();
451     }
452 
waitForWindowFocus(final View view, boolean hasWindowFocus)453     public static boolean waitForWindowFocus(final View view, boolean hasWindowFocus) {
454         final CountDownLatch latch = new CountDownLatch(1);
455 
456         view.getHandler().post(() -> {
457             if (view.hasWindowFocus() == hasWindowFocus) {
458                 latch.countDown();
459                 return;
460             }
461             view.getViewTreeObserver().addOnWindowFocusChangeListener(
462                     new ViewTreeObserver.OnWindowFocusChangeListener() {
463                         @Override
464                         public void onWindowFocusChanged(boolean newFocusState) {
465                             if (hasWindowFocus == newFocusState) {
466                                 view.getViewTreeObserver()
467                                         .removeOnWindowFocusChangeListener(this);
468                                 latch.countDown();
469                             }
470                         }
471                     });
472 
473             view.invalidate();
474         });
475 
476         try {
477             if (!latch.await(HW_TIMEOUT_MULTIPLIER * 10L, TimeUnit.SECONDS)) {
478                 return false;
479             }
480         } catch (InterruptedException e) {
481             return false;
482         }
483         return true;
484     }
485 
dumpWindowsOnScreen(String tag, TestName testName)486     public static void dumpWindowsOnScreen(String tag, TestName testName)
487             throws InterruptedException {
488         waitForWindowInfos(windowInfos -> {
489             if (windowInfos.size() == 0) {
490                 return false;
491             }
492             Log.d(tag, "Dumping windows on screen for test " + testName.getMethodName());
493             for (var windowInfo : windowInfos) {
494                 Log.d(tag, "     " + windowInfo);
495             }
496             return true;
497         }, 5L * HW_TIMEOUT_MULTIPLIER, TimeUnit.SECONDS);
498     }
499 }
500