• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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