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