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