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