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