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