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