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