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