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