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.input; 18 19 import static android.server.wm.ActivityManagerTestBase.launchHomeActivityNoWait; 20 import static android.server.wm.BarTestUtils.assumeHasStatusBar; 21 import static android.server.wm.CtsWindowInfoUtils.getWindowBoundsInDisplaySpace; 22 import static android.server.wm.CtsWindowInfoUtils.waitForStableWindowGeometry; 23 import static android.server.wm.CtsWindowInfoUtils.waitForWindowInfo; 24 import static android.server.wm.UiDeviceUtils.pressUnlockButton; 25 import static android.server.wm.UiDeviceUtils.pressWakeupButton; 26 import static android.server.wm.app.Components.OverlayTestService.EXTRA_LAYOUT_PARAMS; 27 import static android.server.wm.input.WindowUntrustedTouchTest.MIN_POSITIVE_OPACITY; 28 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; 29 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 30 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 31 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; 32 import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; 33 34 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 35 36 import static junit.framework.Assert.assertFalse; 37 import static junit.framework.Assert.assertTrue; 38 39 import static org.junit.Assert.assertEquals; 40 import static org.junit.Assert.fail; 41 42 import android.app.Activity; 43 import android.app.Instrumentation; 44 import android.content.ContentResolver; 45 import android.content.Intent; 46 import android.graphics.Color; 47 import android.graphics.Point; 48 import android.graphics.Rect; 49 import android.hardware.input.InputManager; 50 import android.os.Bundle; 51 import android.os.SystemClock; 52 import android.platform.test.annotations.Presubmit; 53 import android.provider.Settings; 54 import android.server.wm.CtsWindowInfoUtils; 55 import android.server.wm.WindowManagerStateHelper; 56 import android.server.wm.app.Components; 57 import android.server.wm.settings.SettingsSession; 58 import android.util.ArraySet; 59 import android.view.Gravity; 60 import android.view.InputDevice; 61 import android.view.MotionEvent; 62 import android.view.View; 63 import android.view.WindowInsets; 64 import android.view.WindowManager; 65 import android.view.WindowMetrics; 66 import android.window.WindowInfosListenerForTest.WindowInfo; 67 68 import androidx.test.rule.ActivityTestRule; 69 70 import com.android.compatibility.common.util.CtsTouchUtils; 71 import com.android.compatibility.common.util.SystemUtil; 72 73 import org.junit.Before; 74 import org.junit.Test; 75 76 import java.util.ArrayList; 77 import java.util.Objects; 78 import java.util.Random; 79 import java.util.Set; 80 import java.util.concurrent.CompletableFuture; 81 import java.util.concurrent.ExecutorService; 82 import java.util.concurrent.Executors; 83 import java.util.concurrent.TimeUnit; 84 import java.util.concurrent.atomic.AtomicBoolean; 85 import java.util.function.BiConsumer; 86 import java.util.function.Consumer; 87 import java.util.function.Predicate; 88 89 /** 90 * Ensure moving windows and tapping is done synchronously. 91 * 92 * <p>Build/Install/Run: atest CtsWindowManagerDeviceInput:WindowInputTests 93 */ 94 @Presubmit 95 public class WindowInputTests { 96 private static final String TAG = "WindowInputTests"; 97 private final ActivityTestRule<TestActivity> mActivityRule = 98 new ActivityTestRule<>(TestActivity.class); 99 private static final int TAPPING_TARGET_WINDOW_SIZE = 100; 100 private static final int PARTIAL_OBSCURING_WINDOW_SIZE = 30; 101 102 private static final String SECOND_WINDOW_NAME = TAG + ": Second Activity Window"; 103 private static final String OVERLAY_WINDOW_NAME = TAG + ": Overlay Window"; 104 105 private static final long WINDOW_WAIT_TIMEOUT_SECONDS = 20; 106 107 private Instrumentation mInstrumentation; 108 private CtsTouchUtils mCtsTouchUtils; 109 private TestActivity mActivity; 110 private InputManager mInputManager; 111 112 private View mView; 113 private final Random mRandom = new Random(1); 114 115 private int mClickCount = 0; 116 private final long EVENT_FLAGS_WAIT_TIME = 2; 117 118 @Before setUp()119 public void setUp() throws InterruptedException { 120 pressWakeupButton(); 121 pressUnlockButton(); 122 launchHomeActivityNoWait(); 123 124 mInstrumentation = getInstrumentation(); 125 mCtsTouchUtils = new CtsTouchUtils(mInstrumentation.getTargetContext()); 126 mActivity = mActivityRule.launchActivity(null); 127 mInputManager = mActivity.getSystemService(InputManager.class); 128 mInstrumentation.waitForIdleSync(); 129 CtsWindowInfoUtils.waitForWindowOnTop(mActivity.getWindow()); 130 assertTrue("Failed to reach stable window geometry", 131 waitForStableWindowGeometry(WINDOW_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)); 132 mClickCount = 0; 133 } 134 135 /** Synchronously adds a window that is owned by the test activity. */ addActivityWindow(BiConsumer<View, WindowManager.LayoutParams> windowConfig)136 private View addActivityWindow(BiConsumer<View, WindowManager.LayoutParams> windowConfig) 137 throws Throwable { 138 // Initialize layout params with default values for the activity window 139 final var lp = new WindowManager.LayoutParams(); 140 lp.setTitle(SECOND_WINDOW_NAME); 141 lp.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN; 142 lp.width = TAPPING_TARGET_WINDOW_SIZE; 143 lp.height = TAPPING_TARGET_WINDOW_SIZE; 144 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION; 145 lp.gravity = Gravity.CENTER; 146 147 View view = new View(mActivity); 148 mActivityRule.runOnUiThread(() -> { 149 windowConfig.accept(view, lp); 150 mActivity.addWindow(view, lp); 151 }); 152 mInstrumentation.waitForIdleSync(); 153 waitForWindowOnTop(lp.getTitle().toString()); 154 return view; 155 } 156 157 /** Type alias for a configuration function. */ 158 private interface OverlayConfig extends Consumer<WindowManager.LayoutParams> {} 159 160 /** 161 * Synchronously adds an overlay window that is owned by a different UID and process by 162 * using the OverlayTestService. Returns the cleanup function to close the service 163 * and remove the overlay. 164 */ addForeignOverlayWindow(OverlayConfig overlayConfig)165 private AutoCloseable addForeignOverlayWindow(OverlayConfig overlayConfig) 166 throws InterruptedException { 167 // Initialize the layout params with default values for the overlay 168 var lp = new WindowManager.LayoutParams(); 169 lp.setTitle(OVERLAY_WINDOW_NAME); 170 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 171 lp.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN; 172 lp.width = TAPPING_TARGET_WINDOW_SIZE; 173 lp.height = TAPPING_TARGET_WINDOW_SIZE; 174 lp.gravity = Gravity.CENTER; 175 lp.setFitInsetsTypes(0); 176 177 overlayConfig.accept(lp); 178 179 final Intent intent = new Intent(); 180 intent.setComponent(Components.OVERLAY_TEST_SERVICE); 181 intent.putExtra(EXTRA_LAYOUT_PARAMS, lp); 182 mActivity.startForegroundService(intent); 183 184 mInstrumentation.waitForIdleSync(); 185 final String windowName = lp.getTitle().toString(); 186 waitForWindowOnTop(windowName); 187 return () -> { 188 mActivity.stopService(intent); 189 waitForWindowRemoved(windowName); 190 }; 191 } 192 193 @Test testMoveWindowAndTap()194 public void testMoveWindowAndTap() throws Throwable { 195 final int windowSize = 20; 196 197 // Set up window. 198 mView = addActivityWindow((view, lp) -> { 199 view.setBackgroundColor(Color.RED); 200 view.setOnClickListener((v) -> mClickCount++); 201 lp.setFitInsetsTypes( 202 WindowInsets.Type.systemBars() | WindowInsets.Type.systemGestures()); 203 lp.flags = 204 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 205 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 206 lp.width = windowSize; 207 lp.height = windowSize; 208 lp.gravity = Gravity.LEFT | Gravity.TOP; 209 }); 210 211 // The window location will be picked randomly from the selectBounds. Because the x, y of 212 // LayoutParams is the offset from the gravity edge, make sure it offsets to (0,0) in case 213 // the activity is not fullscreen, and insets system bar and window width. 214 final WindowManager wm = mActivity.getWindowManager(); 215 final WindowMetrics windowMetrics = wm.getCurrentWindowMetrics(); 216 final WindowInsets windowInsets = windowMetrics.getWindowInsets(); 217 final Rect selectBounds = new Rect(windowMetrics.getBounds()); 218 selectBounds.offsetTo(0, 0); 219 mActivityRule.runOnUiThread(() -> { 220 var lp = (WindowManager.LayoutParams) mView.getLayoutParams(); 221 var insets = windowInsets.getInsetsIgnoringVisibility(lp.getFitInsetsTypes()); 222 selectBounds.inset( 223 0, 0, insets.left + insets.right + lp.width, 224 insets.top + insets.bottom + lp.height); 225 }); 226 227 final Rect previousWindowBoundsInDisplay = Objects.requireNonNull( 228 getWindowBoundsInDisplaySpace(mView::getWindowToken)); 229 230 // Move the window to a random location in the window and attempt to tap on view multiple 231 // times. 232 final Point locationInWindow = new Point(); 233 final int totalClicks = 50; 234 for (int i = 0; i < totalClicks; i++) { 235 selectRandomLocationInWindow(selectBounds, locationInWindow); 236 mActivityRule.runOnUiThread(() -> { 237 var lp = (WindowManager.LayoutParams) mView.getLayoutParams(); 238 lp.x = locationInWindow.x; 239 lp.y = locationInWindow.y; 240 wm.updateViewLayout(mView, lp); 241 }); 242 mInstrumentation.waitForIdleSync(); 243 244 // Wait for window bounds to update. Since we are trying to avoid insets, it is 245 // difficult to calculate the exact expected bounds from the client. Instead, we just 246 // wait until the window is moved to a new position, assuming there is no animation. 247 Predicate<WindowInfo> hasUpdatedBounds = 248 windowInfo -> { 249 if (previousWindowBoundsInDisplay.equals(windowInfo.bounds)) { 250 return false; 251 } 252 previousWindowBoundsInDisplay.set(windowInfo.bounds); 253 return true; 254 }; 255 assertTrue(waitForWindowInfo(hasUpdatedBounds, WINDOW_WAIT_TIMEOUT_SECONDS, 256 TimeUnit.SECONDS, mView::getWindowToken, mView.getDisplay().getDisplayId())); 257 258 final int previousCount = mClickCount; 259 260 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 261 262 mInstrumentation.waitForIdleSync(); 263 assertEquals(previousCount + 1, mClickCount); 264 } 265 266 assertEquals(totalClicks, mClickCount); 267 } 268 selectRandomLocationInWindow(Rect bounds, Point outLocation)269 private void selectRandomLocationInWindow(Rect bounds, Point outLocation) { 270 int randomX = mRandom.nextInt(bounds.right - bounds.left) + bounds.left; 271 int randomY = mRandom.nextInt(bounds.bottom - bounds.top) + bounds.top; 272 outLocation.set(randomX, randomY); 273 } 274 275 @Test testTouchModalWindow()276 public void testTouchModalWindow() throws Throwable { 277 // Set up 2 touch modal windows, expect the last one will receive all touch events. 278 mView = addActivityWindow((view, lp) -> { 279 lp.width = 20; 280 lp.height = 20; 281 lp.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL; 282 lp.flags &= ~FLAG_NOT_TOUCH_MODAL; 283 view.setFilterTouchesWhenObscured(true); 284 view.setOnClickListener((v) -> mClickCount++); 285 }); 286 addActivityWindow((view, lp) -> { 287 lp.setTitle("Additional Window"); 288 lp.width = 20; 289 lp.height = 20; 290 lp.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL; 291 lp.flags &= ~FLAG_NOT_TOUCH_MODAL; 292 }); 293 294 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 295 assertEquals(0, mClickCount); 296 } 297 298 // If a window is obscured by another window from the same app, touches should still get 299 // delivered to the bottom window, and the FLAG_WINDOW_IS_OBSCURED should not be set. 300 @Test testFilterTouchesWhenObscuredByWindowFromSameUid()301 public void testFilterTouchesWhenObscuredByWindowFromSameUid() throws Throwable { 302 final AtomicBoolean touchReceived = new AtomicBoolean(false); 303 final CompletableFuture<Integer> eventFlags = new CompletableFuture<>(); 304 305 // Set up a touchable window. 306 mView = addActivityWindow((view, lp) -> { 307 view.setFilterTouchesWhenObscured(true); 308 view.setOnClickListener((v) -> mClickCount++); 309 view.setOnTouchListener((v, ev) -> { 310 touchReceived.set(true); 311 eventFlags.complete(ev.getFlags()); 312 return false; 313 }); 314 }); 315 316 // Set up an overlay window that is not touchable on top of the previous one. 317 addActivityWindow((view, lp) -> { 318 lp.setTitle("Overlay Window"); 319 lp.flags |= FLAG_NOT_TOUCHABLE; 320 }); 321 322 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 323 324 assertTrue(touchReceived.get()); 325 assertEquals( 326 0, 327 eventFlags.get(EVENT_FLAGS_WAIT_TIME, TimeUnit.SECONDS) 328 & MotionEvent.FLAG_WINDOW_IS_OBSCURED); 329 assertEquals(1, mClickCount); 330 } 331 332 @Test testFilterTouchesWhenObscuredByWindowFromDifferentUid()333 public void testFilterTouchesWhenObscuredByWindowFromDifferentUid() throws Throwable { 334 final AtomicBoolean touchReceived = new AtomicBoolean(false); 335 336 // Set up a touchable window (similar to before) 337 mView = addActivityWindow((view, lp) -> { 338 view.setFilterTouchesWhenObscured(true); 339 view.setOnClickListener((v) -> mClickCount++); 340 view.setOnTouchListener((v, ev) -> { 341 touchReceived.set(true); 342 return false; 343 }); 344 }); 345 346 // Launch overlapping window owned by a different app and process. 347 final OverlayConfig overlayConfig = lp -> { 348 placeWindowAtCenterOfView(mView, lp); 349 lp.flags |= FLAG_NOT_TOUCHABLE; 350 // Any opacity higher than this would make InputDispatcher block the touch 351 lp.alpha = mInputManager.getMaximumObscuringOpacityForTouch(); 352 }; 353 354 try (var overlay = addForeignOverlayWindow(overlayConfig)) { 355 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 356 357 // Touch not received due to setFilterTouchesWhenObscured(true) 358 assertFalse(touchReceived.get()); 359 assertEquals(0, mClickCount); 360 } 361 } 362 363 @Test testFlagTouchesWhenObscuredByWindowFromDifferentUid()364 public void testFlagTouchesWhenObscuredByWindowFromDifferentUid() throws Throwable { 365 final AtomicBoolean touchReceived = new AtomicBoolean(false); 366 final CompletableFuture<Integer> eventFlags = new CompletableFuture<>(); 367 368 // Set up a touchable window 369 mView = addActivityWindow((view, lp) -> { 370 view.setOnClickListener((v) -> mClickCount++); 371 view.setOnTouchListener((v, ev) -> { 372 touchReceived.set(true); 373 eventFlags.complete(ev.getFlags()); 374 return false; 375 }); 376 }); 377 378 // Set up an overlap window from service 379 final OverlayConfig overlayConfig = lp -> { 380 placeWindowAtCenterOfView(mView, lp); 381 lp.flags |= FLAG_NOT_TOUCHABLE; 382 // Any opacity higher than this would make InputDispatcher block the touch 383 lp.alpha = mInputManager.getMaximumObscuringOpacityForTouch(); 384 }; 385 386 try (var overlay = addForeignOverlayWindow(overlayConfig)) { 387 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 388 389 assertTrue(touchReceived.get()); 390 assertEquals( 391 MotionEvent.FLAG_WINDOW_IS_OBSCURED, 392 eventFlags.get(EVENT_FLAGS_WAIT_TIME, TimeUnit.SECONDS) 393 & MotionEvent.FLAG_WINDOW_IS_OBSCURED); 394 assertEquals(1, mClickCount); 395 } 396 } 397 398 @Test testDoNotFlagTouchesWhenObscuredByZeroOpacityWindow()399 public void testDoNotFlagTouchesWhenObscuredByZeroOpacityWindow() throws Throwable { 400 final AtomicBoolean touchReceived = new AtomicBoolean(false); 401 final CompletableFuture<Integer> eventFlags = new CompletableFuture<>(); 402 403 // Set up a touchable window 404 mView = addActivityWindow((view, lp) -> { 405 view.setOnClickListener((v) -> mClickCount++); 406 view.setOnTouchListener((v, ev) -> { 407 touchReceived.set(true); 408 eventFlags.complete(ev.getFlags()); 409 return false; 410 }); 411 }); 412 413 // Set up an overlay window with zero opacity 414 final OverlayConfig overlayConfig = lp -> { 415 placeWindowAtCenterOfView(mView, lp); 416 lp.flags |= FLAG_NOT_TOUCHABLE; 417 lp.alpha = 0; 418 }; 419 420 try (var overlay = addForeignOverlayWindow(overlayConfig)) { 421 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 422 423 assertTrue(touchReceived.get()); 424 assertEquals( 425 0, 426 eventFlags.get(EVENT_FLAGS_WAIT_TIME, TimeUnit.SECONDS) 427 & MotionEvent.FLAG_WINDOW_IS_OBSCURED); 428 assertEquals(1, mClickCount); 429 } 430 } 431 432 @Test testFlagTouchesWhenObscuredByMinPositiveOpacityWindow()433 public void testFlagTouchesWhenObscuredByMinPositiveOpacityWindow() throws Throwable { 434 final CompletableFuture<Integer> eventFlags = new CompletableFuture<>(); 435 final AtomicBoolean touchReceived = new AtomicBoolean(false); 436 437 // Set up a touchable window 438 mView = addActivityWindow((view, lp) -> { 439 view.setOnClickListener((v) -> mClickCount++); 440 view.setOnTouchListener((v, ev) -> { 441 touchReceived.set(true); 442 eventFlags.complete(ev.getFlags()); 443 return false; 444 }); 445 }); 446 447 // Set up an overlay window with minimum positive opacity 448 final OverlayConfig overlayConfig = lp -> { 449 placeWindowAtCenterOfView(mView, lp); 450 lp.flags |= FLAG_NOT_TOUCHABLE; 451 lp.alpha = MIN_POSITIVE_OPACITY; 452 }; 453 454 try (var overlay = addForeignOverlayWindow(overlayConfig)) { 455 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 456 457 assertTrue(touchReceived.get()); 458 assertEquals( 459 MotionEvent.FLAG_WINDOW_IS_OBSCURED, 460 eventFlags.get(EVENT_FLAGS_WAIT_TIME, TimeUnit.SECONDS) 461 & MotionEvent.FLAG_WINDOW_IS_OBSCURED); 462 assertEquals(1, mClickCount); 463 } 464 } 465 466 @Test testFlagTouchesWhenPartiallyObscuredByZeroOpacityWindow()467 public void testFlagTouchesWhenPartiallyObscuredByZeroOpacityWindow() throws Throwable { 468 final CompletableFuture<Integer> eventFlags = new CompletableFuture<>(); 469 final AtomicBoolean touchReceived = new AtomicBoolean(false); 470 471 // Set up the touchable window 472 mView = addActivityWindow((view, lp) -> { 473 view.setOnClickListener((v) -> mClickCount++); 474 view.setOnTouchListener((v, ev) -> { 475 touchReceived.set(true); 476 eventFlags.complete(ev.getFlags()); 477 return false; 478 }); 479 }); 480 481 // Partially obscuring overlay 482 // TODO(b/327663469): Should the opacity be set to zero, as suggested by the test name? 483 final OverlayConfig overlayConfig = lp -> { 484 lp.width = PARTIAL_OBSCURING_WINDOW_SIZE; 485 lp.height = PARTIAL_OBSCURING_WINDOW_SIZE; 486 placeWindowAtCenterOfView(mView, lp); 487 // Offset y-position to move it off the touch path (center) but still have it 488 // overlap with the view. 489 lp.y += PARTIAL_OBSCURING_WINDOW_SIZE; 490 }; 491 492 try (var overlay = addForeignOverlayWindow(overlayConfig)) { 493 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 494 495 assertTrue(touchReceived.get()); 496 assertEquals( 497 MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED, 498 eventFlags.get(EVENT_FLAGS_WAIT_TIME, TimeUnit.SECONDS) 499 & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED); 500 assertEquals(1, mClickCount); 501 } 502 } 503 504 @Test testDoNotFlagTouchesWhenPartiallyObscuredByNotTouchableZeroOpacityWindow()505 public void testDoNotFlagTouchesWhenPartiallyObscuredByNotTouchableZeroOpacityWindow() 506 throws Throwable { 507 final CompletableFuture<Integer> eventFlags = new CompletableFuture<>(); 508 final AtomicBoolean touchReceived = new AtomicBoolean(false); 509 510 // Set up the touchable window 511 mView = addActivityWindow((view, lp) -> { 512 view.setOnClickListener((v) -> mClickCount++); 513 view.setOnTouchListener((v, ev) -> { 514 touchReceived.set(true); 515 eventFlags.complete(ev.getFlags()); 516 return false; 517 }); 518 }); 519 520 // Partially obscuring overlay (not touchable, zero opacity) 521 final OverlayConfig overlayConfig = lp -> { 522 lp.width = PARTIAL_OBSCURING_WINDOW_SIZE; 523 lp.height = PARTIAL_OBSCURING_WINDOW_SIZE; 524 lp.flags |= FLAG_NOT_TOUCHABLE; 525 lp.alpha = 0; 526 placeWindowAtCenterOfView(mView, lp); 527 }; 528 529 try (var overlay = addForeignOverlayWindow(overlayConfig)) { 530 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 531 532 assertTrue(touchReceived.get()); 533 assertEquals(0, eventFlags.get(EVENT_FLAGS_WAIT_TIME, TimeUnit.SECONDS) & ( 534 MotionEvent.FLAG_WINDOW_IS_OBSCURED 535 | MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED)); 536 assertEquals(1, mClickCount); 537 } 538 } 539 540 @Test testTrustedOverlapWindow()541 public void testTrustedOverlapWindow() throws Throwable { 542 try (final PointerLocationSession session = new PointerLocationSession()) { 543 session.set(true); 544 PointerLocationSession.waitUntilPointerLocationShown(mActivity.getDisplayId()); 545 546 // Set up window. 547 mView = addActivityWindow((view, lp) -> { 548 view.setFilterTouchesWhenObscured(true); 549 view.setOnClickListener((v) -> mClickCount++); 550 }); 551 552 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 553 } 554 assertEquals(1, mClickCount); 555 } 556 557 @Test testWindowBecomesUnTouchable()558 public void testWindowBecomesUnTouchable() throws Throwable { 559 mView = addActivityWindow((view, lp) -> { 560 lp.width = 20; 561 lp.height = 20; 562 view.setOnClickListener((v) -> mClickCount++); 563 }); 564 565 final View overlapView = addActivityWindow((view, lp) -> { 566 lp.setTitle("Overlap Window"); 567 lp.width = 100; 568 lp.height = 100; 569 }); 570 571 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 572 assertEquals(0, mClickCount); 573 574 mActivityRule.runOnUiThread(() -> { 575 var lp = (WindowManager.LayoutParams) overlapView.getLayoutParams(); 576 lp.flags = FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE; 577 mActivity.getWindowManager().updateViewLayout(overlapView, lp); 578 }); 579 mInstrumentation.waitForIdleSync(); 580 Predicate<WindowInfo> hasInputConfigFlags = 581 windowInfo -> !windowInfo.isTouchable && !windowInfo.isFocusable; 582 assertTrue(waitForWindowInfo(hasInputConfigFlags, WINDOW_WAIT_TIMEOUT_SECONDS, 583 TimeUnit.SECONDS, overlapView::getWindowToken, 584 overlapView.getDisplay().getDisplayId())); 585 586 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 587 assertEquals(1, mClickCount); 588 } 589 590 @Test testTapInsideUntouchableWindowResultInOutsideTouches()591 public void testTapInsideUntouchableWindowResultInOutsideTouches() throws Throwable { 592 final Set<MotionEvent> events = new ArraySet<>(); 593 594 mView = addActivityWindow((view, lp) -> { 595 lp.width = 20; 596 lp.height = 20; 597 lp.flags = FLAG_NOT_TOUCHABLE | FLAG_WATCH_OUTSIDE_TOUCH; 598 view.setOnTouchListener((v, e) -> { 599 events.add(MotionEvent.obtain(e)); // Copy to avoid reused objects 600 return false; 601 }); 602 }); 603 604 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView); 605 606 assertEquals(1, events.size()); 607 MotionEvent event = events.iterator().next(); 608 assertEquals(MotionEvent.ACTION_OUTSIDE, event.getAction()); 609 } 610 611 @Test testTapOutsideUntouchableWindowResultInOutsideTouches()612 public void testTapOutsideUntouchableWindowResultInOutsideTouches() throws Throwable { 613 final Set<MotionEvent> events = new ArraySet<>(); 614 final int size = 20; 615 616 // Set up the touchable window 617 mView = addActivityWindow((view, lp) -> { 618 lp.width = size; 619 lp.height = size; 620 lp.flags = FLAG_NOT_TOUCHABLE | FLAG_WATCH_OUTSIDE_TOUCH; 621 view.setOnTouchListener((v, e) -> { 622 events.add(MotionEvent.obtain(e)); // Copy to avoid reused objects 623 return false; 624 }); 625 }); 626 627 // Tap outside the untouchable window 628 mCtsTouchUtils.emulateTapOnView(mInstrumentation, mActivityRule, mView, size + 5, size + 5); 629 630 assertEquals(1, events.size()); 631 MotionEvent event = events.iterator().next(); 632 assertEquals(MotionEvent.ACTION_OUTSIDE, event.getAction()); 633 } 634 635 @Test testInjectToStatusBar()636 public void testInjectToStatusBar() { 637 // Try to inject event to status bar. 638 assumeHasStatusBar(mActivityRule); 639 final long downTime = SystemClock.uptimeMillis(); 640 final MotionEvent eventHover = 641 MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_HOVER_MOVE, 0, 0, 0); 642 eventHover.setSource(InputDevice.SOURCE_MOUSE); 643 try { 644 mInstrumentation.sendPointerSync(eventHover); 645 fail("Not allowed to inject to windows owned by another uid from Instrumentation."); 646 } catch (RuntimeException e) { 647 // Should not be allowed to inject event to a window owned by another uid from the 648 // Instrumentation class. 649 } 650 } 651 652 @Test testInjectFromThread()653 public void testInjectFromThread() throws InterruptedException { 654 // Continually inject event to activity from thread. 655 final int[] decorViewLocation = new int[2]; 656 final View decorView = mActivity.getWindow().getDecorView(); 657 decorView.getLocationOnScreen(decorViewLocation); 658 // Tap at the center of the view. Calculate and tap at the absolute view center location on 659 // screen, so that the tapping location is always as expected regardless of windowing mode. 660 final Point testPoint = 661 new Point( 662 decorViewLocation[0] + decorView.getWidth() / 2, 663 decorViewLocation[1] + decorView.getHeight() / 2); 664 665 final long downTime = SystemClock.uptimeMillis(); 666 final MotionEvent eventDown = 667 MotionEvent.obtain( 668 downTime, 669 downTime, 670 MotionEvent.ACTION_DOWN, 671 testPoint.x, 672 testPoint.y, 673 /* metaState= */ 0); 674 mInstrumentation.sendPointerSync(eventDown); 675 676 final ExecutorService executor = Executors.newSingleThreadExecutor(); 677 boolean[] securityExceptionCaught = new boolean[1]; 678 Exception[] illegalArgumentException = new Exception[1]; 679 executor.execute( 680 () -> { 681 for (int i = 0; i < 20; i++) { 682 final long eventTime = SystemClock.uptimeMillis(); 683 final MotionEvent eventMove = 684 MotionEvent.obtain( 685 downTime, 686 eventTime, 687 MotionEvent.ACTION_MOVE, 688 testPoint.x, 689 testPoint.y, 690 /* metaState= */ 0); 691 try { 692 mInstrumentation.sendPointerSync(eventMove); 693 } catch (SecurityException e) { 694 securityExceptionCaught[0] = true; 695 return; 696 } catch (IllegalArgumentException e) { 697 // InputManagerService throws this exception when input target does not 698 // match. 699 // Store the exception, and raise test failure later to avoid cts thread 700 // crash. 701 illegalArgumentException[0] = e; 702 return; 703 } 704 } 705 }); 706 707 // Launch another activity, should not crash the process. 708 final Intent intent = new Intent(mActivity, TestActivity.class); 709 mActivityRule.launchActivity(intent); 710 mInstrumentation.waitForIdleSync(); 711 712 executor.shutdown(); 713 executor.awaitTermination(5L, TimeUnit.SECONDS); 714 715 if (securityExceptionCaught[0]) { 716 // Fail the test here instead of in the executor lambda, 717 // so the failure is thrown in the test thread. 718 fail("Should be allowed to inject event."); 719 } 720 721 if (illegalArgumentException[0] != null) { 722 fail( 723 "Failed to inject event due to input target mismatch: " 724 + illegalArgumentException[0].getMessage()); 725 } 726 } 727 waitForWindowOnTop(String name)728 private void waitForWindowOnTop(String name) throws InterruptedException { 729 assertTrue("Timed out waiting for window to be on top; window: '" + name + "'", 730 CtsWindowInfoUtils.waitForWindowOnTop(WINDOW_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS, 731 windowInfo -> windowInfo.name.contains(name))); 732 } 733 waitForWindowRemoved(String name)734 private void waitForWindowRemoved(String name) throws InterruptedException { 735 assertTrue("Timed out waiting for window to be removed; window: '" + name + "'", 736 CtsWindowInfoUtils.waitForWindowInfos( 737 windows -> windows.stream().noneMatch(window -> window.name.contains(name)), 738 WINDOW_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)); 739 } 740 741 public static class TestActivity extends Activity { 742 private ArrayList<View> mViews = new ArrayList<>(); 743 744 @Override onCreate(Bundle savedInstanceState)745 protected void onCreate(Bundle savedInstanceState) { 746 super.onCreate(savedInstanceState); 747 } 748 addWindow(View view, WindowManager.LayoutParams attrs)749 void addWindow(View view, WindowManager.LayoutParams attrs) { 750 getWindowManager().addView(view, attrs); 751 mViews.add(view); 752 } 753 removeAllWindows()754 void removeAllWindows() { 755 for (View view : mViews) { 756 getWindowManager().removeViewImmediate(view); 757 } 758 mViews.clear(); 759 } 760 761 @Override onPause()762 protected void onPause() { 763 super.onPause(); 764 removeAllWindows(); 765 } 766 } 767 768 /** 769 * Position the layout params over the center of the given view. 770 * @param view the target view that must already be attached to a window 771 * @param lp the layout params to configure, with its width and height set to positive values 772 */ placeWindowAtCenterOfView(View view, WindowManager.LayoutParams lp)773 private static void placeWindowAtCenterOfView(View view, WindowManager.LayoutParams lp) { 774 if (!view.isAttachedToWindow()) { 775 throw new IllegalArgumentException( 776 "View must be attached to window to get layout bounds"); 777 } 778 if (lp.width <= 0 || lp.height <= 0) { 779 throw new IllegalArgumentException( 780 "Window layout params must be configured to have a positive size to use this " 781 + "method"); 782 } 783 final int[] viewLocation = new int[2]; 784 view.getLocationOnScreen(viewLocation); 785 lp.x = viewLocation[0] + (view.getWidth() - lp.width) / 2; 786 lp.y = viewLocation[1] + (view.getHeight() - lp.height) / 2; 787 lp.gravity = Gravity.TOP | Gravity.LEFT; 788 } 789 790 /** Helper class to save, set, and restore pointer location preferences. */ 791 private static class PointerLocationSession extends SettingsSession<Boolean> { PointerLocationSession()792 PointerLocationSession() { 793 super( 794 Settings.System.getUriFor("pointer_location" /* POINTER_LOCATION */), 795 PointerLocationSession::get, 796 PointerLocationSession::put); 797 } 798 put(ContentResolver contentResolver, String s, boolean v)799 private static void put(ContentResolver contentResolver, String s, boolean v) { 800 SystemUtil.runShellCommand( 801 "settings put system " + "pointer_location" + " " + (v ? 1 : 0)); 802 } 803 get(ContentResolver contentResolver, String s)804 private static boolean get(ContentResolver contentResolver, String s) { 805 try { 806 return Integer.parseInt( 807 SystemUtil.runShellCommand( 808 "settings get system " + "pointer_location") 809 .trim()) 810 == 1; 811 } catch (NumberFormatException e) { 812 return false; 813 } 814 } 815 waitUntilPointerLocationShown(int displayId)816 private static void waitUntilPointerLocationShown(int displayId) { 817 final WindowManagerStateHelper wmState = new WindowManagerStateHelper(); 818 final String windowName = "PointerLocation - display " + displayId; 819 wmState.waitForWithAmState(state -> state.isWindowSurfaceShown(windowName), 820 windowName + "'s surface is appeared"); 821 } 822 } 823 } 824