Home | History | Annotate | Download | only in wm
      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 
     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 
     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 
     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 
    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. */
    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. */
    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)
    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)
    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
    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     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
    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
    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)
    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
    364         public void onAttachedToWindow() {
    365             synchronized (this) {
    366                 mDisplayId = getWindow().getDecorView().getDisplay().getDisplayId();
    367                 notify();
    368             }
    369         }
    370 
    371         @Override
    372         public void onMovedToDisplay(int displayId, Configuration config) {
    373             synchronized (this) {
    374                 mDisplayId = displayId;
    375                 notify();
    376             }
    377         }
    378 
    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
    390         public void onPointerCaptureChanged(boolean hasCapture) {
    391             synchronized (mLockPointerCapture) {
    392                 mHasPointerCapture = hasCapture;
    393                 mLockPointerCapture.notify();
    394             }
    395         }
    396 
    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.
    408         void requestPointerCapture() {
    409             getWindow().getDecorView().requestPointerCapture();
    410         }
    411 
    412         @Override
    413         public boolean dispatchKeyEvent(KeyEvent event) {
    414             synchronized (mLockKeyEvent) {
    415                 mKeyEventList.add(event);
    416                 mLockKeyEvent.notify();
    417             }
    418             return super.dispatchKeyEvent(event);
    419         }
    420 
    421         int getKeyEventCount() {
    422             synchronized (mLockKeyEvent) {
    423                 return mKeyEventList.size();
    424             }
    425         }
    426 
    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 
    441         void assertAndConsumeKeyEvent(int action, int keyCode, int flags) {
    442             assertNotNull(getLogTag() + " must receive key event.",
    443                     consumeKeyEvent(action, keyCode, flags));
    444         }
    445 
    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 
    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
    477         public void onWindowFocusChanged(boolean hasFocus) {
    478             if (!hasFocus && !mChildWindowHasDrawn) {
    479                 synchronized (this) {
    480                     mLosesFocusWhenNewFocusIsNotDrawn = true;
    481                 }
    482             }
    483             super.onWindowFocusChanged(hasFocus);
    484         }
    485 
    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 
    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
    515         public void close() {
    516             if (mVirtualDisplay != null) {
    517                 mVirtualDisplay.release();
    518             }
    519             if (mReader != null) {
    520                 mReader.close();
    521             }
    522         }
    523     }
    524 }
    525