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