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