1 /*
2  * Copyright (C) 2018 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.content.pm.PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS;
20 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
21 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
22 import static android.view.Display.DEFAULT_DISPLAY;
23 import static android.view.Display.INVALID_DISPLAY;
24 import static android.view.KeyEvent.ACTION_DOWN;
25 import static android.view.KeyEvent.ACTION_UP;
26 import static android.view.KeyEvent.FLAG_CANCELED;
27 import static android.view.KeyEvent.KEYCODE_0;
28 import static android.view.KeyEvent.KEYCODE_1;
29 import static android.view.KeyEvent.KEYCODE_2;
30 import static android.view.KeyEvent.KEYCODE_3;
31 import static android.view.KeyEvent.KEYCODE_4;
32 import static android.view.KeyEvent.KEYCODE_5;
33 import static android.view.KeyEvent.KEYCODE_6;
34 import static android.view.KeyEvent.KEYCODE_7;
35 import static android.view.KeyEvent.KEYCODE_8;
36 
37 import static androidx.test.InstrumentationRegistry.getInstrumentation;
38 
39 import static org.junit.Assert.assertEquals;
40 import static org.junit.Assert.assertFalse;
41 import static org.junit.Assert.assertNotNull;
42 import static org.junit.Assume.assumeTrue;
43 import static org.junit.Assume.assumeFalse;
44 
45 import android.content.Context;
46 import android.content.res.Configuration;
47 import android.graphics.Canvas;
48 import android.graphics.PixelFormat;
49 import android.graphics.Point;
50 import android.hardware.display.DisplayManager;
51 import android.hardware.display.VirtualDisplay;
52 import android.media.ImageReader;
53 import android.os.SystemClock;
54 import android.platform.test.annotations.Presubmit;
55 import android.view.Display;
56 import android.view.KeyEvent;
57 import android.view.MotionEvent;
58 import android.view.View;
59 import android.view.WindowManager.LayoutParams;
60 
61 import androidx.test.filters.FlakyTest;
62 
63 import com.android.compatibility.common.util.SystemUtil;
64 
65 import org.junit.Test;
66 
67 import java.util.ArrayList;
68 
69 import javax.annotation.concurrent.GuardedBy;
70 
71 /**
72  * Ensure window focus assignment is executed as expected.
73  *
74  * Build/Install/Run:
75  *     atest WindowFocusTests
76  */
77 @Presubmit
78 public class WindowFocusTests extends WindowManagerTestBase {
79 
sendKey(int action, int keyCode, int displayId)80     private static void sendKey(int action, int keyCode, int displayId) {
81         final KeyEvent keyEvent = new KeyEvent(action, keyCode);
82         keyEvent.setDisplayId(displayId);
83         SystemUtil.runWithShellPermissionIdentity(() -> {
84             getInstrumentation().sendKeySync(keyEvent);
85         });
86     }
87 
sendAndAssertTargetConsumedKey(InputTargetActivity target, int keyCode, int targetDisplayId)88     private static void sendAndAssertTargetConsumedKey(InputTargetActivity target, int keyCode,
89             int targetDisplayId) {
90         sendAndAssertTargetConsumedKey(target, ACTION_DOWN, keyCode, targetDisplayId);
91         sendAndAssertTargetConsumedKey(target, ACTION_UP, keyCode, targetDisplayId);
92     }
93 
sendAndAssertTargetConsumedKey(InputTargetActivity target, int action, int keyCode, int targetDisplayId)94     private static void sendAndAssertTargetConsumedKey(InputTargetActivity target, int action,
95             int keyCode, int targetDisplayId) {
96         final int eventCount = target.getKeyEventCount();
97         sendKey(action, keyCode, targetDisplayId);
98         target.assertAndConsumeKeyEvent(action, keyCode, 0 /* flags */);
99         assertEquals(target.getLogTag() + " must only receive key event sent.", eventCount,
100                 target.getKeyEventCount());
101     }
102 
tapOnCenterOfDisplay(int displayId)103     private static void tapOnCenterOfDisplay(int displayId) {
104         final Point point = new Point();
105         getInstrumentation().getTargetContext()
106                 .getSystemService(DisplayManager.class)
107                 .getDisplay(displayId)
108                 .getSize(point);
109         final int x = point.x / 2;
110         final int y = point.y / 2;
111         final long downTime = SystemClock.elapsedRealtime();
112         final MotionEvent downEvent = MotionEvent.obtain(downTime, downTime,
113                 MotionEvent.ACTION_DOWN, x, y, 0 /* metaState */);
114         downEvent.setDisplayId(displayId);
115         getInstrumentation().sendPointerSync(downEvent);
116         final MotionEvent upEvent = MotionEvent.obtain(downTime, SystemClock.elapsedRealtime(),
117                 MotionEvent.ACTION_UP, x, y, 0 /* metaState */);
118         upEvent.setDisplayId(displayId);
119         getInstrumentation().sendPointerSync(upEvent);
120     }
121 
122     /** Checks if the device supports multi-display. */
supportsMultiDisplay()123     private static boolean supportsMultiDisplay() {
124         return getInstrumentation().getTargetContext().getPackageManager()
125                 .hasSystemFeature(FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS);
126     }
127 
128     /** Checks if per-display-focus is enabled in the device. */
perDisplayFocusEnabled()129     private static boolean perDisplayFocusEnabled() {
130         return getInstrumentation().getTargetContext().getResources()
131                     .getBoolean(android.R.bool.config_perDisplayFocusEnabled);
132     }
133 
134     /**
135      * Test the following conditions:
136      * - Each display can have a focused window at the same time.
137      * - Focused windows can receive display-specified key events.
138      * - The top focused window can receive display-unspecified key events.
139      * - Taping on a display will make the focused window on it become top-focused.
140      * - The window which lost top-focus can receive display-unspecified cancel events.
141      */
142     @Test
143     @FlakyTest(bugId = 131005232)
testKeyReceiving()144     public void testKeyReceiving() throws InterruptedException {
145         final PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class,
146                 DEFAULT_DISPLAY);
147         sendAndAssertTargetConsumedKey(primaryActivity, KEYCODE_0, INVALID_DISPLAY);
148         sendAndAssertTargetConsumedKey(primaryActivity, KEYCODE_1, DEFAULT_DISPLAY);
149 
150         assumeTrue(supportsMultiDisplay());
151         // If config_perDisplayFocusEnabled, tapping on a display will not move the focus.
152         assumeFalse(perDisplayFocusEnabled());
153         try (VirtualDisplaySession displaySession = new VirtualDisplaySession()) {
154             final int secondaryDisplayId = displaySession.createDisplay(
155                     getInstrumentation().getTargetContext()).getDisplayId();
156             final SecondaryActivity secondaryActivity =
157                     startActivity(SecondaryActivity.class, secondaryDisplayId);
158             sendAndAssertTargetConsumedKey(secondaryActivity, KEYCODE_2, INVALID_DISPLAY);
159             sendAndAssertTargetConsumedKey(secondaryActivity, KEYCODE_3, secondaryDisplayId);
160 
161             primaryActivity.waitAndAssertWindowFocusState(false /* hasFocus */);
162 
163             // Press display-unspecified keys and a display-specified key but not release them.
164             sendKey(ACTION_DOWN, KEYCODE_5, INVALID_DISPLAY);
165             sendKey(ACTION_DOWN, KEYCODE_6, secondaryDisplayId);
166             sendKey(ACTION_DOWN, KEYCODE_7, INVALID_DISPLAY);
167             secondaryActivity.assertAndConsumeKeyEvent(ACTION_DOWN, KEYCODE_5, 0 /* flags */);
168             secondaryActivity.assertAndConsumeKeyEvent(ACTION_DOWN, KEYCODE_6, 0 /* flags */);
169             secondaryActivity.assertAndConsumeKeyEvent(ACTION_DOWN, KEYCODE_7, 0 /* flags */);
170 
171             tapOnCenterOfDisplay(DEFAULT_DISPLAY);
172 
173             // Assert only display-unspecified key would be cancelled after secondary activity is
174             // not top focused if per-display focus is enabled. Otherwise, assert all non-released
175             // key events sent to secondary activity would be cancelled.
176             secondaryActivity.waitAssertAndConsumeKeyEvent(ACTION_UP, KEYCODE_5, FLAG_CANCELED);
177             secondaryActivity.waitAssertAndConsumeKeyEvent(ACTION_UP, KEYCODE_7, FLAG_CANCELED);
178             secondaryActivity.waitAssertAndConsumeKeyEvent(ACTION_UP, KEYCODE_6, FLAG_CANCELED);
179             assertEquals(secondaryActivity.getLogTag() + " must only receive expected events.",
180                     0 /* expected event count */, secondaryActivity.getKeyEventCount());
181 
182             // Assert primary activity become top focused after tapping on default display.
183             sendAndAssertTargetConsumedKey(primaryActivity, KEYCODE_8, INVALID_DISPLAY);
184         }
185     }
186 
187     /**
188      * Test if a display targeted by a key event can be moved to top in a single-focus system.
189      */
190     @Test
191     @FlakyTest(bugId = 131005232)
testMovingDisplayToTopByKeyEvent()192     public void testMovingDisplayToTopByKeyEvent() throws InterruptedException {
193         assumeTrue(supportsMultiDisplay());
194         assumeFalse(perDisplayFocusEnabled());
195 
196         final PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class,
197                 DEFAULT_DISPLAY);
198 
199         try (VirtualDisplaySession displaySession = new VirtualDisplaySession()) {
200             final int secondaryDisplayId = displaySession.createDisplay(
201                     getInstrumentation().getTargetContext()).getDisplayId();
202             final SecondaryActivity secondaryActivity =
203                     startActivity(SecondaryActivity.class, secondaryDisplayId);
204 
205             sendAndAssertTargetConsumedKey(primaryActivity, KEYCODE_0, DEFAULT_DISPLAY);
206             sendAndAssertTargetConsumedKey(primaryActivity, KEYCODE_1, INVALID_DISPLAY);
207 
208             sendAndAssertTargetConsumedKey(secondaryActivity, KEYCODE_2, secondaryDisplayId);
209             sendAndAssertTargetConsumedKey(secondaryActivity, KEYCODE_3, INVALID_DISPLAY);
210         }
211     }
212 
213     /**
214      * Test if the client is notified about window-focus lost after the new focused window is drawn.
215      */
216     @Test
testDelayLosingFocus()217     public void testDelayLosingFocus() throws InterruptedException {
218         final LosingFocusActivity activity = startActivity(LosingFocusActivity.class,
219                 DEFAULT_DISPLAY);
220 
221         getInstrumentation().runOnMainSync(activity::addChildWindow);
222         activity.waitAndAssertWindowFocusState(false /* hasFocus */);
223         assertFalse("Activity must lose window focus after new focused window is drawn.",
224                 activity.losesFocusWhenNewFocusIsNotDrawn());
225     }
226 
227 
228     /**
229      * Test the following conditions:
230      * - Only the top focused window can have pointer capture.
231      * - The window which lost top-focus can be notified about pointer-capture lost.
232      */
233     @Test
234     @FlakyTest(bugId = 135574991)
testPointerCapture()235     public void testPointerCapture() throws InterruptedException {
236         final PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class,
237                 DEFAULT_DISPLAY);
238 
239         // Assert primary activity can have pointer capture before we have multiple focused windows.
240         getInstrumentation().runOnMainSync(primaryActivity::requestPointerCapture);
241         primaryActivity.waitAndAssertPointerCaptureState(true /* hasCapture */);
242 
243         assumeTrue(supportsMultiDisplay());
244         assumeFalse(perDisplayFocusEnabled());
245         try (VirtualDisplaySession displaySession = new VirtualDisplaySession()) {
246             final int secondaryDisplayId = displaySession.createDisplay(
247                     getInstrumentation().getTargetContext()).getDisplayId();
248             final SecondaryActivity secondaryActivity =
249                     startActivity(SecondaryActivity.class, secondaryDisplayId);
250 
251             // Assert primary activity lost pointer capture when it is not top focused.
252             primaryActivity.waitAndAssertPointerCaptureState(false /* hasCapture */);
253 
254             // Assert secondary activity can have pointer capture when it is top focused.
255             getInstrumentation().runOnMainSync(secondaryActivity::requestPointerCapture);
256             secondaryActivity.waitAndAssertPointerCaptureState(true /* hasCapture */);
257 
258             tapOnCenterOfDisplay(DEFAULT_DISPLAY);
259 
260             // Assert secondary activity lost pointer capture when it is not top focused.
261             secondaryActivity.waitAndAssertPointerCaptureState(false /* hasCapture */);
262         }
263     }
264 
265     /**
266      * Test if the focused window can still have focus after it is moved to another display.
267      */
268     @Test
testDisplayChanged()269     public void testDisplayChanged() throws InterruptedException {
270         assumeTrue(supportsMultiDisplay());
271 
272         final PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class,
273                 DEFAULT_DISPLAY);
274 
275         final SecondaryActivity secondaryActivity;
276         try (VirtualDisplaySession displaySession = new VirtualDisplaySession()) {
277             final int secondaryDisplayId = displaySession.createDisplay(
278                     getInstrumentation().getTargetContext()).getDisplayId();
279             secondaryActivity = startActivity(SecondaryActivity.class, secondaryDisplayId);
280         }
281         // Secondary display disconnected.
282 
283         assertNotNull("SecondaryActivity must be started.", secondaryActivity);
284         secondaryActivity.waitAndAssertDisplayId(DEFAULT_DISPLAY);
285         secondaryActivity.waitAndAssertWindowFocusState(true /* hasFocus */);
286 
287         primaryActivity.waitAndAssertWindowFocusState(false /* hasFocus */);
288     }
289 
290     /**
291      * Ensure that a non focused display becomes focused when tapping on a focusable window on
292      * that display.
293      */
294     @Test
testTapFocusableWindow()295     public void testTapFocusableWindow() throws InterruptedException {
296         assumeTrue(supportsMultiDisplay());
297         assumeFalse(perDisplayFocusEnabled());
298 
299         PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class, DEFAULT_DISPLAY);
300 
301         try (VirtualDisplaySession displaySession = new VirtualDisplaySession()) {
302             final int secondaryDisplayId = displaySession.createDisplay(
303                     getInstrumentation().getTargetContext()).getDisplayId();
304             SecondaryActivity secondaryActivity = startActivity(SecondaryActivity.class,
305                     secondaryDisplayId);
306 
307             tapOnCenterOfDisplay(DEFAULT_DISPLAY);
308             // Ensure primary activity got focus
309             primaryActivity.waitAndAssertWindowFocusState(true);
310             secondaryActivity.waitAndAssertWindowFocusState(false);
311         }
312     }
313 
314     /**
315      * Ensure that a non focused display does not become focused when tapping on a non-focusable
316      * window on that display.
317      */
318     @Test
319     @FlakyTest(bugId = 130467737)
testTapNonFocusableWindow()320     public void testTapNonFocusableWindow() throws InterruptedException {
321         assumeTrue(supportsMultiDisplay());
322         assumeFalse(perDisplayFocusEnabled());
323 
324         PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class, DEFAULT_DISPLAY);
325 
326         try (VirtualDisplaySession displaySession = new VirtualDisplaySession()) {
327             final int secondaryDisplayId = displaySession.createDisplay(
328                     getInstrumentation().getTargetContext()).getDisplayId();
329             SecondaryActivity secondaryActivity = startActivity(SecondaryActivity.class,
330                     secondaryDisplayId);
331 
332             // Tap on a window that can't be focused and ensure that the other window in that
333             // display, primaryActivity's window, doesn't get focus.
334             getInstrumentation().runOnMainSync(() -> {
335                 View view = new View(primaryActivity);
336                 LayoutParams p = new LayoutParams();
337                 p.flags = LayoutParams.FLAG_NOT_FOCUSABLE;
338                 primaryActivity.getWindowManager().addView(view, p);
339             });
340             getInstrumentation().waitForIdleSync();
341 
342             tapOnCenterOfDisplay(DEFAULT_DISPLAY);
343             // Ensure secondary activity still has focus
344             secondaryActivity.waitAndAssertWindowFocusState(true);
345             primaryActivity.waitAndAssertWindowFocusState(false);
346         }
347     }
348 
349     private static class InputTargetActivity extends FocusableActivity {
350         private static final long TIMEOUT_DISPLAY_CHANGED = 1000; // milliseconds
351         private static final long TIMEOUT_POINTER_CAPTURE_CHANGED = 1000;
352         private static final long TIMEOUT_NEXT_KEY_EVENT = 1000;
353 
354         private final Object mLockPointerCapture = new Object();
355         private final Object mLockKeyEvent = new Object();
356 
357         @GuardedBy("this")
358         private int mDisplayId = INVALID_DISPLAY;
359         @GuardedBy("mLockPointerCapture")
360         private boolean mHasPointerCapture;
361         @GuardedBy("mLockKeyEvent")
362         private ArrayList<KeyEvent> mKeyEventList = new ArrayList<>();
363 
364         @Override
onAttachedToWindow()365         public void onAttachedToWindow() {
366             synchronized (this) {
367                 mDisplayId = getWindow().getDecorView().getDisplay().getDisplayId();
368                 notify();
369             }
370         }
371 
372         @Override
onMovedToDisplay(int displayId, Configuration config)373         public void onMovedToDisplay(int displayId, Configuration config) {
374             synchronized (this) {
375                 mDisplayId = displayId;
376                 notify();
377             }
378         }
379 
waitAndAssertDisplayId(int displayId)380         void waitAndAssertDisplayId(int displayId) throws InterruptedException {
381             synchronized (this) {
382                 if (mDisplayId != displayId) {
383                     wait(TIMEOUT_DISPLAY_CHANGED);
384                 }
385                 assertEquals(getLogTag() + " must be moved to the display.",
386                         displayId, mDisplayId);
387             }
388         }
389 
390         @Override
onPointerCaptureChanged(boolean hasCapture)391         public void onPointerCaptureChanged(boolean hasCapture) {
392             synchronized (mLockPointerCapture) {
393                 mHasPointerCapture = hasCapture;
394                 mLockPointerCapture.notify();
395             }
396         }
397 
waitAndAssertPointerCaptureState(boolean hasCapture)398         void waitAndAssertPointerCaptureState(boolean hasCapture) throws InterruptedException {
399             synchronized (mLockPointerCapture) {
400                 if (mHasPointerCapture != hasCapture) {
401                     mLockPointerCapture.wait(TIMEOUT_POINTER_CAPTURE_CHANGED);
402                 }
403                 assertEquals(getLogTag() + " must" + (hasCapture ? "" : " not")
404                         + " have pointer capture.", hasCapture, mHasPointerCapture);
405             }
406         }
407 
408         // Should be only called from the main thread.
requestPointerCapture()409         void requestPointerCapture() {
410             getWindow().getDecorView().requestPointerCapture();
411         }
412 
413         @Override
dispatchKeyEvent(KeyEvent event)414         public boolean dispatchKeyEvent(KeyEvent event) {
415             synchronized (mLockKeyEvent) {
416                 mKeyEventList.add(event);
417                 mLockKeyEvent.notify();
418             }
419             return super.dispatchKeyEvent(event);
420         }
421 
getKeyEventCount()422         int getKeyEventCount() {
423             synchronized (mLockKeyEvent) {
424                 return mKeyEventList.size();
425             }
426         }
427 
consumeKeyEvent(int action, int keyCode, int flags)428         private KeyEvent consumeKeyEvent(int action, int keyCode, int flags) {
429             synchronized (mLockKeyEvent) {
430                 for (int i = mKeyEventList.size() - 1; i >= 0; i--) {
431                     final KeyEvent event = mKeyEventList.get(i);
432                     if (event.getAction() == action && event.getKeyCode() == keyCode
433                             && (event.getFlags() & flags) == flags) {
434                         mKeyEventList.remove(event);
435                         return event;
436                     }
437                 }
438             }
439             return null;
440         }
441 
assertAndConsumeKeyEvent(int action, int keyCode, int flags)442         void assertAndConsumeKeyEvent(int action, int keyCode, int flags) {
443             assertNotNull(getLogTag() + " must receive key event.",
444                     consumeKeyEvent(action, keyCode, flags));
445         }
446 
waitAssertAndConsumeKeyEvent(int action, int keyCode, int flags)447         void waitAssertAndConsumeKeyEvent(int action, int keyCode, int flags)
448                 throws InterruptedException {
449             if (consumeKeyEvent(action, keyCode, flags) == null) {
450                 synchronized (mLockKeyEvent) {
451                     mLockKeyEvent.wait(TIMEOUT_NEXT_KEY_EVENT);
452                 }
453                 assertAndConsumeKeyEvent(action, keyCode, flags);
454             }
455         }
456     }
457 
458     public static class PrimaryActivity extends InputTargetActivity { }
459 
460     public static class SecondaryActivity extends InputTargetActivity { }
461 
462     public static class LosingFocusActivity extends InputTargetActivity {
463         private boolean mChildWindowHasDrawn = false;
464 
465         @GuardedBy("this")
466         private boolean mLosesFocusWhenNewFocusIsNotDrawn = false;
467 
addChildWindow()468         void addChildWindow() {
469             getWindowManager().addView(new View(this) {
470                 @Override
471                 protected void onDraw(Canvas canvas) {
472                     mChildWindowHasDrawn = true;
473                 }
474             }, new LayoutParams());
475         }
476 
477         @Override
onWindowFocusChanged(boolean hasFocus)478         public void onWindowFocusChanged(boolean hasFocus) {
479             if (!hasFocus && !mChildWindowHasDrawn) {
480                 synchronized (this) {
481                     mLosesFocusWhenNewFocusIsNotDrawn = true;
482                 }
483             }
484             super.onWindowFocusChanged(hasFocus);
485         }
486 
losesFocusWhenNewFocusIsNotDrawn()487         boolean losesFocusWhenNewFocusIsNotDrawn() {
488             synchronized (this) {
489                 return mLosesFocusWhenNewFocusIsNotDrawn;
490             }
491         }
492     }
493 
494     private static class VirtualDisplaySession implements AutoCloseable {
495         private static final int WIDTH = 800;
496         private static final int HEIGHT = 480;
497         private static final int DENSITY = 160;
498 
499         private VirtualDisplay mVirtualDisplay;
500         private ImageReader mReader;
501 
createDisplay(Context context)502         Display createDisplay(Context context) {
503             if (mReader != null) {
504                 throw new IllegalStateException(
505                         "Only one display can be created during this session.");
506             }
507             mReader = ImageReader.newInstance(WIDTH, HEIGHT, PixelFormat.RGBA_8888,
508                     2 /* maxImages */);
509             mVirtualDisplay = context.getSystemService(DisplayManager.class).createVirtualDisplay(
510                     "CtsDisplay", WIDTH, HEIGHT, DENSITY, mReader.getSurface(),
511                     VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
512             return mVirtualDisplay.getDisplay();
513         }
514 
515         @Override
close()516         public void close() {
517             if (mVirtualDisplay != null) {
518                 mVirtualDisplay.release();
519             }
520             if (mReader != null) {
521                 mReader.close();
522             }
523         }
524     }
525 }
526