1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package android.server.wm;
18 
19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
20 import static android.server.wm.app.Components.LAUNCHING_ACTIVITY;
21 import static android.view.Display.DEFAULT_DISPLAY;
22 import static android.view.Surface.ROTATION_0;
23 import static android.view.Surface.ROTATION_90;
24 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
25 
26 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
27 
28 import static org.hamcrest.Matchers.notNullValue;
29 import static org.junit.Assert.assertEquals;
30 import static org.junit.Assert.assertFalse;
31 import static org.junit.Assert.assertTrue;
32 import static org.junit.Assume.assumeFalse;
33 import static org.junit.Assume.assumeTrue;
34 
35 import android.app.Activity;
36 import android.content.ComponentName;
37 import android.content.pm.PackageManager;
38 import android.graphics.Insets;
39 import android.os.Bundle;
40 import android.platform.test.annotations.Presubmit;
41 import android.util.Log;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.view.Window;
45 import android.view.WindowInsets;
46 import android.view.WindowManager.LayoutParams;
47 
48 import androidx.test.rule.ActivityTestRule;
49 
50 import com.android.compatibility.common.util.PollingCheck;
51 
52 import org.hamcrest.CustomTypeSafeMatcher;
53 import org.hamcrest.Matcher;
54 import org.junit.Assert;
55 import org.junit.Before;
56 import org.junit.Rule;
57 import org.junit.Test;
58 import org.junit.rules.ErrorCollector;
59 
60 import java.util.function.Supplier;
61 
62 @Presubmit
63 public class WindowInsetsPolicyTest extends ActivityManagerTestBase {
64     private static final String TAG = WindowInsetsPolicyTest.class.getSimpleName();
65 
66     private ComponentName mTestActivityComponentName;
67 
68     @Rule
69     public final ErrorCollector mErrorCollector = new ErrorCollector();
70 
71     @Rule
72     public final ActivityTestRule<TestActivity> mTestActivity =
73             new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */,
74                     false /* launchActivity */);
75 
76     @Rule
77     public final ActivityTestRule<FullscreenTestActivity> mFullscreenTestActivity =
78             new ActivityTestRule<>(FullscreenTestActivity.class, false /* initialTouchMode */,
79                     false /* launchActivity */);
80 
81     @Rule
82     public final ActivityTestRule<FullscreenWmFlagsTestActivity> mFullscreenWmFlagsTestActivity =
83             new ActivityTestRule<>(FullscreenWmFlagsTestActivity.class,
84                     false /* initialTouchMode */, false /* launchActivity */);
85 
86     @Rule
87     public final ActivityTestRule<ImmersiveFullscreenTestActivity> mImmersiveTestActivity =
88             new ActivityTestRule<>(ImmersiveFullscreenTestActivity.class,
89                     false /* initialTouchMode */, false /* launchActivity */);
90 
91     @Rule
92     public final ActivityTestRule<NaturalOrientationTestActivity> mNaturalOrientationTestActivity =
93             new ActivityTestRule<>(NaturalOrientationTestActivity.class,
94                     false /* initialTouchMode */, false /* launchActivity */);
95 
96     @Before
97     @Override
setUp()98     public void setUp() throws Exception {
99         super.setUp();
100         mTestActivityComponentName = new ComponentName(mContext, TestActivity.class);
101     }
102 
103     @Test
testWindowInsets_dispatched()104     public void testWindowInsets_dispatched() {
105         final TestActivity activity = launchAndWait(mTestActivity);
106 
107         WindowInsets insets = getOnMainSync(activity::getDispatchedInsets);
108         Assert.assertThat("test setup failed, no insets dispatched", insets, notNullValue());
109 
110         commonAsserts(insets);
111     }
112 
113     @Test
testWindowInsets_root()114     public void testWindowInsets_root() {
115         final TestActivity activity = launchAndWait(mTestActivity);
116 
117         WindowInsets insets = getOnMainSync(activity::getRootInsets);
118         Assert.assertThat("test setup failed, no insets at root", insets, notNullValue());
119 
120         commonAsserts(insets);
121     }
122 
123     /**
124      * Tests whether an activity in split screen gets the top insets force consumed if
125      * {@link View#SYSTEM_UI_FLAG_FULLSCREEN} is set, and doesn't otherwise.
126      */
127     @Test
testForcedConsumedTopInsets()128     public void testForcedConsumedTopInsets() throws Exception {
129         assumeTrue("Skipping test: no split multi-window support",
130                 supportsSplitScreenMultiWindow());
131 
132         launchAndWait(mNaturalOrientationTestActivity);
133         mWmState.computeState(new ComponentName[] {});
134         final boolean naturalOrientationPortrait =
135                 mWmState.getDisplay(DEFAULT_DISPLAY)
136                         .mFullConfiguration.orientation == ORIENTATION_PORTRAIT;
137 
138         final RotationSession rotationSession = createManagedRotationSession();
139         rotationSession.set(naturalOrientationPortrait ? ROTATION_90 : ROTATION_0);
140 
141         final TestActivity activity = launchAndWait(mTestActivity);
142         mWmState.waitForValidState(mTestActivityComponentName);
143         final int taskId = mWmState.getTaskByActivity(mTestActivityComponentName).mTaskId;
144         launchActivityInPrimarySplit(LAUNCHING_ACTIVITY);
145         mTaskOrganizer.putTaskInSplitSecondary(taskId);
146         mWmState.waitForValidState(mTestActivityComponentName);
147 
148         // Ensure that top insets are not consumed for LAYOUT_FULLSCREEN
149         WindowInsets insets = getOnMainSync(activity::getDispatchedInsets);
150         final WindowInsets rootInsets = getOnMainSync(activity::getRootInsets);
151         assertEquals("top inset must be dispatched in split screen",
152                 rootInsets.getSystemWindowInsetTop(), insets.getSystemWindowInsetTop());
153 
154         // Ensure that top insets are fully consumed for FULLSCREEN
155         final TestActivity fullscreenActivity = launchAndWait(mFullscreenTestActivity);
156         insets = getOnMainSync(fullscreenActivity::getDispatchedInsets);
157         assertEquals("top insets must be consumed if FULLSCREEN is set",
158                 0, insets.getSystemWindowInsetTop());
159 
160         // Ensure that top insets are fully consumed for FULLSCREEN when setting it over wm
161         // layout params
162         final TestActivity fullscreenWmFlagsActivity =
163                 launchAndWait(mFullscreenWmFlagsTestActivity);
164         insets = getOnMainSync(fullscreenWmFlagsActivity::getDispatchedInsets);
165         assertEquals("top insets must be consumed if FULLSCREEN is set",
166                 0, insets.getSystemWindowInsetTop());
167     }
168 
169     @Test
testNonAutomotiveFullScreenNotBlockedBySystemComponents()170     public void testNonAutomotiveFullScreenNotBlockedBySystemComponents() {
171         assumeFalse("Skipping test: Automotive is allowed to partially block fullscreen "
172                         + "applications with system bars.", isAutomotive());
173 
174         final TestActivity fullscreenActivity = launchAndWait(mFullscreenTestActivity);
175         View decorView = fullscreenActivity.getDecorView();
176         View contentView = decorView.findViewById(android.R.id.content);
177         boolean hasFullWidth = decorView.getMeasuredWidth() == contentView.getMeasuredWidth();
178         boolean hasFullHeight = decorView.getMeasuredHeight() == contentView.getMeasuredHeight();
179 
180         assertTrue(hasFullWidth && hasFullHeight);
181     }
182 
183     @Test
testImmersiveFullscreenHidesSystemBars()184     public void testImmersiveFullscreenHidesSystemBars() throws Throwable {
185         // Run the test twice, because the issue that shows system bars even in the immersive mode,
186         // happens at the 2nd try.
187         for (int i = 1; i <= 2; ++i) {
188             Log.d(TAG, "testImmersiveFullscreenHidesSystemBars: try" + i);
189 
190             TestActivity immersiveActivity = launchAndWait(mImmersiveTestActivity);
191             WindowInsets insets = getOnMainSync(immersiveActivity::getDispatchedInsets);
192 
193             assertFalse(insets.isVisible(WindowInsets.Type.statusBars()));
194             assertFalse(insets.isVisible(WindowInsets.Type.navigationBars()));
195 
196             WindowInsets rootInsets = getOnMainSync(immersiveActivity::getRootInsets);
197             assertFalse(rootInsets.isVisible(WindowInsets.Type.statusBars()));
198             assertFalse(rootInsets.isVisible(WindowInsets.Type.navigationBars()));
199 
200             View statusBarBgView = getOnMainSync(immersiveActivity::getStatusBarBackgroundView);
201             // The status bar background view can be non-existent or invisible.
202             assertTrue(statusBarBgView == null
203                     || statusBarBgView.getVisibility() == android.view.View.INVISIBLE);
204 
205             View navigationBarBgView = getOnMainSync(
206                     immersiveActivity::getNavigationBarBackgroundView);
207             // The navigation bar background view can be non-existent or invisible.
208             assertTrue(navigationBarBgView == null
209                     || navigationBarBgView.getVisibility() == android.view.View.INVISIBLE);
210         }
211     }
212 
commonAsserts(WindowInsets insets)213     private void commonAsserts(WindowInsets insets) {
214         assertForAllInsets("must be non-negative", insets, insetsGreaterThanOrEqualTo(Insets.NONE));
215 
216         assertThat("system gesture insets must include mandatory system gesture insets",
217                 insets.getMandatorySystemGestureInsets(),
218                 insetsLessThanOrEqualTo(insets.getSystemGestureInsets()));
219 
220         Insets stableAndSystem = Insets.min(insets.getSystemWindowInsets(),
221                 insets.getStableInsets());
222         assertThat("mandatory system gesture insets must include intersection between "
223                         + "stable and system window insets",
224                 stableAndSystem,
225                 insetsLessThanOrEqualTo(insets.getMandatorySystemGestureInsets()));
226 
227         assertThat("tappable insets must be at most system window insets",
228                 insets.getTappableElementInsets(),
229                 insetsLessThanOrEqualTo(insets.getSystemWindowInsets()));
230     }
231 
assertForAllInsets(String reason, WindowInsets actual, Matcher<? super Insets> matcher)232     private void assertForAllInsets(String reason, WindowInsets actual,
233             Matcher<? super Insets> matcher) {
234         assertThat("getSystemWindowInsets" + ": " + reason,
235                 actual.getSystemWindowInsets(), matcher);
236         assertThat("getStableInsets" + ": " + reason,
237                 actual.getStableInsets(), matcher);
238         assertThat("getSystemGestureInsets" + ": " + reason,
239                 actual.getSystemGestureInsets(), matcher);
240         assertThat("getMandatorySystemGestureInsets" + ": " + reason,
241                 actual.getMandatorySystemGestureInsets(), matcher);
242         assertThat("getTappableElementInsets" + ": " + reason,
243                 actual.getTappableElementInsets(), matcher);
244     }
245 
assertThat(String reason, T actual, Matcher<? super T> matcher)246     private <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) {
247         mErrorCollector.checkThat(reason, actual, matcher);
248     }
249 
getOnMainSync(Supplier<R> f)250     private <R> R getOnMainSync(Supplier<R> f) {
251         final Object[] result = new Object[1];
252         runOnMainSync(() -> result[0] = f.get());
253         //noinspection unchecked
254         return (R) result[0];
255     }
256 
runOnMainSync(Runnable runnable)257     private void runOnMainSync(Runnable runnable) {
258         getInstrumentation().runOnMainSync(runnable);
259     }
260 
launchAndWait(ActivityTestRule<T> rule)261     private <T extends Activity> T launchAndWait(ActivityTestRule<T> rule) {
262         final T activity = rule.launchActivity(null);
263         PollingCheck.waitFor(activity::hasWindowFocus);
264         return activity;
265     }
266 
isAutomotive()267     private boolean isAutomotive() {
268         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
269     }
270 
insetsLessThanOrEqualTo(Insets max)271     private static Matcher<Insets> insetsLessThanOrEqualTo(Insets max) {
272         return new CustomTypeSafeMatcher<Insets>("must be smaller on each side than " + max) {
273             @Override
274             protected boolean matchesSafely(Insets actual) {
275                 return actual.left <= max.left && actual.top <= max.top
276                         && actual.right <= max.right && actual.bottom <= max.bottom;
277             }
278         };
279     }
280 
281     private static Matcher<Insets> insetsGreaterThanOrEqualTo(Insets min) {
282         return new CustomTypeSafeMatcher<Insets>("must be greater on each side than " + min) {
283             @Override
284             protected boolean matchesSafely(Insets actual) {
285                 return actual.left >= min.left && actual.top >= min.top
286                         && actual.right >= min.right && actual.bottom >= min.bottom;
287             }
288         };
289     }
290 
291     public static class TestActivity extends Activity {
292 
293         private WindowInsets mDispatchedInsets;
294 
295         @Override
296         protected void onCreate(Bundle savedInstanceState) {
297             super.onCreate(savedInstanceState);
298             getWindow().requestFeature(Window.FEATURE_NO_TITLE);
299             View view = new View(this);
300             view.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
301             getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
302                     | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
303             view.setOnApplyWindowInsetsListener((v, insets) -> mDispatchedInsets = insets);
304             setContentView(view);
305         }
306 
307         View getDecorView() {
308             return getWindow().getDecorView();
309         }
310 
311         View getStatusBarBackgroundView() {
312             return getWindow().getStatusBarBackgroundView();
313         }
314 
315         View getNavigationBarBackgroundView() {
316             return getWindow().getNavigationBarBackgroundView();
317         }
318 
319         WindowInsets getRootInsets() {
320             return getWindow().getDecorView().getRootWindowInsets();
321         }
322 
323         WindowInsets getDispatchedInsets() {
324             return mDispatchedInsets;
325         }
326     }
327 
328     public static class FullscreenTestActivity extends TestActivity {
329 
330         @Override
331         protected void onCreate(Bundle savedInstanceState) {
332             super.onCreate(savedInstanceState);
333             getDecorView().setSystemUiVisibility(
334                     getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_FULLSCREEN);
335         }
336     }
337 
338     public static class FullscreenWmFlagsTestActivity extends TestActivity {
339 
340         @Override
341         protected void onCreate(Bundle savedInstanceState) {
342             super.onCreate(savedInstanceState);
343             getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN);
344         }
345     }
346 
347     public static class ImmersiveFullscreenTestActivity extends TestActivity {
348 
349         @Override
350         protected void onCreate(Bundle savedInstanceState) {
351             super.onCreate(savedInstanceState);
352             // See https://developer.android.com/training/system-ui/immersive#EnableFullscreen
353             getDecorView().setSystemUiVisibility(
354                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
355                     // Set the content to appear under the system bars so that the
356                     // content doesn't resize when the system bars hide and show.
357                     | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
358                     | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
359                     | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
360                     // Hide the nav bar and status bar
361                     | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
362                     | View.SYSTEM_UI_FLAG_FULLSCREEN);
363         }
364     }
365 
366     public static class NaturalOrientationTestActivity extends TestActivity {
367     }
368 }
369