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