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