| /* |
| * 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.view.ViewGroup.LayoutParams.MATCH_PARENT; |
| 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.WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE; |
| import static android.view.WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_TOUCH; |
| import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; |
| |
| import static androidx.test.InstrumentationRegistry.getInstrumentation; |
| |
| import static org.hamcrest.Matchers.is; |
| import static org.hamcrest.Matchers.notNullValue; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assume.assumeTrue; |
| |
| import android.app.Activity; |
| import android.app.AlertDialog; |
| import android.os.Bundle; |
| import android.os.SystemClock; |
| import android.platform.test.annotations.Presubmit; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.Window; |
| import android.view.WindowInsets; |
| import android.view.WindowInsets.Type; |
| import android.view.WindowInsetsAnimation; |
| import android.widget.EditText; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| |
| import androidx.test.filters.FlakyTest; |
| |
| import com.android.compatibility.common.util.PollingCheck; |
| import com.android.compatibility.common.util.SystemUtil; |
| |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.ErrorCollector; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Test whether WindowInsetsController controls window insets as expected. |
| * |
| * Build/Install/Run: |
| * atest CtsWindowManagerDeviceTestCases:WindowInsetsControllerTests |
| */ |
| @FlakyTest(detail = "Promote once confirmed non-flaky") |
| @Presubmit |
| public class WindowInsetsControllerTests extends WindowManagerTestBase { |
| |
| private final static long TIMEOUT = 1000; // milliseconds |
| private final static AnimationCallback ANIMATION_CALLBACK = new AnimationCallback(); |
| |
| @Rule |
| public final ErrorCollector mErrorCollector = new ErrorCollector(); |
| |
| @Test |
| public void testHide() { |
| final TestActivity activity = startActivity(TestActivity.class); |
| final View rootView = activity.getWindow().getDecorView(); |
| |
| testHideInternal(rootView, Type.statusBars()); |
| testHideInternal(rootView, Type.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 = startActivity(TestActivity.class); |
| final View rootView = activity.getWindow().getDecorView(); |
| |
| testShowInternal(rootView, Type.statusBars()); |
| testShowInternal(rootView, Type.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)); |
| } |
| } |
| |
| @Test |
| public void testImeShowAndHide() { |
| final TestActivity activity = startActivity(TestActivity.class); |
| 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 testSetSystemBarsBehavior_showBarsByTouch() throws InterruptedException { |
| final TestActivity activity = startActivity(TestActivity.class); |
| final View rootView = activity.getWindow().getDecorView(); |
| |
| // The show-by-touch behavior will only be applied while navigation bars get hidden. |
| final int types = Type.navigationBars(); |
| assumeTrue(rootView.getRootWindowInsets().isVisible(types)); |
| |
| rootView.getWindowInsetsController().setSystemBarsBehavior(BEHAVIOR_SHOW_BARS_BY_TOUCH); |
| |
| hideInsets(rootView, types); |
| |
| // Touching on display can show bars. |
| tapOnDisplay(rootView.getWidth() / 2f, rootView.getHeight() / 2f); |
| PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types)); |
| } |
| |
| @Test |
| public void testSetSystemBarsBehavior_showBarsBySwipe() 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 = Type.statusBars(); |
| assumeTrue(rootView.getRootWindowInsets().isVisible(types)); |
| |
| rootView.getWindowInsetsController().setSystemBarsBehavior(BEHAVIOR_SHOW_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)); |
| |
| // Swiping from top of display can show bars. |
| dragOnDisplay(rootView.getWidth() / 2f, 0 /* downY */, |
| rootView.getWidth() / 2f, rootView.getHeight() / 2f); |
| 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 = Type.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)); |
| |
| // Swiping from top of display can show transient bars, but apps cannot detect that. |
| dragOnDisplay(rootView.getWidth() / 2f, 0 /* downY */, |
| rootView.getWidth() / 2f, rootView.getHeight() /2f); |
| PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types)); |
| } |
| |
| @Test |
| public void testHideOnCreate() throws Exception { |
| final TestHideOnCreateActivity activity = startActivity(TestHideOnCreateActivity.class); |
| final View rootView = activity.getWindow().getDecorView(); |
| ANIMATION_CALLBACK.waitForFinishing(TIMEOUT); |
| PollingCheck.waitFor(TIMEOUT, |
| () -> !rootView.getRootWindowInsets().isVisible(statusBars()) |
| && !rootView.getRootWindowInsets().isVisible(navigationBars())); |
| } |
| |
| @Test |
| public void testShowImeOnCreate() throws Exception { |
| final TestShowOnCreateActivity activity = startActivity(TestShowOnCreateActivity.class); |
| final View rootView = activity.getWindow().getDecorView(); |
| ANIMATION_CALLBACK.waitForFinishing(TIMEOUT); |
| PollingCheck.waitFor(TIMEOUT, |
| () -> rootView.getRootWindowInsets().isVisible(ime())); |
| } |
| |
| @Test |
| public void testInsetsDispatch() throws Exception { |
| // Start an activity which hides system bars. |
| final TestHideOnCreateActivity activity = startActivity(TestHideOnCreateActivity.class); |
| final View rootView = activity.getWindow().getDecorView(); |
| ANIMATION_CALLBACK.waitForFinishing(TIMEOUT); |
| 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 = startActivity(TestHideOnCreateActivity.class); |
| final View rootView = activity.getWindow().getDecorView(); |
| ANIMATION_CALLBACK.waitForFinishing(TIMEOUT); |
| PollingCheck.waitFor(TIMEOUT, |
| () -> !rootView.getRootWindowInsets().isVisible(statusBars()) |
| && !rootView.getRootWindowInsets().isVisible(navigationBars())); |
| |
| final View childWindow = new View(activity); |
| getInstrumentation().runOnMainSync(() -> { |
| activity.getWindowManager().addView(childWindow, |
| new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); |
| mErrorCollector.checkThat(childWindow.getWindowInsetsController(), is(notNullValue())); |
| }); |
| getInstrumentation().waitForIdleSync(); |
| getInstrumentation().runOnMainSync(() -> { |
| activity.getWindowManager().removeView(childWindow); |
| }); |
| |
| } |
| |
| 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(TIMEOUT); |
| PollingCheck.waitFor(TIMEOUT, () -> !view.getRootWindowInsets().isVisible(types)); |
| } |
| |
| private void tapOnDisplay(float x, float y) { |
| dragOnDisplay(x, y, x, y); |
| } |
| |
| 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) { |
| SystemUtil.runWithShellPermissionIdentity( |
| () -> getInstrumentation().sendPointerSync(event)); |
| } |
| |
| private static class AnimationCallback extends WindowInsetsAnimation.Callback { |
| |
| 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(long timeout) throws InterruptedException { |
| synchronized (this) { |
| if (!mFinished) { |
| wait(timeout); |
| } |
| } |
| } |
| |
| void reset() { |
| synchronized (this) { |
| mFinished = false; |
| } |
| } |
| } |
| |
| private static View setViews(Activity activity) { |
| LinearLayout layout = new LinearLayout(activity); |
| View text = new TextView(activity); |
| EditText editor = new EditText(activity); |
| layout.addView(text); |
| layout.addView(editor); |
| activity.setContentView(layout); |
| editor.requestFocus(); |
| return layout; |
| } |
| |
| public static class TestActivity extends FocusableActivity { |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setViews(this); |
| } |
| } |
| |
| public static class TestHideOnCreateActivity extends FocusableActivity { |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| View layout = setViews(this); |
| 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); |
| View layout = setViews(this); |
| ANIMATION_CALLBACK.reset(); |
| getWindow().getDecorView().setWindowInsetsAnimationCallback(ANIMATION_CALLBACK); |
| getWindow().getInsetsController().show(ime()); |
| } |
| } |
| } |