1 /* 2 * Copyright (C) 2019 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.systemui.cts; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 20 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 21 import static android.provider.AndroidDeviceConfig.KEY_SYSTEM_GESTURE_EXCLUSION_LIMIT_DP; 22 import static android.provider.DeviceConfig.NAMESPACE_ANDROID; 23 import static android.provider.Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS; 24 import static android.view.View.SYSTEM_UI_CLEARABLE_FLAGS; 25 import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; 26 import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; 27 import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; 28 import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; 29 import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; 30 import static android.view.View.SYSTEM_UI_FLAG_VISIBLE; 31 32 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 33 34 import static junit.framework.Assert.assertEquals; 35 import static junit.framework.TestCase.fail; 36 37 import static org.junit.Assert.assertTrue; 38 import static org.junit.Assume.assumeTrue; 39 40 import static java.util.concurrent.TimeUnit.SECONDS; 41 42 import android.app.ActivityOptions; 43 import android.content.ComponentName; 44 import android.content.Context; 45 import android.content.Intent; 46 import android.content.pm.PackageManager; 47 import android.content.res.Resources; 48 import android.graphics.Insets; 49 import android.graphics.Point; 50 import android.graphics.Rect; 51 import android.hardware.display.DisplayManager; 52 import android.os.Bundle; 53 import android.provider.DeviceConfig; 54 import android.provider.Settings; 55 import android.server.wm.settings.SettingsSession; 56 import android.util.ArrayMap; 57 import android.util.DisplayMetrics; 58 import android.view.Display; 59 import android.view.View; 60 import android.view.ViewTreeObserver; 61 import android.view.WindowInsets; 62 63 import androidx.test.platform.app.InstrumentationRegistry; 64 import androidx.test.rule.ActivityTestRule; 65 import androidx.test.runner.AndroidJUnit4; 66 import androidx.test.uiautomator.By; 67 import androidx.test.uiautomator.BySelector; 68 import androidx.test.uiautomator.UiDevice; 69 import androidx.test.uiautomator.UiObject2; 70 import androidx.test.uiautomator.Until; 71 72 import com.android.compatibility.common.util.SystemUtil; 73 import com.android.compatibility.common.util.ThrowingRunnable; 74 75 import com.google.common.collect.Lists; 76 77 import org.junit.After; 78 import org.junit.AfterClass; 79 import org.junit.Before; 80 import org.junit.BeforeClass; 81 import org.junit.Rule; 82 import org.junit.Test; 83 import org.junit.rules.RuleChain; 84 import org.junit.runner.RunWith; 85 86 import java.lang.reflect.Array; 87 import java.util.ArrayList; 88 import java.util.List; 89 import java.util.Map; 90 import java.util.concurrent.CountDownLatch; 91 import java.util.function.BiConsumer; 92 import java.util.function.Consumer; 93 94 @RunWith(AndroidJUnit4.class) 95 public class WindowInsetsBehaviorTests { 96 private static SettingsSession<String> sImmersiveModeConfirmationSetting; 97 private static final String DEF_SCREENSHOT_BASE_PATH = 98 "/sdcard/WindowInsetsBehaviorTests"; 99 private static final String SETTINGS_PACKAGE_NAME = "com.android.settings"; 100 private static final String ARGUMENT_KEY_FORCE_ENABLE = "force_enable_gesture_navigation"; 101 private static final String NAV_BAR_INTERACTION_MODE_RES_NAME = "config_navBarInteractionMode"; 102 private static final int STEPS = 10; 103 private static final int INTERVAL_CLICKS = 300; 104 105 // The minimum value of the system gesture exclusion limit is 200 dp. The value here should be 106 // greater than that, so that we can test if the limit can be changed by DeviceConfig or not. 107 private static final int EXCLUSION_LIMIT_DP = 210; 108 109 private static final int NAV_BAR_INTERACTION_MODE_GESTURAL = 2; 110 111 private final boolean mForceEnableGestureNavigation; 112 private final Map<String, Boolean> mSystemGestureOptionsMap; 113 private float mPixelsPerDp; 114 private float mDensityPerCm; 115 private int mDisplayWidth; 116 private int mExclusionLimit; 117 private UiDevice mDevice; 118 // Bounds for actions like swipe and click. 119 private Rect mActionBounds; 120 private String mEdgeToEdgeNavigationTitle; 121 private String mSystemNavigationTitle; 122 private String mGesturePreferenceTitle; 123 private TouchHelper mTouchHelper; 124 private boolean mConfiguredInSettings; 125 126 @BeforeClass setUpClass()127 public static void setUpClass() { 128 sImmersiveModeConfirmationSetting = new SettingsSession<>( 129 Settings.Secure.getUriFor(IMMERSIVE_MODE_CONFIRMATIONS), 130 Settings.Secure::getString, Settings.Secure::putString); 131 sImmersiveModeConfirmationSetting.set("confirmed"); 132 } 133 134 @AfterClass tearDownClass()135 public static void tearDownClass() { 136 if (sImmersiveModeConfirmationSetting != null) { 137 sImmersiveModeConfirmationSetting.close(); 138 } 139 } 140 getSettingsString(Resources res, String strResName)141 private static String getSettingsString(Resources res, String strResName) { 142 int resIdString = res.getIdentifier(strResName, "string", SETTINGS_PACKAGE_NAME); 143 if (resIdString <= 0x7f000000) { 144 return null; /* most of application res id must be larger than 0x7f000000 */ 145 } 146 147 return res.getString(resIdString); 148 } 149 150 /** 151 * To initial all of options in System Gesture. 152 */ WindowInsetsBehaviorTests()153 public WindowInsetsBehaviorTests() { 154 Bundle bundle = InstrumentationRegistry.getArguments(); 155 mForceEnableGestureNavigation = (bundle != null) 156 && "true".equalsIgnoreCase(bundle.getString(ARGUMENT_KEY_FORCE_ENABLE)); 157 158 mSystemGestureOptionsMap = new ArrayMap(); 159 160 if (!mForceEnableGestureNavigation) { 161 return; 162 } 163 164 Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 165 PackageManager packageManager = context.getPackageManager(); 166 Resources res = null; 167 try { 168 res = packageManager.getResourcesForApplication(SETTINGS_PACKAGE_NAME); 169 } catch (PackageManager.NameNotFoundException e) { 170 return; 171 } 172 if (res == null) { 173 return; 174 } 175 176 mEdgeToEdgeNavigationTitle = getSettingsString(res, "edge_to_edge_navigation_title"); 177 mGesturePreferenceTitle = getSettingsString(res, "gesture_preference_title"); 178 mSystemNavigationTitle = getSettingsString(res, "system_navigation_title"); 179 180 String text = getSettingsString(res, "edge_to_edge_navigation_title"); 181 if (text != null) { 182 mSystemGestureOptionsMap.put(text, false); 183 } 184 text = getSettingsString(res, "swipe_up_to_switch_apps_title"); 185 if (text != null) { 186 mSystemGestureOptionsMap.put(text, false); 187 } 188 text = getSettingsString(res, "legacy_navigation_title"); 189 if (text != null) { 190 mSystemGestureOptionsMap.put(text, false); 191 } 192 193 mConfiguredInSettings = false; 194 } 195 196 private ScreenshotTestRule mScreenshotTestRule = 197 new ScreenshotTestRule(DEF_SCREENSHOT_BASE_PATH); 198 199 @Rule 200 public RuleChain mRuleChain = RuleChain 201 .outerRule(new ActivityTestRule<>( 202 WindowInsetsActivity.class, true, false)) 203 .around(mScreenshotTestRule); 204 205 private WindowInsetsActivity mActivity; 206 private WindowInsets mContentViewWindowInsets; 207 private List<Point> mActionCancelPoints; 208 private List<Point> mActionDownPoints; 209 private List<Point> mActionUpPoints; 210 211 private Context mTargetContext; 212 private int mClickCount; 213 mainThreadRun(Runnable runnable)214 private void mainThreadRun(Runnable runnable) { 215 getInstrumentation().runOnMainSync(runnable); 216 mDevice.waitForIdle(); 217 } 218 hasSystemGestureFeature()219 private boolean hasSystemGestureFeature() { 220 final PackageManager pm = mTargetContext.getPackageManager(); 221 222 // No bars on embedded devices. 223 // No bars on TVs and watches. 224 return !(pm.hasSystemFeature(PackageManager.FEATURE_WATCH) 225 || pm.hasSystemFeature(PackageManager.FEATURE_EMBEDDED) 226 || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) 227 || pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)); 228 } 229 230 findSystemNavigationObject(String text, boolean addCheckSelector)231 private UiObject2 findSystemNavigationObject(String text, boolean addCheckSelector) { 232 BySelector widgetFrameSelector = By.res("android", "widget_frame"); 233 BySelector checkboxSelector = By.checkable(true); 234 if (addCheckSelector) { 235 checkboxSelector = checkboxSelector.checked(true); 236 } 237 BySelector textSelector = By.text(text); 238 BySelector targetSelector = By.hasChild(widgetFrameSelector).hasDescendant(textSelector) 239 .hasDescendant(checkboxSelector); 240 241 return mDevice.findObject(targetSelector); 242 } 243 launchToSettingsSystemGesture()244 private boolean launchToSettingsSystemGesture() { 245 if (!mForceEnableGestureNavigation) { 246 return false; 247 } 248 249 /* launch to the close to the system gesture fragment */ 250 Intent intent = new Intent(Intent.ACTION_MAIN); 251 ComponentName settingComponent = new ComponentName(SETTINGS_PACKAGE_NAME, 252 String.format("%s.%s$%s", SETTINGS_PACKAGE_NAME, "Settings", 253 "SystemDashboardActivity")); 254 intent.setComponent(settingComponent); 255 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 256 mTargetContext.startActivity(intent); 257 258 // Wait for the app to appear 259 mDevice.wait(Until.hasObject(By.pkg("com.android.settings").depth(0)), 260 5000); 261 mDevice.wait(Until.hasObject(By.text(mGesturePreferenceTitle)), 5000); 262 if (mDevice.findObject(By.text(mGesturePreferenceTitle)) == null) { 263 return false; 264 } 265 mDevice.findObject(By.text(mGesturePreferenceTitle)).click(); 266 mDevice.wait(Until.hasObject(By.text(mSystemNavigationTitle)), 5000); 267 if (mDevice.findObject(By.text(mSystemNavigationTitle)) == null) { 268 return false; 269 } 270 mDevice.findObject(By.text(mSystemNavigationTitle)).click(); 271 mDevice.wait(Until.hasObject(By.text(mEdgeToEdgeNavigationTitle)), 5000); 272 273 return mDevice.hasObject(By.text(mEdgeToEdgeNavigationTitle)); 274 } 275 leaveSettings()276 private void leaveSettings() { 277 mDevice.pressBack(); /* Back to Gesture */ 278 mDevice.waitForIdle(); 279 mDevice.pressBack(); /* Back to System */ 280 mDevice.waitForIdle(); 281 mDevice.pressBack(); /* back to Settings */ 282 mDevice.waitForIdle(); 283 mDevice.pressBack(); /* Back to Home */ 284 mDevice.waitForIdle(); 285 286 mDevice.pressHome(); /* double confirm back to home */ 287 mDevice.waitForIdle(); 288 } 289 290 /** 291 * To prepare the things needed to run the tests. 292 * <p> 293 * There are several things needed to prepare 294 * * return to home screen 295 * * launch the activity 296 * * pixel per dp 297 * * the WindowInsets that received by the content view of activity 298 * </p> 299 * @throws Exception caused by permission, nullpointer, etc. 300 */ 301 @Before setUp()302 public void setUp() throws Exception { 303 mDevice = UiDevice.getInstance(getInstrumentation()); 304 mTouchHelper = new TouchHelper(getInstrumentation()); 305 mTargetContext = getInstrumentation().getTargetContext(); 306 if (!hasSystemGestureFeature()) { 307 return; 308 } 309 310 final DisplayManager dm = mTargetContext.getSystemService(DisplayManager.class); 311 final Display display = dm.getDisplay(Display.DEFAULT_DISPLAY); 312 final DisplayMetrics metrics = new DisplayMetrics(); 313 display.getRealMetrics(metrics); 314 mPixelsPerDp = metrics.density; 315 mDensityPerCm = (int) ((float) metrics.densityDpi / 2.54); 316 mDisplayWidth = metrics.widthPixels; 317 mExclusionLimit = (int) (EXCLUSION_LIMIT_DP * mPixelsPerDp); 318 319 // To setup the Edge to Edge environment by do the operation on Settings 320 boolean isOperatedSettingsToExpectedOption = launchToSettingsSystemGesture(); 321 if (isOperatedSettingsToExpectedOption) { 322 for (Map.Entry<String, Boolean> entry : mSystemGestureOptionsMap.entrySet()) { 323 UiObject2 uiObject2 = findSystemNavigationObject(entry.getKey(), true); 324 entry.setValue(uiObject2 != null); 325 } 326 UiObject2 edgeToEdgeObj = mDevice.findObject(By.text(mEdgeToEdgeNavigationTitle)); 327 if (edgeToEdgeObj != null) { 328 edgeToEdgeObj.click(); 329 mConfiguredInSettings = true; 330 } 331 } 332 mDevice.waitForIdle(); 333 leaveSettings(); 334 335 336 mDevice.pressHome(); 337 mDevice.waitForIdle(); 338 339 // launch the Activity and wait until Activity onAttach 340 CountDownLatch latch = new CountDownLatch(1); 341 mActivity = launchActivity(); 342 mActivity.setInitialFinishCallBack(isFinish -> latch.countDown()); 343 mDevice.waitForIdle(); 344 345 latch.await(5, SECONDS); 346 } 347 launchActivity()348 private WindowInsetsActivity launchActivity() { 349 final ActivityOptions options= ActivityOptions.makeBasic(); 350 options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN); 351 final WindowInsetsActivity[] activity = (WindowInsetsActivity[]) Array.newInstance( 352 WindowInsetsActivity.class, 1); 353 SystemUtil.runWithShellPermissionIdentity(() -> { 354 activity[0] = (WindowInsetsActivity) getInstrumentation().startActivitySync( 355 new Intent(getInstrumentation().getTargetContext(), WindowInsetsActivity.class) 356 .addFlags(FLAG_ACTIVITY_NEW_TASK), options.toBundle()); 357 }); 358 return activity[0]; 359 } 360 361 /** 362 * Restore the original configured value for the system gesture by operating Settings. 363 */ 364 @After tearDown()365 public void tearDown() { 366 if (!hasSystemGestureFeature()) { 367 return; 368 } 369 370 if (mConfiguredInSettings) { 371 launchToSettingsSystemGesture(); 372 for (Map.Entry<String, Boolean> entry : mSystemGestureOptionsMap.entrySet()) { 373 if (entry.getValue()) { 374 UiObject2 uiObject2 = findSystemNavigationObject(entry.getKey(), false); 375 if (uiObject2 != null) { 376 uiObject2.click(); 377 } 378 } 379 } 380 leaveSettings(); 381 } 382 } 383 384 swipeByUiDevice(Point p1, Point p2)385 private void swipeByUiDevice(Point p1, Point p2) { 386 mDevice.swipe(p1.x, p1.y, p2.x, p2.y, STEPS); 387 } 388 clickAndWaitByUiDevice(Point p)389 private void clickAndWaitByUiDevice(Point p) { 390 CountDownLatch latch = new CountDownLatch(1); 391 mActivity.setOnClickConsumer((view) -> { 392 latch.countDown(); 393 }); 394 // mDevice.click(p.x, p.y) has the limitation without consideration of the cutout 395 if (!mTouchHelper.click(p.x, p.y)) { 396 fail("Can't inject event at" + p); 397 } 398 399 /* wait until the OnClickListener triggered, and then click the next point */ 400 try { 401 latch.await(5, SECONDS); 402 } catch (InterruptedException e) { 403 fail("Wait too long and onClickEvent doesn't receive"); 404 } 405 406 if (latch.getCount() > 0) { 407 fail("Doesn't receive onClickEvent at " + p); 408 } 409 } 410 swipeBigX(Rect viewBoundary, BiConsumer<Point, Point> callback)411 private int swipeBigX(Rect viewBoundary, BiConsumer<Point, Point> callback) { 412 final int theLeftestLine = viewBoundary.left + 1; 413 final int theToppestLine = viewBoundary.top + 1; 414 final int theRightestLine = viewBoundary.right - 1; 415 final int theBottomestLine = viewBoundary.bottom - 1; 416 417 if (callback != null) { 418 callback.accept(new Point(theLeftestLine, theToppestLine), 419 new Point(theRightestLine, theBottomestLine)); 420 } 421 mDevice.waitForIdle(); 422 423 if (callback != null) { 424 callback.accept(new Point(theRightestLine, theToppestLine), 425 new Point(viewBoundary.left, theBottomestLine)); 426 } 427 mDevice.waitForIdle(); 428 429 return 2; 430 } 431 clickAllOfHorizontalSamplePoints(Rect viewBoundary, int y, double interval, Consumer<Point> callback)432 private int clickAllOfHorizontalSamplePoints(Rect viewBoundary, int y, double interval, 433 Consumer<Point> callback) { 434 final int theLeftestLine = viewBoundary.left + 1; 435 final int theRightestLine = viewBoundary.right - 1; 436 437 int count = 0; 438 for (int i = theLeftestLine; i < theRightestLine; i += interval) { 439 if (callback != null) { 440 callback.accept(new Point(i, y)); 441 } 442 mDevice.waitForIdle(); 443 count++; 444 } 445 446 if (callback != null) { 447 callback.accept(new Point(theRightestLine, y)); 448 } 449 mDevice.waitForIdle(); 450 count++; 451 452 return count; 453 } 454 clickAllOfSamplePoints(Rect viewBoundary, Consumer<Point> callback)455 private int clickAllOfSamplePoints(Rect viewBoundary, Consumer<Point> callback) { 456 if (viewBoundary.isEmpty()) { 457 return 0; 458 } 459 final int theToppestLine = viewBoundary.top + 1; 460 final int theBottomestLine = viewBoundary.bottom - 1; 461 final int width = viewBoundary.width(); 462 final int height = viewBoundary.height(); 463 final double interval = height / Math.sqrt((double) height / width * INTERVAL_CLICKS); 464 int count = 0; 465 for (int i = theToppestLine; i < theBottomestLine; i += interval) { 466 count += clickAllOfHorizontalSamplePoints(viewBoundary, i, interval, callback); 467 } 468 count += clickAllOfHorizontalSamplePoints( 469 viewBoundary, theBottomestLine, interval, callback); 470 471 return count; 472 } 473 swipeAllOfHorizontalLinesFromLeftToRight(Rect viewBoundary, BiConsumer<Point, Point> callback)474 private int swipeAllOfHorizontalLinesFromLeftToRight(Rect viewBoundary, 475 BiConsumer<Point, Point> callback) { 476 final int theLeftestLine = viewBoundary.left + 1; 477 final int theToppestLine = viewBoundary.top + 1; 478 final int theBottomestLine = viewBoundary.bottom - 1; 479 480 int count = 0; 481 482 for (int i = theToppestLine; i < theBottomestLine; i += mDensityPerCm * 2) { 483 if (callback != null) { 484 callback.accept(new Point(theLeftestLine, i), 485 new Point(viewBoundary.centerX(), i)); 486 } 487 mDevice.waitForIdle(); 488 count++; 489 } 490 if (callback != null) { 491 callback.accept(new Point(theLeftestLine, theBottomestLine), 492 new Point(viewBoundary.centerX(), theBottomestLine)); 493 } 494 mDevice.waitForIdle(); 495 count++; 496 497 return count; 498 } 499 swipeAllOfHorizontalLinesFromRightToLeft(Rect viewBoundary, BiConsumer<Point, Point> callback)500 private int swipeAllOfHorizontalLinesFromRightToLeft(Rect viewBoundary, 501 BiConsumer<Point, Point> callback) { 502 final int theToppestLine = viewBoundary.top + 1; 503 final int theRightestLine = viewBoundary.right - 1; 504 final int theBottomestLine = viewBoundary.bottom - 1; 505 506 int count = 0; 507 for (int i = theToppestLine; i < theBottomestLine; i += mDensityPerCm * 2) { 508 if (callback != null) { 509 callback.accept(new Point(theRightestLine, i), 510 new Point(viewBoundary.centerX(), i)); 511 } 512 mDevice.waitForIdle(); 513 count++; 514 } 515 if (callback != null) { 516 callback.accept(new Point(theRightestLine, theBottomestLine), 517 new Point(viewBoundary.centerX(), theBottomestLine)); 518 } 519 mDevice.waitForIdle(); 520 count++; 521 522 return count; 523 } 524 swipeAllOfHorizontalLines(Rect viewBoundary, BiConsumer<Point, Point> callback)525 private int swipeAllOfHorizontalLines(Rect viewBoundary, BiConsumer<Point, Point> callback) { 526 int count = 0; 527 528 count += swipeAllOfHorizontalLinesFromLeftToRight(viewBoundary, callback); 529 count += swipeAllOfHorizontalLinesFromRightToLeft(viewBoundary, callback); 530 531 return count; 532 } 533 swipeAllOfVerticalLinesFromTopToBottom(Rect viewBoundary, BiConsumer<Point, Point> callback)534 private int swipeAllOfVerticalLinesFromTopToBottom(Rect viewBoundary, 535 BiConsumer<Point, Point> callback) { 536 final int theLeftestLine = viewBoundary.left + 1; 537 final int theToppestLine = viewBoundary.top + 1; 538 final int theRightestLine = viewBoundary.right - 1; 539 540 int count = 0; 541 for (int i = theLeftestLine; i < theRightestLine; i += mDensityPerCm * 2) { 542 if (callback != null) { 543 callback.accept(new Point(i, theToppestLine), 544 new Point(i, viewBoundary.centerY())); 545 } 546 mDevice.waitForIdle(); 547 count++; 548 } 549 if (callback != null) { 550 callback.accept(new Point(theRightestLine, theToppestLine), 551 new Point(theRightestLine, viewBoundary.centerY())); 552 } 553 mDevice.waitForIdle(); 554 count++; 555 556 return count; 557 } 558 swipeAllOfVerticalLinesFromBottomToTop(Rect viewBoundary, BiConsumer<Point, Point> callback)559 private int swipeAllOfVerticalLinesFromBottomToTop(Rect viewBoundary, 560 BiConsumer<Point, Point> callback) { 561 final int theLeftestLine = viewBoundary.left + 1; 562 final int theRightestLine = viewBoundary.right - 1; 563 final int theBottomestLine = viewBoundary.bottom - 1; 564 565 int count = 0; 566 for (int i = theLeftestLine; i < theRightestLine; i += mDensityPerCm * 2) { 567 if (callback != null) { 568 callback.accept(new Point(i, theBottomestLine), 569 new Point(i, viewBoundary.centerY())); 570 } 571 mDevice.waitForIdle(); 572 count++; 573 } 574 if (callback != null) { 575 callback.accept(new Point(theRightestLine, theBottomestLine), 576 new Point(theRightestLine, viewBoundary.centerY())); 577 } 578 mDevice.waitForIdle(); 579 count++; 580 581 return count; 582 } 583 swipeAllOfVerticalLines(Rect viewBoundary, BiConsumer<Point, Point> callback)584 private int swipeAllOfVerticalLines(Rect viewBoundary, BiConsumer<Point, Point> callback) { 585 int count = 0; 586 587 count += swipeAllOfVerticalLinesFromTopToBottom(viewBoundary, callback); 588 count += swipeAllOfVerticalLinesFromBottomToTop(viewBoundary, callback); 589 590 return count; 591 } 592 swipeInViewBoundary(Rect viewBoundary, BiConsumer<Point, Point> callback)593 private int swipeInViewBoundary(Rect viewBoundary, BiConsumer<Point, Point> callback) { 594 int count = 0; 595 596 count += swipeBigX(viewBoundary, callback); 597 count += swipeAllOfHorizontalLines(viewBoundary, callback); 598 count += swipeAllOfVerticalLines(viewBoundary, callback); 599 600 return count; 601 } 602 swipeInViewBoundary(Rect viewBoundary)603 private int swipeInViewBoundary(Rect viewBoundary) { 604 return swipeInViewBoundary(viewBoundary, this::swipeByUiDevice); 605 } 606 splitBoundsAccordingToExclusionLimit(Rect rect)607 private List<Rect> splitBoundsAccordingToExclusionLimit(Rect rect) { 608 final int exclusionHeightLimit = (int) (EXCLUSION_LIMIT_DP * mPixelsPerDp + 0.5f); 609 final List<Rect> bounds = new ArrayList<>(); 610 int nextTop = rect.top; 611 while (nextTop < rect.bottom) { 612 final int top = nextTop; 613 int bottom = top + exclusionHeightLimit; 614 if (bottom > rect.bottom) { 615 bottom = rect.bottom; 616 } 617 618 bounds.add(new Rect(rect.left, top, rect.right, bottom)); 619 620 nextTop = bottom; 621 } 622 623 return bounds; 624 } 625 626 /** 627 * @throws Throwable when setting the property goes wrong. 628 */ 629 @Test systemGesture_excludeViewRects_withoutAnyCancel()630 public void systemGesture_excludeViewRects_withoutAnyCancel() 631 throws Throwable { 632 assumeTrue(hasSystemGestureFeature()); 633 634 mainThreadRun(() -> mContentViewWindowInsets = mActivity.getDecorViewWindowInsets()); 635 mainThreadRun(() -> mActionBounds = mActivity.getActionBounds( 636 mContentViewWindowInsets.getSystemGestureInsets(), mContentViewWindowInsets)); 637 final Rect exclusionRect = new Rect(); 638 mainThreadRun(() -> exclusionRect.set(mActivity.getSystemGestureExclusionBounds( 639 mContentViewWindowInsets.getMandatorySystemGestureInsets(), 640 mContentViewWindowInsets))); 641 642 final int[] swipeCount = {0}; 643 doInExclusionLimitSession(() -> { 644 final List<Rect> swipeBounds = splitBoundsAccordingToExclusionLimit(mActionBounds); 645 final List<Rect> exclusionRects = splitBoundsAccordingToExclusionLimit(exclusionRect); 646 final int size = swipeBounds.size(); 647 for (int i = 0; i < size; i++) { 648 setAndWaitForSystemGestureExclusionRectsListenerTrigger(exclusionRects.get(i)); 649 swipeCount[0] += swipeInViewBoundary(swipeBounds.get(i)); 650 } 651 }); 652 mainThreadRun(() -> { 653 mActionDownPoints = mActivity.getActionDownPoints(); 654 mActionUpPoints = mActivity.getActionUpPoints(); 655 mActionCancelPoints = mActivity.getActionCancelPoints(); 656 }); 657 mScreenshotTestRule.capture(); 658 659 assertEquals(0, mActionCancelPoints.size()); 660 assertEquals(swipeCount[0], mActionUpPoints.size()); 661 assertEquals(swipeCount[0], mActionDownPoints.size()); 662 } 663 664 @Test systemGesture_notExcludeViewRects_withoutAnyCancel()665 public void systemGesture_notExcludeViewRects_withoutAnyCancel() { 666 assumeTrue(hasSystemGestureFeature()); 667 668 mainThreadRun(() -> mActivity.setSystemGestureExclusion(null)); 669 mainThreadRun(() -> mContentViewWindowInsets = mActivity.getDecorViewWindowInsets()); 670 mainThreadRun(() -> mActionBounds = mActivity.getActionBounds( 671 mContentViewWindowInsets.getSystemGestureInsets(), mContentViewWindowInsets)); 672 final int swipeCount = swipeInViewBoundary(mActionBounds); 673 674 mainThreadRun(() -> { 675 mActionDownPoints = mActivity.getActionDownPoints(); 676 mActionUpPoints = mActivity.getActionUpPoints(); 677 mActionCancelPoints = mActivity.getActionCancelPoints(); 678 }); 679 mScreenshotTestRule.capture(); 680 681 assertEquals(0, mActionCancelPoints.size()); 682 assertEquals(swipeCount, mActionUpPoints.size()); 683 assertEquals(swipeCount, mActionDownPoints.size()); 684 } 685 686 @Test tappableElements_tapSamplePoints_excludeViewRects_withoutAnyCancel()687 public void tappableElements_tapSamplePoints_excludeViewRects_withoutAnyCancel() 688 throws InterruptedException { 689 assumeTrue(hasSystemGestureFeature()); 690 mainThreadRun(() -> mContentViewWindowInsets = mActivity.getDecorViewWindowInsets()); 691 mainThreadRun(() -> mActionBounds = mActivity.getActionBounds( 692 mContentViewWindowInsets.getTappableElementInsets(), mContentViewWindowInsets)); 693 694 final int count = clickAllOfSamplePoints(mActionBounds, this::clickAndWaitByUiDevice); 695 696 mainThreadRun(() -> { 697 mClickCount = mActivity.getClickCount(); 698 mActionCancelPoints = mActivity.getActionCancelPoints(); 699 }); 700 mScreenshotTestRule.capture(); 701 702 assertEquals("The number of click not match", count, mClickCount); 703 assertEquals("The Number of the canceled points not match", 0, 704 mActionCancelPoints.size()); 705 } 706 707 @Test tappableElements_tapSamplePoints_notExcludeViewRects_withoutAnyCancel()708 public void tappableElements_tapSamplePoints_notExcludeViewRects_withoutAnyCancel() { 709 assumeTrue(hasSystemGestureFeature()); 710 711 mainThreadRun(() -> mActivity.setSystemGestureExclusion(null)); 712 mainThreadRun(() -> mContentViewWindowInsets = mActivity.getDecorViewWindowInsets()); 713 mainThreadRun(() -> mActionBounds = mActivity.getActionBounds( 714 mContentViewWindowInsets.getTappableElementInsets(), mContentViewWindowInsets)); 715 716 final int count = clickAllOfSamplePoints(mActionBounds, this::clickAndWaitByUiDevice); 717 718 mainThreadRun(() -> { 719 mClickCount = mActivity.getClickCount(); 720 mActionCancelPoints = mActivity.getActionCancelPoints(); 721 }); 722 mScreenshotTestRule.capture(); 723 724 assertEquals("The number of click not match", count, mClickCount); 725 assertEquals("The Number of the canceled points not match", 0, 726 mActionCancelPoints.size()); 727 } 728 729 @Test swipeInsideLimit_systemUiVisible_noEventCanceled()730 public void swipeInsideLimit_systemUiVisible_noEventCanceled() throws Throwable { 731 assumeTrue(hasSystemGestureFeature()); 732 733 final int swipeCount = 1; 734 final boolean insideLimit = true; 735 testSystemGestureExclusionLimit(swipeCount, insideLimit, SYSTEM_UI_FLAG_VISIBLE); 736 737 assertEquals("Swipe must not be canceled.", 0, mActionCancelPoints.size()); 738 assertEquals("Action up points.", swipeCount, mActionUpPoints.size()); 739 assertEquals("Action down points.", swipeCount, mActionDownPoints.size()); 740 } 741 742 @Test swipeOutsideLimit_systemUiVisible_allEventsCanceled()743 public void swipeOutsideLimit_systemUiVisible_allEventsCanceled() throws Throwable { 744 assumeTrue(hasSystemGestureFeature()); 745 746 assumeGestureNavigationMode(); 747 748 final int swipeCount = 1; 749 final boolean insideLimit = false; 750 testSystemGestureExclusionLimit(swipeCount, insideLimit, SYSTEM_UI_FLAG_VISIBLE); 751 752 assertEquals("Swipe must be always canceled.", swipeCount, mActionCancelPoints.size()); 753 assertEquals("Action up points.", 0, mActionUpPoints.size()); 754 assertEquals("Action down points.", swipeCount, mActionDownPoints.size()); 755 } 756 757 @Test swipeInsideLimit_immersiveSticky_noEventCanceled()758 public void swipeInsideLimit_immersiveSticky_noEventCanceled() throws Throwable { 759 assumeTrue(hasSystemGestureFeature()); 760 761 // The first event may be never canceled. So we need to swipe at least twice. 762 final int swipeCount = 2; 763 final boolean insideLimit = true; 764 testSystemGestureExclusionLimit(swipeCount, insideLimit, SYSTEM_UI_FLAG_IMMERSIVE_STICKY 765 | SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_HIDE_NAVIGATION 766 | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); 767 768 assertEquals("Swipe must not be canceled.", 0, mActionCancelPoints.size()); 769 assertEquals("Action up points.", swipeCount, mActionUpPoints.size()); 770 assertEquals("Action down points.", swipeCount, mActionDownPoints.size()); 771 } 772 773 @Test swipeOutsideLimit_immersiveSticky_noEventCanceled()774 public void swipeOutsideLimit_immersiveSticky_noEventCanceled() throws Throwable { 775 assumeTrue(hasSystemGestureFeature()); 776 777 // The first event may be never canceled. So we need to swipe at least twice. 778 final int swipeCount = 2; 779 final boolean insideLimit = false; 780 testSystemGestureExclusionLimit(swipeCount, insideLimit, SYSTEM_UI_FLAG_IMMERSIVE_STICKY 781 | SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_HIDE_NAVIGATION 782 | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); 783 784 assertEquals("Swipe must not be canceled.", 0, mActionCancelPoints.size()); 785 assertEquals("Action up points.", swipeCount, mActionUpPoints.size()); 786 assertEquals("Action down points.", swipeCount, mActionDownPoints.size()); 787 } 788 testSystemGestureExclusionLimit(int swipeCount, boolean insideLimit, int systemUiVisibility)789 private void testSystemGestureExclusionLimit(int swipeCount, boolean insideLimit, 790 int systemUiVisibility) throws Throwable { 791 final int shiftY = insideLimit ? 1 : -1; 792 assumeGestureNavigation(); 793 doInExclusionLimitSession(() -> { 794 setSystemUiVisibility(systemUiVisibility); 795 setAndWaitForSystemGestureExclusionRectsListenerTrigger(null); 796 797 final Rect swipeBounds = new Rect(); 798 mainThreadRun(() -> { 799 final View rootView = mActivity.getWindow().getDecorView(); 800 swipeBounds.set(mActivity.getViewBoundOnScreen(rootView)); 801 }); 802 // The limit is consumed from bottom to top. 803 final int swipeY = swipeBounds.bottom - mExclusionLimit + shiftY; 804 805 for (int i = 0; i < swipeCount; i++) { 806 mDevice.swipe(swipeBounds.left, swipeY, swipeBounds.right, swipeY, STEPS); 807 } 808 809 mainThreadRun(() -> { 810 mActionDownPoints = mActivity.getActionDownPoints(); 811 mActionUpPoints = mActivity.getActionUpPoints(); 812 mActionCancelPoints = mActivity.getActionCancelPoints(); 813 }); 814 }); 815 } 816 assumeGestureNavigation()817 private void assumeGestureNavigation() { 818 final Insets[] insets = new Insets[1]; 819 mainThreadRun(() -> { 820 final View view = mActivity.getWindow().getDecorView(); 821 insets[0] = view.getRootWindowInsets().getSystemGestureInsets(); 822 }); 823 assumeTrue("Gesture navigation required.", insets[0].left > 0); 824 } 825 assumeGestureNavigationMode()826 private void assumeGestureNavigationMode() { 827 // TODO: b/153032202 consider the CTS on GSI case. 828 Resources res = mTargetContext.getResources(); 829 int naviMode = res.getIdentifier(NAV_BAR_INTERACTION_MODE_RES_NAME, "integer", "android"); 830 831 assumeTrue("Gesture navigation required", naviMode == NAV_BAR_INTERACTION_MODE_GESTURAL); 832 } 833 834 /** 835 * Set system UI visibility and wait for it is applied by the system. 836 * 837 * @param flags the visibility flags. 838 * @throws InterruptedException when the test gets aborted. 839 */ setSystemUiVisibility(int flags)840 private void setSystemUiVisibility(int flags) throws InterruptedException { 841 final CountDownLatch flagsApplied = new CountDownLatch(1); 842 final int targetFlags = SYSTEM_UI_CLEARABLE_FLAGS & flags; 843 mainThreadRun(() -> { 844 final View view = mActivity.getWindow().getDecorView(); 845 if ((view.getSystemUiVisibility() & SYSTEM_UI_CLEARABLE_FLAGS) == targetFlags) { 846 // System UI visibility is already what we want. Stop waiting for the callback. 847 flagsApplied.countDown(); 848 return; 849 } 850 view.setOnSystemUiVisibilityChangeListener(visibility -> { 851 if (visibility == targetFlags) { 852 flagsApplied.countDown(); 853 } 854 }); 855 view.setSystemUiVisibility(flags); 856 }); 857 assertTrue("System UI visibility must be applied.", flagsApplied.await(3, SECONDS)); 858 } 859 860 /** 861 * Set an exclusion rectangle and wait for it is applied by the system. 862 * <p> 863 * if the parameter rect doesn't provide or is null, the decorView will be used to set into 864 * the exclusion rects. 865 * </p> 866 * 867 * @param rect the rectangle that is added into the system gesture exclusion rects. 868 * @throws InterruptedException when the test gets aborted. 869 */ setAndWaitForSystemGestureExclusionRectsListenerTrigger(Rect rect)870 private void setAndWaitForSystemGestureExclusionRectsListenerTrigger(Rect rect) 871 throws InterruptedException { 872 final CountDownLatch exclusionApplied = new CountDownLatch(1); 873 mainThreadRun(() -> { 874 final View view = mActivity.getWindow().getDecorView(); 875 final ViewTreeObserver vto = view.getViewTreeObserver(); 876 vto.addOnSystemGestureExclusionRectsChangedListener( 877 rects -> exclusionApplied.countDown()); 878 Rect exclusiveRect = new Rect(0, 0, view.getWidth(), view.getHeight()); 879 if (rect != null) { 880 exclusiveRect = rect; 881 } 882 view.setSystemGestureExclusionRects(Lists.newArrayList(exclusiveRect)); 883 }); 884 assertTrue("Exclusion must be applied.", exclusionApplied.await(3, SECONDS)); 885 } 886 887 /** 888 * Run the given task while the system gesture exclusion limit has been changed to 889 * {@link #EXCLUSION_LIMIT_DP}, and then restore the value while the task is finished. 890 * 891 * @param task the task to be run. 892 * @throws Throwable when something goes unexpectedly. 893 */ doInExclusionLimitSession(ThrowingRunnable task)894 private static void doInExclusionLimitSession(ThrowingRunnable task) throws Throwable { 895 final int[] originalLimitDp = new int[1]; 896 SystemUtil.runWithShellPermissionIdentity(() -> { 897 originalLimitDp[0] = DeviceConfig.getInt(NAMESPACE_ANDROID, 898 KEY_SYSTEM_GESTURE_EXCLUSION_LIMIT_DP, -1); 899 DeviceConfig.setProperty( 900 NAMESPACE_ANDROID, 901 KEY_SYSTEM_GESTURE_EXCLUSION_LIMIT_DP, 902 Integer.toString(EXCLUSION_LIMIT_DP), false /* makeDefault */); 903 }); 904 905 try { 906 task.run(); 907 } finally { 908 // Restore the value 909 SystemUtil.runWithShellPermissionIdentity(() -> DeviceConfig.setProperty( 910 NAMESPACE_ANDROID, 911 KEY_SYSTEM_GESTURE_EXCLUSION_LIMIT_DP, 912 (originalLimitDp[0] != -1) ? Integer.toString(originalLimitDp[0]) : null, 913 false /* makeDefault */)); 914 } 915 } 916 } 917