• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
testPointerCapture()234     public void testPointerCapture() throws InterruptedException {
235         final PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class,
236                 DEFAULT_DISPLAY);
237 
238         // Assert primary activity can have pointer capture before we have multiple focused windows.
239         getInstrumentation().runOnMainSync(primaryActivity::requestPointerCapture);
240         primaryActivity.waitAndAssertPointerCaptureState(true /* hasCapture */);
241 
242         assumeTrue(supportsMultiDisplay());
243         assumeFalse(perDisplayFocusEnabled());
244         try (VirtualDisplaySession displaySession = new VirtualDisplaySession()) {
245             final int secondaryDisplayId = displaySession.createDisplay(
246                     getInstrumentation().getTargetContext()).getDisplayId();
247             final SecondaryActivity secondaryActivity =
248                     startActivity(SecondaryActivity.class, secondaryDisplayId);
249 
250             // Assert primary activity lost pointer capture when it is not top focused.
251             primaryActivity.waitAndAssertPointerCaptureState(false /* hasCapture */);
252 
253             // Assert secondary activity can have pointer capture when it is top focused.
254             getInstrumentation().runOnMainSync(secondaryActivity::requestPointerCapture);
255             secondaryActivity.waitAndAssertPointerCaptureState(true /* hasCapture */);
256 
257             tapOnCenterOfDisplay(DEFAULT_DISPLAY);
258 
259             // Assert secondary activity lost pointer capture when it is not top focused.
260             secondaryActivity.waitAndAssertPointerCaptureState(false /* hasCapture */);
261         }
262     }
263 
264     /**
265      * Test if the focused window can still have focus after it is moved to another display.
266      */
267     @Test
testDisplayChanged()268     public void testDisplayChanged() throws InterruptedException {
269         assumeTrue(supportsMultiDisplay());
270 
271         final PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class,
272                 DEFAULT_DISPLAY);
273 
274         final SecondaryActivity secondaryActivity;
275         try (VirtualDisplaySession displaySession = new VirtualDisplaySession()) {
276             final int secondaryDisplayId = displaySession.createDisplay(
277                     getInstrumentation().getTargetContext()).getDisplayId();
278             secondaryActivity = startActivity(SecondaryActivity.class, secondaryDisplayId);
279         }
280         // Secondary display disconnected.
281 
282         assertNotNull("SecondaryActivity must be started.", secondaryActivity);
283         secondaryActivity.waitAndAssertDisplayId(DEFAULT_DISPLAY);
284         secondaryActivity.waitAndAssertWindowFocusState(true /* hasFocus */);
285 
286         primaryActivity.waitAndAssertWindowFocusState(false /* hasFocus */);
287     }
288 
289     /**
290      * Ensure that a non focused display becomes focused when tapping on a focusable window on
291      * that display.
292      */
293     @Test
testTapFocusableWindow()294     public void testTapFocusableWindow() throws InterruptedException {
295         assumeTrue(supportsMultiDisplay());
296         assumeFalse(perDisplayFocusEnabled());
297 
298         PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class, DEFAULT_DISPLAY);
299 
300         try (VirtualDisplaySession displaySession = new VirtualDisplaySession()) {
301             final int secondaryDisplayId = displaySession.createDisplay(
302                     getInstrumentation().getTargetContext()).getDisplayId();
303             SecondaryActivity secondaryActivity = startActivity(SecondaryActivity.class,
304                     secondaryDisplayId);
305 
306             tapOnCenterOfDisplay(DEFAULT_DISPLAY);
307             // Ensure primary activity got focus
308             primaryActivity.waitAndAssertWindowFocusState(true);
309             secondaryActivity.waitAndAssertWindowFocusState(false);
310         }
311     }
312 
313     /**
314      * Ensure that a non focused display does not become focused when tapping on a non-focusable
315      * window on that display.
316      */
317     @Test
318     @FlakyTest(bugId = 130467737)
testTapNonFocusableWindow()319     public void testTapNonFocusableWindow() throws InterruptedException {
320         assumeTrue(supportsMultiDisplay());
321         assumeFalse(perDisplayFocusEnabled());
322 
323         PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class, DEFAULT_DISPLAY);
324 
325         try (VirtualDisplaySession displaySession = new VirtualDisplaySession()) {
326             final int secondaryDisplayId = displaySession.createDisplay(
327                     getInstrumentation().getTargetContext()).getDisplayId();
328             SecondaryActivity secondaryActivity = startActivity(SecondaryActivity.class,
329                     secondaryDisplayId);
330 
331             // Tap on a window that can't be focused and ensure that the other window in that
332             // display, primaryActivity's window, doesn't get focus.
333             getInstrumentation().runOnMainSync(() -> {
334                 View view = new View(primaryActivity);
335                 LayoutParams p = new LayoutParams();
336                 p.flags = LayoutParams.FLAG_NOT_FOCUSABLE;
337                 primaryActivity.getWindowManager().addView(view, p);
338             });
339             getInstrumentation().waitForIdleSync();
340 
341             tapOnCenterOfDisplay(DEFAULT_DISPLAY);
342             // Ensure secondary activity still has focus
343             secondaryActivity.waitAndAssertWindowFocusState(true);
344             primaryActivity.waitAndAssertWindowFocusState(false);
345         }
346     }
347 
348     private static class InputTargetActivity extends FocusableActivity {
349         private static final long TIMEOUT_DISPLAY_CHANGED = 1000; // milliseconds
350         private static final long TIMEOUT_POINTER_CAPTURE_CHANGED = 1000;
351         private static final long TIMEOUT_NEXT_KEY_EVENT = 1000;
352 
353         private final Object mLockPointerCapture = new Object();
354         private final Object mLockKeyEvent = new Object();
355 
356         @GuardedBy("this")
357         private int mDisplayId = INVALID_DISPLAY;
358         @GuardedBy("mLockPointerCapture")
359         private boolean mHasPointerCapture;
360         @GuardedBy("mLockKeyEvent")
361         private ArrayList<KeyEvent> mKeyEventList = new ArrayList<>();
362 
363         @Override
onAttachedToWindow()364         public void onAttachedToWindow() {
365             synchronized (this) {
366                 mDisplayId = getWindow().getDecorView().getDisplay().getDisplayId();
367                 notify();
368             }
369         }
370 
371         @Override
onMovedToDisplay(int displayId, Configuration config)372         public void onMovedToDisplay(int displayId, Configuration config) {
373             synchronized (this) {
374                 mDisplayId = displayId;
375                 notify();
376             }
377         }
378 
waitAndAssertDisplayId(int displayId)379         void waitAndAssertDisplayId(int displayId) throws InterruptedException {
380             synchronized (this) {
381                 if (mDisplayId != displayId) {
382                     wait(TIMEOUT_DISPLAY_CHANGED);
383                 }
384                 assertEquals(getLogTag() + " must be moved to the display.",
385                         displayId, mDisplayId);
386             }
387         }
388 
389         @Override
onPointerCaptureChanged(boolean hasCapture)390         public void onPointerCaptureChanged(boolean hasCapture) {
391             synchronized (mLockPointerCapture) {
392                 mHasPointerCapture = hasCapture;
393                 mLockPointerCapture.notify();
394             }
395         }
396 
waitAndAssertPointerCaptureState(boolean hasCapture)397         void waitAndAssertPointerCaptureState(boolean hasCapture) throws InterruptedException {
398             synchronized (mLockPointerCapture) {
399                 if (mHasPointerCapture != hasCapture) {
400                     mLockPointerCapture.wait(TIMEOUT_POINTER_CAPTURE_CHANGED);
401                 }
402                 assertEquals(getLogTag() + " must" + (hasCapture ? "" : " not")
403                         + " have pointer capture.", hasCapture, mHasPointerCapture);
404             }
405         }
406 
407         // Should be only called from the main thread.
requestPointerCapture()408         void requestPointerCapture() {
409             getWindow().getDecorView().requestPointerCapture();
410         }
411 
412         @Override
dispatchKeyEvent(KeyEvent event)413         public boolean dispatchKeyEvent(KeyEvent event) {
414             synchronized (mLockKeyEvent) {
415                 mKeyEventList.add(event);
416                 mLockKeyEvent.notify();
417             }
418             return super.dispatchKeyEvent(event);
419         }
420 
getKeyEventCount()421         int getKeyEventCount() {
422             synchronized (mLockKeyEvent) {
423                 return mKeyEventList.size();
424             }
425         }
426 
consumeKeyEvent(int action, int keyCode, int flags)427         private KeyEvent consumeKeyEvent(int action, int keyCode, int flags) {
428             synchronized (mLockKeyEvent) {
429                 for (int i = mKeyEventList.size() - 1; i >= 0; i--) {
430                     final KeyEvent event = mKeyEventList.get(i);
431                     if (event.getAction() == action && event.getKeyCode() == keyCode
432                             && (event.getFlags() & flags) == flags) {
433                         mKeyEventList.remove(event);
434                         return event;
435                     }
436                 }
437             }
438             return null;
439         }
440 
assertAndConsumeKeyEvent(int action, int keyCode, int flags)441         void assertAndConsumeKeyEvent(int action, int keyCode, int flags) {
442             assertNotNull(getLogTag() + " must receive key event.",
443                     consumeKeyEvent(action, keyCode, flags));
444         }
445 
waitAssertAndConsumeKeyEvent(int action, int keyCode, int flags)446         void waitAssertAndConsumeKeyEvent(int action, int keyCode, int flags)
447                 throws InterruptedException {
448             if (consumeKeyEvent(action, keyCode, flags) == null) {
449                 synchronized (mLockKeyEvent) {
450                     mLockKeyEvent.wait(TIMEOUT_NEXT_KEY_EVENT);
451                 }
452                 assertAndConsumeKeyEvent(action, keyCode, flags);
453             }
454         }
455     }
456 
457     public static class PrimaryActivity extends InputTargetActivity { }
458 
459     public static class SecondaryActivity extends InputTargetActivity { }
460 
461     public static class LosingFocusActivity extends InputTargetActivity {
462         private boolean mChildWindowHasDrawn = false;
463 
464         @GuardedBy("this")
465         private boolean mLosesFocusWhenNewFocusIsNotDrawn = false;
466 
addChildWindow()467         void addChildWindow() {
468             getWindowManager().addView(new View(this) {
469                 @Override
470                 protected void onDraw(Canvas canvas) {
471                     mChildWindowHasDrawn = true;
472                 }
473             }, new LayoutParams());
474         }
475 
476         @Override
onWindowFocusChanged(boolean hasFocus)477         public void onWindowFocusChanged(boolean hasFocus) {
478             if (!hasFocus && !mChildWindowHasDrawn) {
479                 synchronized (this) {
480                     mLosesFocusWhenNewFocusIsNotDrawn = true;
481                 }
482             }
483             super.onWindowFocusChanged(hasFocus);
484         }
485 
losesFocusWhenNewFocusIsNotDrawn()486         boolean losesFocusWhenNewFocusIsNotDrawn() {
487             synchronized (this) {
488                 return mLosesFocusWhenNewFocusIsNotDrawn;
489             }
490         }
491     }
492 
493     private static class VirtualDisplaySession implements AutoCloseable {
494         private static final int WIDTH = 800;
495         private static final int HEIGHT = 480;
496         private static final int DENSITY = 160;
497 
498         private VirtualDisplay mVirtualDisplay;
499         private ImageReader mReader;
500 
createDisplay(Context context)501         Display createDisplay(Context context) {
502             if (mReader != null) {
503                 throw new IllegalStateException(
504                         "Only one display can be created during this session.");
505             }
506             mReader = ImageReader.newInstance(WIDTH, HEIGHT, PixelFormat.RGBA_8888,
507                     2 /* maxImages */);
508             mVirtualDisplay = context.getSystemService(DisplayManager.class).createVirtualDisplay(
509                     "CtsDisplay", WIDTH, HEIGHT, DENSITY, mReader.getSurface(),
510                     VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
511             return mVirtualDisplay.getDisplay();
512         }
513 
514         @Override
close()515         public void close() {
516             if (mVirtualDisplay != null) {
517                 mVirtualDisplay.release();
518             }
519             if (mReader != null) {
520                 mReader.close();
521             }
522         }
523     }
524 }
525