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