blob: 65465b278959c10905e3cc0a8271b1fbd1dd5ced [file] [log] [blame]
/*
* Copyright (C) 2020 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.server.wm;
import static android.graphics.PixelFormat.TRANSLUCENT;
import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.KEYCODE_BACK;
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;
import static android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.statusBars;
import static android.view.WindowInsets.Type.systemBars;
import static android.view.WindowInsets.Type.systemGestures;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeThat;
import static org.junit.Assume.assumeTrue;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.SystemClock;
import android.platform.test.annotations.Presubmit;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.android.compatibility.common.util.PollingCheck;
import com.android.cts.mockime.ImeEventStream;
import com.android.cts.mockime.ImeSettings;
import com.android.cts.mockime.MockImeSession;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
* Test whether WindowInsetsController controls window insets as expected.
*
* Build/Install/Run:
* atest CtsWindowManagerDeviceTestCases:WindowInsetsControllerTests
*/
@Presubmit
public class WindowInsetsControllerTests extends WindowManagerTestBase {
private final static long TIMEOUT = 1000; // milliseconds
private final static long TIMEOUT_UPDATING_INPUT_WINDOW = 500; // milliseconds
private final static long TIME_SLICE = 50; // milliseconds
private final static AnimationCallback ANIMATION_CALLBACK = new AnimationCallback();
private static final String AM_BROADCAST_CLOSE_SYSTEM_DIALOGS =
"am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS";
@Rule
public final ErrorCollector mErrorCollector = new ErrorCollector();
@Test
public void testHide() {
final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
testHideInternal(rootView, statusBars());
testHideInternal(rootView, navigationBars());
}
private void testHideInternal(View rootView, int types) {
if (rootView.getRootWindowInsets().isVisible(types)) {
getInstrumentation().runOnMainSync(() -> {
rootView.getWindowInsetsController().hide(types);
});
PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
}
}
@Test
public void testShow() {
final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
testShowInternal(rootView, statusBars());
testShowInternal(rootView, navigationBars());
}
private void testShowInternal(View rootView, int types) {
if (rootView.getRootWindowInsets().isVisible(types)) {
getInstrumentation().runOnMainSync(() -> {
rootView.getWindowInsetsController().hide(types);
});
PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
getInstrumentation().runOnMainSync(() -> {
rootView.getWindowInsetsController().show(types);
});
PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
}
}
private void testTopAppHidesStatusBarInternal(Activity activity, View rootView,
Runnable hidingStatusBar) {
if (rootView.getRootWindowInsets().isVisible(statusBars())) {
// The top-fullscreen-app window hides status bar.
getInstrumentation().runOnMainSync(hidingStatusBar);
PollingCheck.waitFor(TIMEOUT,
() -> !rootView.getRootWindowInsets().isVisible(statusBars()));
// Add a non-fullscreen window on top of the fullscreen window.
// The new focused window doesn't hide status bar.
getInstrumentation().runOnMainSync(
() -> activity.getWindowManager().addView(
new View(activity),
new WindowManager.LayoutParams(1 /* w */, 1 /* h */, TYPE_APPLICATION,
0 /* flags */, TRANSLUCENT)));
// Check if status bar stays invisible.
for (long time = TIMEOUT; time >= 0; time -= TIME_SLICE) {
assertFalse(rootView.getRootWindowInsets().isVisible(statusBars()));
SystemClock.sleep(TIME_SLICE);
}
}
}
@Test
public void testTopAppHidesStatusBarByMethod() {
final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
testTopAppHidesStatusBarInternal(activity, rootView,
() -> rootView.getWindowInsetsController().hide(statusBars()));
}
@Test
public void testTopAppHidesStatusBarByWindowFlag() {
final TestActivity activity = startActivity(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
testTopAppHidesStatusBarInternal(activity, rootView,
() -> activity.getWindow().addFlags(FLAG_FULLSCREEN));
}
@Test
public void testTopAppHidesStatusBarBySystemUiFlag() {
final TestActivity activity = startActivity(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
testTopAppHidesStatusBarInternal(activity, rootView,
() -> rootView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN));
}
@Test
public void testImeShowAndHide() throws Exception {
final Instrumentation instrumentation = getInstrumentation();
assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
nullValue());
final MockImeSession imeSession = MockImeHelper.createManagedMockImeSession(this);
final ImeEventStream stream = imeSession.openEventStream();
final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
expectEvent(stream, editorMatcher("onStartInput", activity.mEditTextMarker), TIMEOUT);
final View rootView = activity.getWindow().getDecorView();
getInstrumentation().runOnMainSync(() -> rootView.getWindowInsetsController().show(ime()));
PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(ime()));
getInstrumentation().runOnMainSync(() -> rootView.getWindowInsetsController().hide(ime()));
PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(ime()));
}
@Test
public void testImeForceShowingNavigationBar() throws Exception {
final Instrumentation instrumentation = getInstrumentation();
assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
nullValue());
final MockImeSession imeSession = MockImeHelper.createManagedMockImeSession(this);
final ImeEventStream stream = imeSession.openEventStream();
final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
expectEvent(stream, editorMatcher("onStartInput", activity.mEditTextMarker), TIMEOUT);
final View rootView = activity.getWindow().getDecorView();
assumeTrue(rootView.getRootWindowInsets().isVisible(navigationBars()));
getInstrumentation().runOnMainSync(
() -> rootView.getWindowInsetsController().hide(navigationBars()));
PollingCheck.waitFor(TIMEOUT,
() -> !rootView.getRootWindowInsets().isVisible(navigationBars()));
getInstrumentation().runOnMainSync(() -> rootView.getWindowInsetsController().show(ime()));
PollingCheck.waitFor(TIMEOUT,
() -> rootView.getRootWindowInsets().isVisible(ime() | navigationBars()));
getInstrumentation().runOnMainSync(() -> rootView.getWindowInsetsController().hide(ime()));
PollingCheck.waitFor(TIMEOUT,
() -> !rootView.getRootWindowInsets().isVisible(ime())
&& !rootView.getRootWindowInsets().isVisible(navigationBars()));
}
@Test
public void testSetSystemBarsAppearance() {
final TestActivity activity = startActivity(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
final WindowInsetsController controller = rootView.getWindowInsetsController();
getInstrumentation().runOnMainSync(() -> {
// Set APPEARANCE_LIGHT_STATUS_BARS.
controller.setSystemBarsAppearance(
APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS);
// Clear APPEARANCE_LIGHT_NAVIGATION_BARS.
controller.setSystemBarsAppearance(
0 /* appearance */, APPEARANCE_LIGHT_NAVIGATION_BARS);
});
waitForIdle();
// We must have APPEARANCE_LIGHT_STATUS_BARS, but not APPEARANCE_LIGHT_NAVIGATION_BARS.
assertEquals(APPEARANCE_LIGHT_STATUS_BARS,
controller.getSystemBarsAppearance()
& (APPEARANCE_LIGHT_STATUS_BARS | APPEARANCE_LIGHT_NAVIGATION_BARS));
final boolean[] onPreDrawCalled = { false };
rootView.getViewTreeObserver().addOnPreDrawListener(() -> {
onPreDrawCalled[0] = true;
return true;
});
// Clear APPEARANCE_LIGHT_NAVIGATION_BARS again.
getInstrumentation().runOnMainSync(() -> controller.setSystemBarsAppearance(
0 /* appearance */, APPEARANCE_LIGHT_NAVIGATION_BARS));
waitForIdle();
assertFalse("Setting the same appearance must not cause a new traversal",
onPreDrawCalled[0]);
}
@Test
public void testSetSystemBarsBehavior_default() throws InterruptedException {
final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
// Assume we have the bars and they can be visible.
final int types = statusBars();
assumeTrue(rootView.getRootWindowInsets().isVisible(types));
rootView.getWindowInsetsController().setSystemBarsBehavior(BEHAVIOR_DEFAULT);
hideInsets(rootView, types);
// Tapping on display cannot show bars.
tapOnDisplay(rootView.getWidth() / 2f, rootView.getHeight() / 2f);
PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
// Wait for status bar invisible from InputDispatcher. Otherwise, the following
// dragFromTopToCenter might expand notification shade.
SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW);
// Swiping from top of display can show bars.
dragFromTopToCenter(rootView);
PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
}
@Test
public void testSetSystemBarsBehavior_showTransientBarsBySwipe() throws InterruptedException {
final TestActivity activity = startActivity(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
// Assume we have the bars and they can be visible.
final int types = statusBars();
assumeTrue(rootView.getRootWindowInsets().isVisible(types));
rootView.getWindowInsetsController().setSystemBarsBehavior(
BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
hideInsets(rootView, types);
// Tapping on display cannot show bars.
tapOnDisplay(rootView.getWidth() / 2f, rootView.getHeight() / 2f);
PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
// Wait for status bar invisible from InputDispatcher. Otherwise, the following
// dragFromTopToCenter might expand notification shade.
SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW);
// Swiping from top of display can show transient bars, but apps cannot detect that.
dragFromTopToCenter(rootView);
// Make sure status bar stays invisible.
for (long time = TIMEOUT; time >= 0; time -= TIME_SLICE) {
assertFalse(rootView.getRootWindowInsets().isVisible(types));
SystemClock.sleep(TIME_SLICE);
}
}
@Test
public void testSetSystemBarsBehavior_systemGesture_default() throws InterruptedException {
final TestActivity activity = startActivity(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
// Assume the current navigation mode has the back gesture.
assumeTrue(rootView.getRootWindowInsets().getInsets(systemGestures()).left > 0);
assumeTrue(canTriggerBackGesture(rootView));
rootView.getWindowInsetsController().setSystemBarsBehavior(BEHAVIOR_DEFAULT);
hideInsets(rootView, systemBars());
// Test if the back gesture can be triggered while system bars are hidden with the behavior.
assertTrue(canTriggerBackGesture(rootView));
}
@Test
public void testSetSystemBarsBehavior_systemGesture_showTransientBarsBySwipe()
throws InterruptedException {
final TestActivity activity = startActivity(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
// Assume the current navigation mode has the back gesture.
assumeTrue(rootView.getRootWindowInsets().getInsets(systemGestures()).left > 0);
assumeTrue(canTriggerBackGesture(rootView));
rootView.getWindowInsetsController().setSystemBarsBehavior(
BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
hideInsets(rootView, systemBars());
// Test if the back gesture can be triggered while system bars are hidden with the behavior.
assertFalse(canTriggerBackGesture(rootView));
}
private boolean canTriggerBackGesture(View rootView) throws InterruptedException {
final boolean[] hasBack = { false };
final CountDownLatch latch = new CountDownLatch(1);
rootView.findFocus().setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KEYCODE_BACK && event.getAction() == ACTION_DOWN) {
hasBack[0] = true;
latch.countDown();
return true;
}
return false;
});
dragFromLeftToCenter(rootView);
latch.await(1, TimeUnit.SECONDS);
return hasBack[0];
}
@Test
public void testSystemUiVisibilityCallbackCausedByInsets() {
final TestActivity activity = startActivity(TestActivity.class);
final View controlTarget = activity.getWindow().getDecorView();
final int[] targetSysUiVis = new int[1];
final View nonControlTarget = new View(mTargetContext);
final int[] nonTargetSysUiVis = new int[1];
final WindowManager.LayoutParams nonTargetAttrs =
new WindowManager.LayoutParams(TYPE_APPLICATION);
nonTargetAttrs.flags = FLAG_NOT_FOCUSABLE;
getInstrumentation().runOnMainSync(() -> {
controlTarget.setOnSystemUiVisibilityChangeListener(
visibility -> targetSysUiVis[0] = visibility);
nonControlTarget.setOnSystemUiVisibilityChangeListener(
visibility -> nonTargetSysUiVis[0] = visibility);
activity.getWindowManager().addView(nonControlTarget, nonTargetAttrs);
});
waitForIdle();
testSysUiVisCallbackCausedByInsets(statusBars(), SYSTEM_UI_FLAG_FULLSCREEN,
controlTarget, targetSysUiVis, nonTargetSysUiVis);
testSysUiVisCallbackCausedByInsets(navigationBars(), SYSTEM_UI_FLAG_HIDE_NAVIGATION,
controlTarget, targetSysUiVis, nonTargetSysUiVis);
}
private void testSysUiVisCallbackCausedByInsets(int insetsType, int sysUiFlag, View target,
int[] targetSysUiVis, int[] nonTargetSysUiVis) {
if (target.getRootWindowInsets().isVisible(insetsType)) {
// Controlled by methods
getInstrumentation().runOnMainSync(
() -> target.getWindowInsetsController().hide(insetsType));
PollingCheck.waitFor(TIMEOUT, () ->
targetSysUiVis[0] == sysUiFlag && targetSysUiVis[0] == nonTargetSysUiVis[0]);
getInstrumentation().runOnMainSync(
() -> target.getWindowInsetsController().show(insetsType));
PollingCheck.waitFor(TIMEOUT, () ->
targetSysUiVis[0] == 0 && targetSysUiVis[0] == nonTargetSysUiVis[0]);
// Controlled by legacy flags
getInstrumentation().runOnMainSync(
() -> target.setSystemUiVisibility(sysUiFlag));
PollingCheck.waitFor(TIMEOUT, () ->
targetSysUiVis[0] == sysUiFlag && targetSysUiVis[0] == nonTargetSysUiVis[0]);
getInstrumentation().runOnMainSync(
() -> target.setSystemUiVisibility(0));
PollingCheck.waitFor(TIMEOUT, () ->
targetSysUiVis[0] == 0 && targetSysUiVis[0] == nonTargetSysUiVis[0]);
}
}
@Test
public void testSystemUiVisibilityCallbackCausedByAppearance() {
final TestActivity activity = startActivity(TestActivity.class);
final View controlTarget = activity.getWindow().getDecorView();
final int[] targetSysUiVis = new int[1];
getInstrumentation().runOnMainSync(() -> {
controlTarget.setOnSystemUiVisibilityChangeListener(
visibility -> targetSysUiVis[0] = visibility);
});
waitForIdle();
final int sysUiFlag = SYSTEM_UI_FLAG_LOW_PROFILE;
getInstrumentation().runOnMainSync(() -> controlTarget.setSystemUiVisibility(sysUiFlag));
PollingCheck.waitFor(TIMEOUT, () -> targetSysUiVis[0] == sysUiFlag);
getInstrumentation().runOnMainSync(() -> controlTarget.setSystemUiVisibility(0));
PollingCheck.waitFor(TIMEOUT, () -> targetSysUiVis[0] == 0);
}
@Test
public void testSetSystemUiVisibilityAfterCleared_showBarsBySwipe() throws Exception {
final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
// Assume we have the bars and they can be visible.
final int types = statusBars();
assumeTrue(rootView.getRootWindowInsets().isVisible(types));
final int targetFlags = SYSTEM_UI_FLAG_IMMERSIVE | SYSTEM_UI_FLAG_FULLSCREEN;
// Use flags to hide status bar.
ANIMATION_CALLBACK.reset();
getInstrumentation().runOnMainSync(() -> {
rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
rootView.setSystemUiVisibility(targetFlags);
});
ANIMATION_CALLBACK.waitForFinishing();
PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
// Wait for status bar invisible from InputDispatcher. Otherwise, the following
// dragFromTopToCenter might expand notification shade.
SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW);
// Swiping from top of display can show bars.
ANIMATION_CALLBACK.reset();
dragFromTopToCenter(rootView);
ANIMATION_CALLBACK.waitForFinishing();
PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types)
&& rootView.getSystemUiVisibility() != targetFlags);
// Use flags to hide status bar again.
ANIMATION_CALLBACK.reset();
getInstrumentation().runOnMainSync(() -> {
rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
rootView.setSystemUiVisibility(targetFlags);
});
ANIMATION_CALLBACK.waitForFinishing();
PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
// Wait for status bar invisible from InputDispatcher. Otherwise, the following
// dragFromTopToCenter might expand notification shade.
SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW);
// Swiping from top of display can show bars.
ANIMATION_CALLBACK.reset();
dragFromTopToCenter(rootView);
ANIMATION_CALLBACK.waitForFinishing();
PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
// The swipe action brings down the notification shade which causes subsequent tests to
// fail.
if (isAutomotive(mContext)) {
// Bring system to a known state before requesting to close system dialogs.
launchHomeActivity();
broadcastCloseSystemDialogs();
}
}
@Test
public void testSetSystemUiVisibilityAfterCleared_showBarsByApp() throws Exception {
final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
// Assume we have the bars and they can be visible.
final int types = statusBars();
assumeTrue(rootView.getRootWindowInsets().isVisible(types));
// Use the flag to hide status bar.
ANIMATION_CALLBACK.reset();
getInstrumentation().runOnMainSync(() -> {
rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
rootView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN);
});
ANIMATION_CALLBACK.waitForFinishing();
PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
// Clearing the flag can show status bar.
getInstrumentation().runOnMainSync(() -> {
rootView.setSystemUiVisibility(0);
});
PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
// Use the flag to hide status bar again.
ANIMATION_CALLBACK.reset();
getInstrumentation().runOnMainSync(() -> {
rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
rootView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN);
});
ANIMATION_CALLBACK.waitForFinishing();
PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
// Clearing the flag can show status bar.
getInstrumentation().runOnMainSync(() -> {
rootView.setSystemUiVisibility(0);
});
PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
}
@Test
public void testHideOnCreate() throws Exception {
final TestHideOnCreateActivity activity =
startActivityInWindowingModeFullScreen(TestHideOnCreateActivity.class);
final View rootView = activity.getWindow().getDecorView();
ANIMATION_CALLBACK.waitForFinishing();
PollingCheck.waitFor(TIMEOUT,
() -> !rootView.getRootWindowInsets().isVisible(statusBars())
&& !rootView.getRootWindowInsets().isVisible(navigationBars()));
}
@Test
public void testShowImeOnCreate() throws Exception {
final Instrumentation instrumentation = getInstrumentation();
assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
nullValue());
MockImeHelper.createManagedMockImeSession(this);
final TestShowOnCreateActivity activity = startActivity(TestShowOnCreateActivity.class);
final View rootView = activity.getWindow().getDecorView();
ANIMATION_CALLBACK.waitForFinishing();
PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(ime()));
}
@Test
public void testShowImeOnCreate_doesntCauseImeToReappearWhenDialogIsShown() throws Exception {
final Instrumentation instrumentation = getInstrumentation();
assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
nullValue());
try (MockImeSession imeSession = MockImeSession.create(instrumentation.getContext(),
instrumentation.getUiAutomation(), new ImeSettings.Builder())) {
final TestShowOnCreateActivity activity =
startActivityInWindowingModeFullScreen(TestShowOnCreateActivity.class);
final View rootView = activity.getWindow().getDecorView();
PollingCheck.waitFor(TIMEOUT,
() -> rootView.getRootWindowInsets().isVisible(ime()));
ANIMATION_CALLBACK.waitForFinishing();
ANIMATION_CALLBACK.reset();
getInstrumentation().runOnMainSync(() -> {
rootView.getWindowInsetsController().hide(ime());
});
PollingCheck.waitFor(TIMEOUT,
() -> !rootView.getRootWindowInsets().isVisible(ime()));
ANIMATION_CALLBACK.waitForFinishing();
getInstrumentation().runOnMainSync(() -> {
activity.showAltImDialog();
});
for (long time = TIMEOUT; time >= 0; time -= TIME_SLICE) {
assertFalse("IME visible when it shouldn't be",
rootView.getRootWindowInsets().isVisible(ime()));
SystemClock.sleep(TIME_SLICE);
}
}
}
@Test
public void testShowIme_immediatelyAfterDetachAndReattach() throws Exception {
final Instrumentation instrumentation = getInstrumentation();
assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
nullValue());
MockImeHelper.createManagedMockImeSession(this);
final TestActivity activity = startActivity(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
PollingCheck.waitFor(TIMEOUT, () -> getOnMainSync(rootView::hasWindowFocus));
View editor = getOnMainSync(rootView::findFocus);
ViewGroup parent = (ViewGroup) getOnMainSync(editor::getParent);
getInstrumentation().runOnMainSync(() -> {
parent.removeView(editor);
});
// Wait until checkFocus() is dispatched
getInstrumentation().waitForIdleSync();
getInstrumentation().runOnMainSync(() -> {
parent.addView(editor);
editor.requestFocus();
editor.getWindowInsetsController().show(ime());
});
PollingCheck.waitFor(TIMEOUT, () -> getOnMainSync(
() -> rootView.getRootWindowInsets().isVisible(ime())),
"Expected IME to become visible but didn't.");
}
@Test
public void testInsetsDispatch() throws Exception {
// Start an activity which hides system bars in fullscreen mode,
// otherwise, it might not be able to hide system bars in other windowing modes.
final TestHideOnCreateActivity activity = startActivityInWindowingModeFullScreen(
TestHideOnCreateActivity.class);
final View rootView = activity.getWindow().getDecorView();
ANIMATION_CALLBACK.waitForFinishing();
PollingCheck.waitFor(TIMEOUT,
() -> !rootView.getRootWindowInsets().isVisible(statusBars())
&& !rootView.getRootWindowInsets().isVisible(navigationBars()));
// Add a dialog which hides system bars before the dialog is added to the system while the
// system bar was hidden previously, and collect the window insets that the dialog receives.
final ArrayList<WindowInsets> windowInsetsList = new ArrayList<>();
getInstrumentation().runOnMainSync(() -> {
final AlertDialog dialog = new AlertDialog.Builder(activity).create();
final Window dialogWindow = dialog.getWindow();
dialogWindow.getDecorView().setOnApplyWindowInsetsListener((view, insets) -> {
windowInsetsList.add(insets);
return view.onApplyWindowInsets(insets);
});
dialogWindow.getInsetsController().hide(statusBars() | navigationBars());
dialog.show();
});
getInstrumentation().waitForIdleSync();
// The dialog must never receive any of visible insets of system bars.
for (WindowInsets windowInsets : windowInsetsList) {
assertFalse(windowInsets.isVisible(statusBars()));
assertFalse(windowInsets.isVisible(navigationBars()));
}
}
@Test
public void testWindowInsetsController_availableAfterAddView() throws Exception {
final TestHideOnCreateActivity activity =
startActivityInWindowingModeFullScreen(TestHideOnCreateActivity.class);
final View rootView = activity.getWindow().getDecorView();
ANIMATION_CALLBACK.waitForFinishing();
PollingCheck.waitFor(TIMEOUT,
() -> !rootView.getRootWindowInsets().isVisible(statusBars())
&& !rootView.getRootWindowInsets().isVisible(navigationBars()));
final View childWindow = new View(activity);
getInstrumentation().runOnMainSync(() -> {
activity.getWindowManager().addView(childWindow,
new WindowManager.LayoutParams(TYPE_APPLICATION));
mErrorCollector.checkThat(childWindow.getWindowInsetsController(), is(notNullValue()));
});
getInstrumentation().waitForIdleSync();
getInstrumentation().runOnMainSync(() -> {
activity.getWindowManager().removeView(childWindow);
});
}
@Test
public void testDispatchApplyWindowInsetsCount_systemBars() throws InterruptedException {
final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
getInstrumentation().waitForIdleSync();
// Assume we have at least one visible system bar.
assumeTrue(rootView.getRootWindowInsets().isVisible(statusBars())
|| rootView.getRootWindowInsets().isVisible(navigationBars()));
getInstrumentation().runOnMainSync(() -> {
// This makes the window frame stable while changing the system bar visibility.
final WindowManager.LayoutParams attrs = activity.getWindow().getAttributes();
attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
activity.getWindow().setAttributes(attrs);
});
getInstrumentation().waitForIdleSync();
final int[] dispatchApplyWindowInsetsCount = {0};
rootView.setOnApplyWindowInsetsListener((v, insets) -> {
dispatchApplyWindowInsetsCount[0]++;
return v.onApplyWindowInsets(insets);
});
// One hide-system-bar call...
ANIMATION_CALLBACK.reset();
getInstrumentation().runOnMainSync(() -> {
rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
rootView.getWindowInsetsController().hide(systemBars());
});
ANIMATION_CALLBACK.waitForFinishing();
// ... should only trigger one dispatchApplyWindowInsets
assertEquals(1, dispatchApplyWindowInsetsCount[0]);
// One show-system-bar call...
dispatchApplyWindowInsetsCount[0] = 0;
ANIMATION_CALLBACK.reset();
getInstrumentation().runOnMainSync(() -> {
rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
rootView.getWindowInsetsController().show(systemBars());
});
ANIMATION_CALLBACK.waitForFinishing();
// ... should only trigger one dispatchApplyWindowInsets
assertEquals(1, dispatchApplyWindowInsetsCount[0]);
}
@Test
public void testDispatchApplyWindowInsetsCount_ime() throws Exception {
assumeFalse("Automotive is to skip this test until showing and hiding certain insets "
+ "simultaneously in a single request is supported", isAutomotive(mContext));
assumeThat(MockImeSession.getUnavailabilityReason(getInstrumentation().getContext()),
nullValue());
MockImeHelper.createManagedMockImeSession(this);
final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
final View rootView = activity.getWindow().getDecorView();
getInstrumentation().waitForIdleSync();
final int[] dispatchApplyWindowInsetsCount = {0};
rootView.setOnApplyWindowInsetsListener((v, insets) -> {
dispatchApplyWindowInsetsCount[0]++;
return v.onApplyWindowInsets(insets);
});
// One show-ime call...
ANIMATION_CALLBACK.reset();
getInstrumentation().runOnMainSync(() -> {
rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
rootView.getWindowInsetsController().show(ime());
});
ANIMATION_CALLBACK.waitForFinishing();
// ... should only trigger one dispatchApplyWindowInsets
assertEquals(1, dispatchApplyWindowInsetsCount[0]);
// One hide-ime call...
dispatchApplyWindowInsetsCount[0] = 0;
ANIMATION_CALLBACK.reset();
getInstrumentation().runOnMainSync(() -> {
rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
rootView.getWindowInsetsController().hide(ime());
});
ANIMATION_CALLBACK.waitForFinishing();
// ... should only trigger one dispatchApplyWindowInsets
assertEquals(1, dispatchApplyWindowInsetsCount[0]);
}
private static void broadcastCloseSystemDialogs() {
executeShellCommand(AM_BROADCAST_CLOSE_SYSTEM_DIALOGS);
}
private static boolean isAutomotive(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
}
private static void hideInsets(View view, int types) throws InterruptedException {
ANIMATION_CALLBACK.reset();
getInstrumentation().runOnMainSync(() -> {
view.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
view.getWindowInsetsController().hide(types);
});
ANIMATION_CALLBACK.waitForFinishing();
PollingCheck.waitFor(TIMEOUT, () -> !view.getRootWindowInsets().isVisible(types));
}
private void tapOnDisplay(float x, float y) {
dragOnDisplay(x, y, x, y);
}
private void dragFromTopToCenter(View view) {
dragOnDisplay(view.getWidth() / 2f, 0 /* downY */,
view.getWidth() / 2f, view.getHeight() / 2f);
}
private void dragFromLeftToCenter(View view) {
dragOnDisplay(0 /* downX */, view.getHeight() / 2f,
view.getWidth() / 2f, view.getHeight() / 2f);
}
private void dragOnDisplay(float downX, float downY, float upX, float upY) {
final long downTime = SystemClock.elapsedRealtime();
// down event
MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,
downX, downY, 0 /* metaState */);
sendPointerSync(event);
event.recycle();
// move event
event = MotionEvent.obtain(downTime, downTime + 1, MotionEvent.ACTION_MOVE,
(downX + upX) / 2f, (downY + upY) / 2f, 0 /* metaState */);
sendPointerSync(event);
event.recycle();
// up event
event = MotionEvent.obtain(downTime, downTime + 2, MotionEvent.ACTION_UP,
upX, upY, 0 /* metaState */);
sendPointerSync(event);
event.recycle();
}
private void sendPointerSync(MotionEvent event) {
event.setSource(event.getSource() | InputDevice.SOURCE_CLASS_POINTER);
// Use UiAutomation to inject into TestActivity because it is started and owned by the
// Shell, which has a different uid than this instrumentation.
getInstrumentation().getUiAutomation().injectInputEvent(event, true);
}
private static class AnimationCallback extends WindowInsetsAnimation.Callback {
private static final long ANIMATION_TIMEOUT = 5000; // milliseconds
private boolean mFinished = false;
AnimationCallback() {
super(DISPATCH_MODE_CONTINUE_ON_SUBTREE);
}
@Override
public WindowInsets onProgress(WindowInsets insets,
List<WindowInsetsAnimation> runningAnimations) {
return insets;
}
@Override
public void onEnd(WindowInsetsAnimation animation) {
synchronized (this) {
mFinished = true;
notify();
}
}
void waitForFinishing() throws InterruptedException {
synchronized (this) {
if (!mFinished) {
wait(ANIMATION_TIMEOUT);
}
}
}
void reset() {
synchronized (this) {
mFinished = false;
}
}
}
private static View setViews(Activity activity, @Nullable String privateImeOptions) {
LinearLayout layout = new LinearLayout(activity);
View text = new TextView(activity);
EditText editor = new EditText(activity);
editor.setPrivateImeOptions(privateImeOptions);
layout.addView(text);
layout.addView(editor);
activity.setContentView(layout);
editor.requestFocus();
return layout;
}
public static class TestActivity extends FocusableActivity {
final String mEditTextMarker =
getClass().getName() + "/" + SystemClock.elapsedRealtimeNanos();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setViews(this, mEditTextMarker);
getWindow().setSoftInputMode(SOFT_INPUT_STATE_HIDDEN);
}
}
public static class TestHideOnCreateActivity extends FocusableActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View layout = setViews(this, null /* privateImeOptions */);
ANIMATION_CALLBACK.reset();
getWindow().getDecorView().setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
getWindow().getInsetsController().hide(statusBars());
layout.getWindowInsetsController().hide(navigationBars());
}
}
public static class TestShowOnCreateActivity extends FocusableActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setViews(this, null /* privateImeOptions */);
ANIMATION_CALLBACK.reset();
getWindow().getDecorView().setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
getWindow().getInsetsController().show(ime());
}
void showAltImDialog() {
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("TestDialog")
.create();
dialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
dialog.show();
}
}
private <R> R getOnMainSync(Supplier<R> f) {
final Object[] result = new Object[1];
getInstrumentation().runOnMainSync(() -> result[0] = f.get());
//noinspection unchecked
return (R) result[0];
}
}