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