blob: 1f0369ab38f6239aaae785e2ba2fa1c8d859dabb [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.systemui.cts;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.provider.DeviceConfig.NAMESPACE_ANDROID;
import static android.provider.AndroidDeviceConfig.KEY_SYSTEM_GESTURE_EXCLUSION_LIMIT_DP;
import static android.view.View.SYSTEM_UI_CLEARABLE_FLAGS;
import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
import static android.view.View.SYSTEM_UI_FLAG_VISIBLE;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static junit.framework.Assert.assertEquals;
import static junit.framework.TestCase.fail;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Build;
import android.os.Bundle;
import android.provider.DeviceConfig;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.ApiLevelUtil;
import com.android.compatibility.common.util.CtsDownstreamingTest;
import com.android.compatibility.common.util.SystemUtil;
import com.android.compatibility.common.util.ThrowingRunnable;
import com.google.common.collect.Lists;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@RunWith(AndroidJUnit4.class)
public class WindowInsetsBehaviorTests {
private static final String DEF_SCREENSHOT_BASE_PATH =
"/sdcard/WindowInsetsBehaviorTests";
private static final String SETTINGS_PACKAGE_NAME = "com.android.settings";
private static final String ARGUMENT_KEY_FORCE_ENABLE = "force_enable_gesture_navigation";
private static final String NAV_BAR_INTERACTION_MODE_RES_NAME = "config_navBarInteractionMode";
private static final int STEPS = 10;
// The minimum value of the system gesture exclusion limit is 200 dp. The value here should be
// greater than that, so that we can test if the limit can be changed by DeviceConfig or not.
private static final int EXCLUSION_LIMIT_DP = 210;
private static final int NAV_BAR_INTERACTION_MODE_GESTURAL = 2;
private final boolean mForceEnableGestureNavigation;
private final Map<String, Boolean> mSystemGestureOptionsMap;
private float mPixelsPerDp;
private float mDensityPerCm;
private int mDisplayWidth;
private int mExclusionLimit;
private UiDevice mDevice;
// Bounds for actions like swipe and click.
private Rect mActionBounds;
private String mEdgeToEdgeNavigationTitle;
private String mSystemNavigationTitle;
private String mGesturePreferenceTitle;
private TouchHelper mTouchHelper;
private boolean mConfiguredInSettings;
private static String getSettingsString(Resources res, String strResName) {
int resIdString = res.getIdentifier(strResName, "string", SETTINGS_PACKAGE_NAME);
if (resIdString <= 0x7f000000) {
return null; /* most of application res id must be larger than 0x7f000000 */
}
return res.getString(resIdString);
}
/**
* To initial all of options in System Gesture.
*/
public WindowInsetsBehaviorTests() {
Bundle bundle = InstrumentationRegistry.getArguments();
mForceEnableGestureNavigation = (bundle != null)
&& "true".equalsIgnoreCase(bundle.getString(ARGUMENT_KEY_FORCE_ENABLE));
mSystemGestureOptionsMap = new ArrayMap();
if (!mForceEnableGestureNavigation) {
return;
}
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
PackageManager packageManager = context.getPackageManager();
Resources res = null;
try {
res = packageManager.getResourcesForApplication(SETTINGS_PACKAGE_NAME);
} catch (PackageManager.NameNotFoundException e) {
return;
}
if (res == null) {
return;
}
mEdgeToEdgeNavigationTitle = getSettingsString(res, "edge_to_edge_navigation_title");
mGesturePreferenceTitle = getSettingsString(res, "gesture_preference_title");
mSystemNavigationTitle = getSettingsString(res, "system_navigation_title");
String text = getSettingsString(res, "edge_to_edge_navigation_title");
if (text != null) {
mSystemGestureOptionsMap.put(text, false);
}
text = getSettingsString(res, "swipe_up_to_switch_apps_title");
if (text != null) {
mSystemGestureOptionsMap.put(text, false);
}
text = getSettingsString(res, "legacy_navigation_title");
if (text != null) {
mSystemGestureOptionsMap.put(text, false);
}
mConfiguredInSettings = false;
}
@Rule
public ScreenshotTestRule mScreenshotTestRule =
new ScreenshotTestRule(DEF_SCREENSHOT_BASE_PATH);
@Rule
public ActivityTestRule<WindowInsetsActivity> mActivityRule = new ActivityTestRule<>(
WindowInsetsActivity.class, true, false);
@Rule
public RuleChain mRuleChain = RuleChain.outerRule(mActivityRule)
.around(mScreenshotTestRule);
private WindowInsetsActivity mActivity;
private WindowInsets mContentViewWindowInsets;
private List<Point> mActionCancelPoints;
private List<Point> mActionDownPoints;
private List<Point> mActionUpPoints;
private Context mTargetContext;
private int mClickCount;
private void mainThreadRun(Runnable runnable) {
getInstrumentation().runOnMainSync(runnable);
mDevice.waitForIdle();
}
private boolean hasSystemGestureFeature() {
final PackageManager pm = mTargetContext.getPackageManager();
// No bars on embedded devices.
// No bars on TVs and watches.
return !(pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
|| pm.hasSystemFeature(PackageManager.FEATURE_EMBEDDED)
|| pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|| pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE));
}
private UiObject2 findSystemNavigationObject(String text, boolean addCheckSelector) {
BySelector widgetFrameSelector = By.res("android", "widget_frame");
BySelector checkboxSelector = By.checkable(true);
if (addCheckSelector) {
checkboxSelector = checkboxSelector.checked(true);
}
BySelector textSelector = By.text(text);
BySelector targetSelector = By.hasChild(widgetFrameSelector).hasDescendant(textSelector)
.hasDescendant(checkboxSelector);
return mDevice.findObject(targetSelector);
}
private boolean launchToSettingsSystemGesture() {
if (!mForceEnableGestureNavigation) {
return false;
}
/* launch to the close to the system gesture fragment */
Intent intent = new Intent(Intent.ACTION_MAIN);
ComponentName settingComponent = new ComponentName(SETTINGS_PACKAGE_NAME,
String.format("%s.%s$%s", SETTINGS_PACKAGE_NAME, "Settings",
"SystemDashboardActivity"));
intent.setComponent(settingComponent);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
mTargetContext.startActivity(intent);
// Wait for the app to appear
mDevice.wait(Until.hasObject(By.pkg("com.android.settings").depth(0)),
5000);
mDevice.wait(Until.hasObject(By.text(mGesturePreferenceTitle)), 5000);
if (mDevice.findObject(By.text(mGesturePreferenceTitle)) == null) {
return false;
}
mDevice.findObject(By.text(mGesturePreferenceTitle)).click();
mDevice.wait(Until.hasObject(By.text(mSystemNavigationTitle)), 5000);
if (mDevice.findObject(By.text(mSystemNavigationTitle)) == null) {
return false;
}
mDevice.findObject(By.text(mSystemNavigationTitle)).click();
mDevice.wait(Until.hasObject(By.text(mEdgeToEdgeNavigationTitle)), 5000);
return mDevice.hasObject(By.text(mEdgeToEdgeNavigationTitle));
}
private void leaveSettings() {
mDevice.pressBack(); /* Back to Gesture */
mDevice.waitForIdle();
mDevice.pressBack(); /* Back to System */
mDevice.waitForIdle();
mDevice.pressBack(); /* back to Settings */
mDevice.waitForIdle();
mDevice.pressBack(); /* Back to Home */
mDevice.waitForIdle();
mDevice.pressHome(); /* double confirm back to home */
mDevice.waitForIdle();
}
/**
* To prepare the things needed to run the tests.
* <p>
* There are several things needed to prepare
* * return to home screen
* * launch the activity
* * pixel per dp
* * the WindowInsets that received by the content view of activity
* </p>
* @throws Exception caused by permission, nullpointer, etc.
*/
@Before
public void setUp() throws Exception {
mDevice = UiDevice.getInstance(getInstrumentation());
mTouchHelper = new TouchHelper(getInstrumentation());
mTargetContext = getInstrumentation().getTargetContext();
if (!hasSystemGestureFeature()) {
return;
}
final DisplayManager dm = mTargetContext.getSystemService(DisplayManager.class);
final Display display = dm.getDisplay(Display.DEFAULT_DISPLAY);
final DisplayMetrics metrics = new DisplayMetrics();
display.getRealMetrics(metrics);
mPixelsPerDp = metrics.density;
mDensityPerCm = (int) ((float) metrics.densityDpi / 2.54);
mDisplayWidth = metrics.widthPixels;
mExclusionLimit = (int) (EXCLUSION_LIMIT_DP * mPixelsPerDp);
// To setup the Edge to Edge environment by do the operation on Settings
boolean isOperatedSettingsToExpectedOption = launchToSettingsSystemGesture();
if (isOperatedSettingsToExpectedOption) {
for (Map.Entry<String, Boolean> entry : mSystemGestureOptionsMap.entrySet()) {
UiObject2 uiObject2 = findSystemNavigationObject(entry.getKey(), true);
entry.setValue(uiObject2 != null);
}
UiObject2 edgeToEdgeObj = mDevice.findObject(By.text(mEdgeToEdgeNavigationTitle));
if (edgeToEdgeObj != null) {
edgeToEdgeObj.click();
mConfiguredInSettings = true;
}
}
mDevice.waitForIdle();
leaveSettings();
mDevice.pressHome();
mDevice.waitForIdle();
// launch the Activity and wait until Activity onAttach
CountDownLatch latch = new CountDownLatch(1);
mActivity = launchActivity();
mActivity.setInitialFinishCallBack(isFinish -> latch.countDown());
mDevice.waitForIdle();
latch.await(5, SECONDS);
}
private WindowInsetsActivity launchActivity() {
final ActivityOptions options= ActivityOptions.makeBasic();
options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN);
final WindowInsetsActivity[] activity = (WindowInsetsActivity[]) Array.newInstance(
WindowInsetsActivity.class, 1);
SystemUtil.runWithShellPermissionIdentity(() -> {
activity[0] = (WindowInsetsActivity) getInstrumentation().startActivitySync(
new Intent(getInstrumentation().getTargetContext(), WindowInsetsActivity.class)
.addFlags(FLAG_ACTIVITY_NEW_TASK), options.toBundle());
});
return activity[0];
}
/**
* Restore the original configured value for the system gesture by operating Settings.
*/
@After
public void tearDown() {
if (!hasSystemGestureFeature()) {
return;
}
if (mConfiguredInSettings) {
launchToSettingsSystemGesture();
for (Map.Entry<String, Boolean> entry : mSystemGestureOptionsMap.entrySet()) {
if (entry.getValue()) {
UiObject2 uiObject2 = findSystemNavigationObject(entry.getKey(), false);
if (uiObject2 != null) {
uiObject2.click();
}
}
}
leaveSettings();
}
}
private void swipeByUiDevice(Point p1, Point p2) {
mDevice.swipe(p1.x, p1.y, p2.x, p2.y, STEPS);
}
private void clickAndWaitByUiDevice(Point p) {
CountDownLatch latch = new CountDownLatch(1);
mActivity.setOnClickConsumer((view) -> {
latch.countDown();
});
// mDevice.click(p.x, p.y) has the limitation without consideration of the cutout
if (!mTouchHelper.click(p.x, p.y)) {
fail("Can't inject event at" + p);
}
/* wait until the OnClickListener triggered, and then click the next point */
try {
latch.await(5, SECONDS);
} catch (InterruptedException e) {
fail("Wait too long and onClickEvent doesn't receive");
}
if (latch.getCount() > 0) {
fail("Doesn't receive onClickEvent at " + p);
}
}
private int swipeBigX(Rect viewBoundary, BiConsumer<Point, Point> callback) {
final int theLeftestLine = viewBoundary.left + 1;
final int theToppestLine = viewBoundary.top + 1;
final int theRightestLine = viewBoundary.right - 1;
final int theBottomestLine = viewBoundary.bottom - 1;
if (callback != null) {
callback.accept(new Point(theLeftestLine, theToppestLine),
new Point(theRightestLine, theBottomestLine));
}
mDevice.waitForIdle();
if (callback != null) {
callback.accept(new Point(theRightestLine, theToppestLine),
new Point(viewBoundary.left, theBottomestLine));
}
mDevice.waitForIdle();
return 2;
}
private int clickAllOfHorizontalSamplePoints(Rect viewBoundary, int y,
Consumer<Point> callback) {
final int theLeftestLine = viewBoundary.left + 1;
final int theRightestLine = viewBoundary.right - 1;
final float interval = mDensityPerCm;
int count = 0;
for (int i = theLeftestLine; i < theRightestLine; i += interval) {
if (callback != null) {
callback.accept(new Point(i, y));
}
mDevice.waitForIdle();
count++;
}
if (callback != null) {
callback.accept(new Point(theRightestLine, y));
}
mDevice.waitForIdle();
count++;
return count;
}
private int clickAllOfSamplePoints(Rect viewBoundary, Consumer<Point> callback) {
final int theToppestLine = viewBoundary.top + 1;
final int theBottomestLine = viewBoundary.bottom - 1;
final float interval = mDensityPerCm;
int count = 0;
for (int i = theToppestLine; i < theBottomestLine; i += interval) {
count += clickAllOfHorizontalSamplePoints(viewBoundary, i, callback);
}
count += clickAllOfHorizontalSamplePoints(viewBoundary, theBottomestLine, callback);
return count;
}
private int swipeAllOfHorizontalLinesFromLeftToRight(Rect viewBoundary,
BiConsumer<Point, Point> callback) {
final int theLeftestLine = viewBoundary.left + 1;
final int theToppestLine = viewBoundary.top + 1;
final int theBottomestLine = viewBoundary.bottom - 1;
int count = 0;
for (int i = theToppestLine; i < theBottomestLine; i += mDensityPerCm * 2) {
if (callback != null) {
callback.accept(new Point(theLeftestLine, i),
new Point(viewBoundary.centerX(), i));
}
mDevice.waitForIdle();
count++;
}
if (callback != null) {
callback.accept(new Point(theLeftestLine, theBottomestLine),
new Point(viewBoundary.centerX(), theBottomestLine));
}
mDevice.waitForIdle();
count++;
return count;
}
private int swipeAllOfHorizontalLinesFromRightToLeft(Rect viewBoundary,
BiConsumer<Point, Point> callback) {
final int theToppestLine = viewBoundary.top + 1;
final int theRightestLine = viewBoundary.right - 1;
final int theBottomestLine = viewBoundary.bottom - 1;
int count = 0;
for (int i = theToppestLine; i < theBottomestLine; i += mDensityPerCm * 2) {
if (callback != null) {
callback.accept(new Point(theRightestLine, i),
new Point(viewBoundary.centerX(), i));
}
mDevice.waitForIdle();
count++;
}
if (callback != null) {
callback.accept(new Point(theRightestLine, theBottomestLine),
new Point(viewBoundary.centerX(), theBottomestLine));
}
mDevice.waitForIdle();
count++;
return count;
}
private int swipeAllOfHorizontalLines(Rect viewBoundary, BiConsumer<Point, Point> callback) {
int count = 0;
count += swipeAllOfHorizontalLinesFromLeftToRight(viewBoundary, callback);
count += swipeAllOfHorizontalLinesFromRightToLeft(viewBoundary, callback);
return count;
}
private int swipeAllOfVerticalLinesFromTopToBottom(Rect viewBoundary,
BiConsumer<Point, Point> callback) {
final int theLeftestLine = viewBoundary.left + 1;
final int theToppestLine = viewBoundary.top + 1;
final int theRightestLine = viewBoundary.right - 1;
int count = 0;
for (int i = theLeftestLine; i < theRightestLine; i += mDensityPerCm * 2) {
if (callback != null) {
callback.accept(new Point(i, theToppestLine),
new Point(i, viewBoundary.centerY()));
}
mDevice.waitForIdle();
count++;
}
if (callback != null) {
callback.accept(new Point(theRightestLine, theToppestLine),
new Point(theRightestLine, viewBoundary.centerY()));
}
mDevice.waitForIdle();
count++;
return count;
}
private int swipeAllOfVerticalLinesFromBottomToTop(Rect viewBoundary,
BiConsumer<Point, Point> callback) {
final int theLeftestLine = viewBoundary.left + 1;
final int theRightestLine = viewBoundary.right - 1;
final int theBottomestLine = viewBoundary.bottom - 1;
int count = 0;
for (int i = theLeftestLine; i < theRightestLine; i += mDensityPerCm * 2) {
if (callback != null) {
callback.accept(new Point(i, theBottomestLine),
new Point(i, viewBoundary.centerY()));
}
mDevice.waitForIdle();
count++;
}
if (callback != null) {
callback.accept(new Point(theRightestLine, theBottomestLine),
new Point(theRightestLine, viewBoundary.centerY()));
}
mDevice.waitForIdle();
count++;
return count;
}
private int swipeAllOfVerticalLines(Rect viewBoundary, BiConsumer<Point, Point> callback) {
int count = 0;
count += swipeAllOfVerticalLinesFromTopToBottom(viewBoundary, callback);
count += swipeAllOfVerticalLinesFromBottomToTop(viewBoundary, callback);
return count;
}
private int swipeInViewBoundary(Rect viewBoundary, BiConsumer<Point, Point> callback) {
int count = 0;
count += swipeBigX(viewBoundary, callback);
count += swipeAllOfHorizontalLines(viewBoundary, callback);
count += swipeAllOfVerticalLines(viewBoundary, callback);
return count;
}
private int swipeInViewBoundary(Rect viewBoundary) {
return swipeInViewBoundary(viewBoundary, this::swipeByUiDevice);
}
private List<Rect> splitBoundsAccordingToExclusionLimit(Rect rect) {
final int exclusionHeightLimit = (int) (EXCLUSION_LIMIT_DP * mPixelsPerDp + 0.5f);
final List<Rect> bounds = new ArrayList<>();
int nextTop = rect.top;
while (nextTop < rect.bottom) {
final int top = nextTop;
int bottom = top + exclusionHeightLimit;
if (bottom > rect.bottom) {
bottom = rect.bottom;
}
bounds.add(new Rect(rect.left, top, rect.right, bottom));
nextTop = bottom;
}
return bounds;
}
/**
* @throws Throwable when setting the property goes wrong.
*/
@CtsDownstreamingTest
@Test
public void systemGesture_excludeViewRects_withoutAnyCancel()
throws Throwable {
assumeTrue(ApiLevelUtil.isAtLeast(Build.VERSION_CODES.S_V2));
assumeTrue(hasSystemGestureFeature());
mainThreadRun(() -> mContentViewWindowInsets = mActivity.getDecorViewWindowInsets());
mainThreadRun(() -> mActionBounds = mActivity.getActionBounds(
mContentViewWindowInsets.getSystemGestureInsets(), mContentViewWindowInsets));
final Rect exclusionRect = new Rect();
mainThreadRun(() -> exclusionRect.set(mActivity.getSystemGestureExclusionBounds(
mContentViewWindowInsets.getMandatorySystemGestureInsets(),
mContentViewWindowInsets)));
final int[] swipeCount = {0};
doInExclusionLimitSession(() -> {
final List<Rect> swipeBounds = splitBoundsAccordingToExclusionLimit(mActionBounds);
final List<Rect> exclusionRects = splitBoundsAccordingToExclusionLimit(exclusionRect);
final int size = swipeBounds.size();
for (int i = 0; i < size; i++) {
setAndWaitForSystemGestureExclusionRectsListenerTrigger(exclusionRects.get(i));
swipeCount[0] += swipeInViewBoundary(swipeBounds.get(i));
}
});
mainThreadRun(() -> {
mActionDownPoints = mActivity.getActionDownPoints();
mActionUpPoints = mActivity.getActionUpPoints();
mActionCancelPoints = mActivity.getActionCancelPoints();
});
mScreenshotTestRule.capture();
assertEquals(0, mActionCancelPoints.size());
assertEquals(swipeCount[0], mActionUpPoints.size());
assertEquals(swipeCount[0], mActionDownPoints.size());
}
@CtsDownstreamingTest
@Test
public void systemGesture_notExcludeViewRects_withoutAnyCancel() {
assumeTrue(ApiLevelUtil.isAtLeast(Build.VERSION_CODES.S_V2));
assumeTrue(hasSystemGestureFeature());
mainThreadRun(() -> mActivity.setSystemGestureExclusion(null));
mainThreadRun(() -> mContentViewWindowInsets = mActivity.getDecorViewWindowInsets());
mainThreadRun(() -> mActionBounds = mActivity.getActionBounds(
mContentViewWindowInsets.getSystemGestureInsets(), mContentViewWindowInsets));
final int swipeCount = swipeInViewBoundary(mActionBounds);
mainThreadRun(() -> {
mActionDownPoints = mActivity.getActionDownPoints();
mActionUpPoints = mActivity.getActionUpPoints();
mActionCancelPoints = mActivity.getActionCancelPoints();
});
mScreenshotTestRule.capture();
assertEquals(0, mActionCancelPoints.size());
assertEquals(swipeCount, mActionUpPoints.size());
assertEquals(swipeCount, mActionDownPoints.size());
}
@Test
public void tappableElements_tapSamplePoints_excludeViewRects_withoutAnyCancel()
throws InterruptedException {
assumeTrue(hasSystemGestureFeature());
mainThreadRun(() -> mContentViewWindowInsets = mActivity.getDecorViewWindowInsets());
mainThreadRun(() -> mActionBounds = mActivity.getActionBounds(
mContentViewWindowInsets.getTappableElementInsets(), mContentViewWindowInsets));
final int count = clickAllOfSamplePoints(mActionBounds, this::clickAndWaitByUiDevice);
mainThreadRun(() -> {
mClickCount = mActivity.getClickCount();
mActionCancelPoints = mActivity.getActionCancelPoints();
});
mScreenshotTestRule.capture();
assertEquals("The number of click not match", count, mClickCount);
assertEquals("The Number of the canceled points not match", 0,
mActionCancelPoints.size());
}
@Test
public void tappableElements_tapSamplePoints_notExcludeViewRects_withoutAnyCancel() {
assumeTrue(hasSystemGestureFeature());
mainThreadRun(() -> mActivity.setSystemGestureExclusion(null));
mainThreadRun(() -> mContentViewWindowInsets = mActivity.getDecorViewWindowInsets());
mainThreadRun(() -> mActionBounds = mActivity.getActionBounds(
mContentViewWindowInsets.getTappableElementInsets(), mContentViewWindowInsets));
final int count = clickAllOfSamplePoints(mActionBounds, this::clickAndWaitByUiDevice);
mainThreadRun(() -> {
mClickCount = mActivity.getClickCount();
mActionCancelPoints = mActivity.getActionCancelPoints();
});
mScreenshotTestRule.capture();
assertEquals("The number of click not match", count, mClickCount);
assertEquals("The Number of the canceled points not match", 0,
mActionCancelPoints.size());
}
@Test
public void swipeInsideLimit_systemUiVisible_noEventCanceled() throws Throwable {
assumeTrue(hasSystemGestureFeature());
final int swipeCount = 1;
final boolean insideLimit = true;
testSystemGestureExclusionLimit(swipeCount, insideLimit, SYSTEM_UI_FLAG_VISIBLE);
assertEquals("Swipe must not be canceled.", 0, mActionCancelPoints.size());
assertEquals("Action up points.", swipeCount, mActionUpPoints.size());
assertEquals("Action down points.", swipeCount, mActionDownPoints.size());
}
@Test
public void swipeOutsideLimit_systemUiVisible_allEventsCanceled() throws Throwable {
assumeTrue(hasSystemGestureFeature());
assumeGestureNavigationMode();
final int swipeCount = 1;
final boolean insideLimit = false;
testSystemGestureExclusionLimit(swipeCount, insideLimit, SYSTEM_UI_FLAG_VISIBLE);
assertEquals("Swipe must be always canceled.", swipeCount, mActionCancelPoints.size());
assertEquals("Action up points.", 0, mActionUpPoints.size());
assertEquals("Action down points.", swipeCount, mActionDownPoints.size());
}
@Test
public void swipeInsideLimit_immersiveSticky_noEventCanceled() throws Throwable {
assumeTrue(hasSystemGestureFeature());
// The first event may be never canceled. So we need to swipe at least twice.
final int swipeCount = 2;
final boolean insideLimit = true;
testSystemGestureExclusionLimit(swipeCount, insideLimit, SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_HIDE_NAVIGATION);
assertEquals("Swipe must not be canceled.", 0, mActionCancelPoints.size());
assertEquals("Action up points.", swipeCount, mActionUpPoints.size());
assertEquals("Action down points.", swipeCount, mActionDownPoints.size());
}
@Test
public void swipeOutsideLimit_immersiveSticky_noEventCanceled() throws Throwable {
assumeTrue(hasSystemGestureFeature());
// The first event may be never canceled. So we need to swipe at least twice.
final int swipeCount = 2;
final boolean insideLimit = false;
testSystemGestureExclusionLimit(swipeCount, insideLimit, SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_HIDE_NAVIGATION);
assertEquals("Swipe must not be canceled.", 0, mActionCancelPoints.size());
assertEquals("Action up points.", swipeCount, mActionUpPoints.size());
assertEquals("Action down points.", swipeCount, mActionDownPoints.size());
}
private void testSystemGestureExclusionLimit(int swipeCount, boolean insideLimit,
int systemUiVisibility) throws Throwable {
final int shiftY = insideLimit ? 1 : -1;
assumeGestureNavigation();
doInExclusionLimitSession(() -> {
setSystemUiVisibility(systemUiVisibility);
setAndWaitForSystemGestureExclusionRectsListenerTrigger(null);
final Rect swipeBounds = new Rect();
mainThreadRun(() -> {
final View rootView = mActivity.getWindow().getDecorView();
swipeBounds.set(mActivity.getViewBoundOnScreen(rootView));
});
// The limit is consumed from bottom to top.
final int swipeY = swipeBounds.bottom - mExclusionLimit + shiftY;
for (int i = 0; i < swipeCount; i++) {
mDevice.swipe(swipeBounds.left, swipeY, swipeBounds.right, swipeY, STEPS);
}
mainThreadRun(() -> {
mActionDownPoints = mActivity.getActionDownPoints();
mActionUpPoints = mActivity.getActionUpPoints();
mActionCancelPoints = mActivity.getActionCancelPoints();
});
});
}
private void assumeGestureNavigation() {
final Insets[] insets = new Insets[1];
mainThreadRun(() -> {
final View view = mActivity.getWindow().getDecorView();
insets[0] = view.getRootWindowInsets().getSystemGestureInsets();
});
assumeTrue("Gesture navigation required.", insets[0].left > 0);
}
private void assumeGestureNavigationMode() {
// TODO: b/153032202 consider the CTS on GSI case.
Resources res = mTargetContext.getResources();
int naviMode = res.getIdentifier(NAV_BAR_INTERACTION_MODE_RES_NAME, "integer", "android");
assumeTrue("Gesture navigation required", naviMode == NAV_BAR_INTERACTION_MODE_GESTURAL);
}
/**
* Set system UI visibility and wait for it is applied by the system.
*
* @param flags the visibility flags.
* @throws InterruptedException when the test gets aborted.
*/
private void setSystemUiVisibility(int flags) throws InterruptedException {
final CountDownLatch flagsApplied = new CountDownLatch(1);
final int targetFlags = SYSTEM_UI_CLEARABLE_FLAGS & flags;
mainThreadRun(() -> {
final View view = mActivity.getWindow().getDecorView();
if ((view.getSystemUiVisibility() & SYSTEM_UI_CLEARABLE_FLAGS) == targetFlags) {
// System UI visibility is already what we want. Stop waiting for the callback.
flagsApplied.countDown();
return;
}
view.setOnSystemUiVisibilityChangeListener(visibility -> {
if (visibility == targetFlags) {
flagsApplied.countDown();
}
});
view.setSystemUiVisibility(flags);
});
assertTrue("System UI visibility must be applied.", flagsApplied.await(3, SECONDS));
}
/**
* Set an exclusion rectangle and wait for it is applied by the system.
* <p>
* if the parameter rect doesn't provide or is null, the decorView will be used to set into
* the exclusion rects.
* </p>
*
* @param rect the rectangle that is added into the system gesture exclusion rects.
* @throws InterruptedException when the test gets aborted.
*/
private void setAndWaitForSystemGestureExclusionRectsListenerTrigger(Rect rect)
throws InterruptedException {
final CountDownLatch exclusionApplied = new CountDownLatch(1);
mainThreadRun(() -> {
final View view = mActivity.getWindow().getDecorView();
final ViewTreeObserver vto = view.getViewTreeObserver();
vto.addOnSystemGestureExclusionRectsChangedListener(
rects -> exclusionApplied.countDown());
Rect exclusiveRect = new Rect(0, 0, view.getWidth(), view.getHeight());
if (rect != null) {
exclusiveRect = rect;
}
view.setSystemGestureExclusionRects(Lists.newArrayList(exclusiveRect));
});
assertTrue("Exclusion must be applied.", exclusionApplied.await(3, SECONDS));
}
/**
* Run the given task while the system gesture exclusion limit has been changed to
* {@link #EXCLUSION_LIMIT_DP}, and then restore the value while the task is finished.
*
* @param task the task to be run.
* @throws Throwable when something goes unexpectedly.
*/
private static void doInExclusionLimitSession(ThrowingRunnable task) throws Throwable {
final int[] originalLimitDp = new int[1];
SystemUtil.runWithShellPermissionIdentity(() -> {
originalLimitDp[0] = DeviceConfig.getInt(NAMESPACE_ANDROID,
KEY_SYSTEM_GESTURE_EXCLUSION_LIMIT_DP, -1);
DeviceConfig.setProperty(
NAMESPACE_ANDROID,
KEY_SYSTEM_GESTURE_EXCLUSION_LIMIT_DP,
Integer.toString(EXCLUSION_LIMIT_DP), false /* makeDefault */);
});
try {
task.run();
} finally {
// Restore the value
SystemUtil.runWithShellPermissionIdentity(() -> DeviceConfig.setProperty(
NAMESPACE_ANDROID,
KEY_SYSTEM_GESTURE_EXCLUSION_LIMIT_DP,
(originalLimitDp[0] != -1) ? Integer.toString(originalLimitDp[0]) : null,
false /* makeDefault */));
}
}
}