| /* |
| * 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.server.wm; |
| |
| import static android.content.res.Configuration.ORIENTATION_PORTRAIT; |
| import static android.provider.Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS; |
| import static android.server.wm.app.Components.LAUNCHING_ACTIVITY; |
| import static android.view.Surface.ROTATION_0; |
| import static android.view.Surface.ROTATION_180; |
| import static android.view.Surface.ROTATION_90; |
| import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; |
| |
| import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; |
| |
| import static org.hamcrest.Matchers.notNullValue; |
| 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.assumeTrue; |
| |
| import android.app.Activity; |
| import android.content.ComponentName; |
| import android.content.pm.PackageManager; |
| import android.graphics.Insets; |
| import android.os.Bundle; |
| import android.platform.test.annotations.Presubmit; |
| import android.provider.Settings; |
| import android.server.wm.settings.SettingsSession; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.Window; |
| import android.view.WindowInsets; |
| import android.view.WindowManager.LayoutParams; |
| |
| import androidx.test.rule.ActivityTestRule; |
| |
| import com.android.compatibility.common.util.WindowUtil; |
| |
| import org.hamcrest.CustomTypeSafeMatcher; |
| import org.hamcrest.Matcher; |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.ErrorCollector; |
| |
| import java.util.function.Supplier; |
| |
| @Presubmit |
| public class WindowInsetsPolicyTest extends ActivityManagerTestBase { |
| private static final String TAG = WindowInsetsPolicyTest.class.getSimpleName(); |
| |
| private ComponentName mTestActivityComponentName; |
| |
| private SettingsSession<String> mImmersiveModeConfirmationSetting; |
| |
| @Rule |
| public final ErrorCollector mErrorCollector = new ErrorCollector(); |
| |
| @Rule |
| public final ActivityTestRule<TestActivity> mTestActivity = |
| new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */, |
| false /* launchActivity */); |
| |
| @Rule |
| public final ActivityTestRule<FullscreenTestActivity> mFullscreenTestActivity = |
| new ActivityTestRule<>(FullscreenTestActivity.class, false /* initialTouchMode */, |
| false /* launchActivity */); |
| |
| @Rule |
| public final ActivityTestRule<FullscreenWmFlagsTestActivity> mFullscreenWmFlagsTestActivity = |
| new ActivityTestRule<>(FullscreenWmFlagsTestActivity.class, |
| false /* initialTouchMode */, false /* launchActivity */); |
| |
| @Rule |
| public final ActivityTestRule<ImmersiveFullscreenTestActivity> mImmersiveTestActivity = |
| new ActivityTestRule<>(ImmersiveFullscreenTestActivity.class, |
| false /* initialTouchMode */, false /* launchActivity */); |
| |
| @Before |
| @Override |
| public void setUp() throws Exception { |
| super.setUp(); |
| mTestActivityComponentName = new ComponentName(mContext, TestActivity.class); |
| mImmersiveModeConfirmationSetting = new SettingsSession<>( |
| Settings.Secure.getUriFor(IMMERSIVE_MODE_CONFIRMATIONS), |
| Settings.Secure::getString, Settings.Secure::putString); |
| mImmersiveModeConfirmationSetting.set("confirmed"); |
| |
| } |
| |
| @After |
| public void tearDown() { |
| if (mImmersiveModeConfirmationSetting != null) { |
| mImmersiveModeConfirmationSetting.close(); |
| } |
| } |
| |
| @Test |
| public void testWindowInsets_dispatched() { |
| final TestActivity activity = launchAndWait(mTestActivity); |
| |
| WindowInsets insets = getOnMainSync(activity::getDispatchedInsets); |
| Assert.assertThat("test setup failed, no insets dispatched", insets, notNullValue()); |
| |
| commonAsserts(insets); |
| } |
| |
| @Test |
| public void testWindowInsets_root() { |
| final TestActivity activity = launchAndWait(mTestActivity); |
| |
| WindowInsets insets = getOnMainSync(activity::getRootInsets); |
| Assert.assertThat("test setup failed, no insets at root", insets, notNullValue()); |
| |
| commonAsserts(insets); |
| } |
| |
| /** |
| * Tests whether an activity in split screen gets the top insets force consumed if |
| * {@link View#SYSTEM_UI_FLAG_FULLSCREEN} is set, and doesn't otherwise. |
| */ |
| @Test |
| public void testForcedConsumedTopInsets() throws Exception { |
| assumeTrue("Skipping test: no split multi-window support", |
| supportsSplitScreenMultiWindow()); |
| |
| final TestActivity activity = launchAndWait(mTestActivity); |
| final int rotation = activity.getDisplay().getRotation(); |
| final boolean isPortrait = activity.getResources().getConfiguration() |
| .orientation == ORIENTATION_PORTRAIT; |
| final RotationSession rotationSession = createManagedRotationSession(); |
| if (isPortrait) { |
| // Rotate to landscape. |
| rotationSession.set(rotation == ROTATION_0 || rotation == ROTATION_180 |
| ? ROTATION_90 : ROTATION_0); |
| } else { |
| // Keep in landscape. |
| rotationSession.set(rotation); |
| } |
| |
| mWmState.waitForValidState(mTestActivityComponentName); |
| final int taskId = mWmState.getTaskByActivity(mTestActivityComponentName).mTaskId; |
| launchActivityInPrimarySplit(LAUNCHING_ACTIVITY); |
| mTaskOrganizer.putTaskInSplitSecondary(taskId); |
| mWmState.waitForValidState(mTestActivityComponentName); |
| |
| // Ensure that top insets are not consumed for LAYOUT_FULLSCREEN |
| WindowInsets insets = getOnMainSync(activity::getDispatchedInsets); |
| final WindowInsets rootInsets = getOnMainSync(activity::getRootInsets); |
| assertEquals("top inset must be dispatched in split screen", |
| rootInsets.getSystemWindowInsetTop(), insets.getSystemWindowInsetTop()); |
| |
| // Ensure that top insets are fully consumed for FULLSCREEN |
| final TestActivity fullscreenActivity = launchAndWait(mFullscreenTestActivity); |
| insets = getOnMainSync(fullscreenActivity::getDispatchedInsets); |
| assertEquals("top insets must be consumed if FULLSCREEN is set", |
| 0, insets.getSystemWindowInsetTop()); |
| |
| // Ensure that top insets are fully consumed for FULLSCREEN when setting it over wm |
| // layout params |
| final TestActivity fullscreenWmFlagsActivity = |
| launchAndWait(mFullscreenWmFlagsTestActivity); |
| insets = getOnMainSync(fullscreenWmFlagsActivity::getDispatchedInsets); |
| assertEquals("top insets must be consumed if FULLSCREEN is set", |
| 0, insets.getSystemWindowInsetTop()); |
| } |
| |
| @Test |
| public void testNonAutomotiveFullScreenNotBlockedBySystemComponents() { |
| assumeFalse("Skipping test: Automotive is allowed to partially block fullscreen " |
| + "applications with system bars.", isAutomotive()); |
| |
| final TestActivity fullscreenActivity = launchAndWait(mFullscreenTestActivity); |
| View decorView = fullscreenActivity.getDecorView(); |
| View contentView = decorView.findViewById(android.R.id.content); |
| boolean hasFullWidth = decorView.getMeasuredWidth() == contentView.getMeasuredWidth(); |
| boolean hasFullHeight = decorView.getMeasuredHeight() == contentView.getMeasuredHeight(); |
| |
| assertTrue(hasFullWidth && hasFullHeight); |
| } |
| |
| @Test |
| public void testImmersiveFullscreenHidesSystemBars() throws Throwable { |
| // Run the test twice, because the issue that shows system bars even in the immersive mode, |
| // happens at the 2nd try. |
| for (int i = 1; i <= 2; ++i) { |
| Log.d(TAG, "testImmersiveFullscreenHidesSystemBars: try" + i); |
| |
| TestActivity immersiveActivity = launchAndWait(mImmersiveTestActivity); |
| WindowInsets insets = getOnMainSync(immersiveActivity::getDispatchedInsets); |
| |
| assertFalse(insets.isVisible(WindowInsets.Type.statusBars())); |
| assertFalse(insets.isVisible(WindowInsets.Type.navigationBars())); |
| |
| WindowInsets rootInsets = getOnMainSync(immersiveActivity::getRootInsets); |
| assertFalse(rootInsets.isVisible(WindowInsets.Type.statusBars())); |
| assertFalse(rootInsets.isVisible(WindowInsets.Type.navigationBars())); |
| |
| View statusBarBgView = getOnMainSync(immersiveActivity::getStatusBarBackgroundView); |
| // The status bar background view can be non-existent or invisible. |
| assertTrue(statusBarBgView == null |
| || statusBarBgView.getVisibility() == android.view.View.INVISIBLE); |
| |
| View navigationBarBgView = getOnMainSync( |
| immersiveActivity::getNavigationBarBackgroundView); |
| // The navigation bar background view can be non-existent or invisible. |
| assertTrue(navigationBarBgView == null |
| || navigationBarBgView.getVisibility() == android.view.View.INVISIBLE); |
| } |
| } |
| |
| private void commonAsserts(WindowInsets insets) { |
| assertForAllInsets("must be non-negative", insets, insetsGreaterThanOrEqualTo(Insets.NONE)); |
| |
| assertThat("system gesture insets must include mandatory system gesture insets", |
| insets.getMandatorySystemGestureInsets(), |
| insetsLessThanOrEqualTo(insets.getSystemGestureInsets())); |
| |
| Insets stableAndSystem = Insets.min(insets.getSystemWindowInsets(), |
| insets.getStableInsets()); |
| assertThat("mandatory system gesture insets must include intersection between " |
| + "stable and system window insets", |
| stableAndSystem, |
| insetsLessThanOrEqualTo(insets.getMandatorySystemGestureInsets())); |
| |
| assertThat("tappable insets must be at most system window insets", |
| insets.getTappableElementInsets(), |
| insetsLessThanOrEqualTo(insets.getSystemWindowInsets())); |
| } |
| |
| private void assertForAllInsets(String reason, WindowInsets actual, |
| Matcher<? super Insets> matcher) { |
| assertThat("getSystemWindowInsets" + ": " + reason, |
| actual.getSystemWindowInsets(), matcher); |
| assertThat("getStableInsets" + ": " + reason, |
| actual.getStableInsets(), matcher); |
| assertThat("getSystemGestureInsets" + ": " + reason, |
| actual.getSystemGestureInsets(), matcher); |
| assertThat("getMandatorySystemGestureInsets" + ": " + reason, |
| actual.getMandatorySystemGestureInsets(), matcher); |
| assertThat("getTappableElementInsets" + ": " + reason, |
| actual.getTappableElementInsets(), matcher); |
| } |
| |
| private <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) { |
| mErrorCollector.checkThat(reason, actual, matcher); |
| } |
| |
| private <R> R getOnMainSync(Supplier<R> f) { |
| final Object[] result = new Object[1]; |
| runOnMainSync(() -> result[0] = f.get()); |
| //noinspection unchecked |
| return (R) result[0]; |
| } |
| |
| private void runOnMainSync(Runnable runnable) { |
| getInstrumentation().runOnMainSync(runnable); |
| } |
| |
| private <T extends Activity> T launchAndWait(ActivityTestRule<T> rule) { |
| final T activity = rule.launchActivity(null); |
| WindowUtil.waitForFocus(activity); |
| return activity; |
| } |
| |
| private boolean isAutomotive() { |
| return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); |
| } |
| |
| private static Matcher<Insets> insetsLessThanOrEqualTo(Insets max) { |
| return new CustomTypeSafeMatcher<Insets>("must be smaller on each side than " + max) { |
| @Override |
| protected boolean matchesSafely(Insets actual) { |
| return actual.left <= max.left && actual.top <= max.top |
| && actual.right <= max.right && actual.bottom <= max.bottom; |
| } |
| }; |
| } |
| |
| private static Matcher<Insets> insetsGreaterThanOrEqualTo(Insets min) { |
| return new CustomTypeSafeMatcher<Insets>("must be greater on each side than " + min) { |
| @Override |
| protected boolean matchesSafely(Insets actual) { |
| return actual.left >= min.left && actual.top >= min.top |
| && actual.right >= min.right && actual.bottom >= min.bottom; |
| } |
| }; |
| } |
| |
| public static class TestActivity extends Activity { |
| |
| private WindowInsets mDispatchedInsets; |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| getWindow().requestFeature(Window.FEATURE_NO_TITLE); |
| View view = new View(this); |
| view.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); |
| getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
| | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); |
| view.setOnApplyWindowInsetsListener((v, insets) -> mDispatchedInsets = insets); |
| setContentView(view); |
| } |
| |
| View getDecorView() { |
| return getWindow().getDecorView(); |
| } |
| |
| View getStatusBarBackgroundView() { |
| return getWindow().getStatusBarBackgroundView(); |
| } |
| |
| View getNavigationBarBackgroundView() { |
| return getWindow().getNavigationBarBackgroundView(); |
| } |
| |
| WindowInsets getRootInsets() { |
| return getWindow().getDecorView().getRootWindowInsets(); |
| } |
| |
| WindowInsets getDispatchedInsets() { |
| return mDispatchedInsets; |
| } |
| } |
| |
| public static class FullscreenTestActivity extends TestActivity { |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| getDecorView().setSystemUiVisibility( |
| getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_FULLSCREEN); |
| } |
| } |
| |
| public static class FullscreenWmFlagsTestActivity extends TestActivity { |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN); |
| } |
| } |
| |
| public static class ImmersiveFullscreenTestActivity extends TestActivity { |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| // See https://developer.android.com/training/system-ui/immersive#EnableFullscreen |
| getDecorView().setSystemUiVisibility( |
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
| // Set the content to appear under the system bars so that the |
| // content doesn't resize when the system bars hide and show. |
| | View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
| | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
| | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
| // Hide the nav bar and status bar |
| | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
| | View.SYSTEM_UI_FLAG_FULLSCREEN); |
| } |
| } |
| |
| public static class NaturalOrientationTestActivity extends TestActivity { |
| } |
| } |