• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.graphics.PixelFormat.TRANSLUCENT;
20 import static android.server.wm.ActivityManagerTestBase.isTablet;
21 import static android.view.KeyEvent.ACTION_DOWN;
22 import static android.view.KeyEvent.KEYCODE_BACK;
23 import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
24 import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
25 import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE;
26 import static android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE;
27 import static android.view.WindowInsets.Type.ime;
28 import static android.view.WindowInsets.Type.navigationBars;
29 import static android.view.WindowInsets.Type.statusBars;
30 import static android.view.WindowInsets.Type.systemBars;
31 import static android.view.WindowInsets.Type.systemGestures;
32 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
33 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
34 import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
35 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
36 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
37 import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN;
38 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
39 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
40 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
41 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
42 
43 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
44 
45 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
46 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
47 
48 import static org.hamcrest.Matchers.is;
49 import static org.hamcrest.Matchers.notNullValue;
50 import static org.hamcrest.Matchers.nullValue;
51 import static org.junit.Assert.assertEquals;
52 import static org.junit.Assert.assertFalse;
53 import static org.junit.Assert.assertTrue;
54 import static org.junit.Assume.assumeFalse;
55 import static org.junit.Assume.assumeThat;
56 import static org.junit.Assume.assumeTrue;
57 
58 import android.app.Activity;
59 import android.app.AlertDialog;
60 import android.app.Instrumentation;
61 import android.content.Context;
62 import android.content.pm.PackageManager;
63 import android.content.res.Resources;
64 import android.os.Bundle;
65 import android.os.SystemClock;
66 import android.platform.test.annotations.Presubmit;
67 import android.view.InputDevice;
68 import android.view.MotionEvent;
69 import android.view.View;
70 import android.view.ViewGroup;
71 import android.view.Window;
72 import android.view.WindowInsets;
73 import android.view.WindowInsetsAnimation;
74 import android.view.WindowInsetsController;
75 import android.view.WindowManager;
76 import android.widget.EditText;
77 import android.widget.LinearLayout;
78 import android.widget.TextView;
79 
80 import androidx.annotation.Nullable;
81 
82 import com.android.compatibility.common.util.PollingCheck;
83 import com.android.cts.mockime.ImeEventStream;
84 import com.android.cts.mockime.ImeSettings;
85 import com.android.cts.mockime.MockImeSession;
86 
87 import org.junit.Rule;
88 import org.junit.Test;
89 import org.junit.rules.ErrorCollector;
90 
91 import java.util.ArrayList;
92 import java.util.List;
93 import java.util.concurrent.CountDownLatch;
94 import java.util.concurrent.TimeUnit;
95 import java.util.function.Supplier;
96 
97 /**
98  * Test whether WindowInsetsController controls window insets as expected.
99  *
100  * Build/Install/Run:
101  *     atest CtsWindowManagerDeviceTestCases:WindowInsetsControllerTests
102  */
103 @Presubmit
104 public class WindowInsetsControllerTests extends WindowManagerTestBase {
105 
106     private final static long TIMEOUT = 1000; // milliseconds
107     private final static long TIMEOUT_UPDATING_INPUT_WINDOW = 500; // milliseconds
108     private final static long TIME_SLICE = 50; // milliseconds
109     private final static AnimationCallback ANIMATION_CALLBACK = new AnimationCallback();
110 
111     private static final String AM_BROADCAST_CLOSE_SYSTEM_DIALOGS =
112             "am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS";
113 
114     @Rule
115     public final ErrorCollector mErrorCollector = new ErrorCollector();
116 
117     @Test
testHide()118     public void testHide() {
119         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
120 
121         final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
122         final View rootView = activity.getWindow().getDecorView();
123 
124         testHideInternal(rootView, statusBars());
125         testHideInternal(rootView, navigationBars());
126     }
127 
testHideInternal(View rootView, int types)128     private void testHideInternal(View rootView, int types) {
129         if (rootView.getRootWindowInsets().isVisible(types)) {
130             getInstrumentation().runOnMainSync(() -> {
131                 rootView.getWindowInsetsController().hide(types);
132             });
133             PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
134         }
135     }
136 
137     @Test
testShow()138     public void testShow() {
139         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
140 
141         final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
142         final View rootView = activity.getWindow().getDecorView();
143 
144         testShowInternal(rootView, statusBars());
145         testShowInternal(rootView, navigationBars());
146     }
147 
testShowInternal(View rootView, int types)148     private void testShowInternal(View rootView, int types) {
149         if (rootView.getRootWindowInsets().isVisible(types)) {
150             getInstrumentation().runOnMainSync(() -> {
151                 rootView.getWindowInsetsController().hide(types);
152             });
153             PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
154             getInstrumentation().runOnMainSync(() -> {
155                 rootView.getWindowInsetsController().show(types);
156             });
157             PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
158         }
159     }
160 
testTopAppHidesStatusBarInternal(Activity activity, View rootView, Runnable hidingStatusBar)161     private void testTopAppHidesStatusBarInternal(Activity activity, View rootView,
162             Runnable hidingStatusBar) {
163         if (rootView.getRootWindowInsets().isVisible(statusBars())) {
164 
165             // The top-fullscreen-app window hides status bar.
166             getInstrumentation().runOnMainSync(hidingStatusBar);
167             PollingCheck.waitFor(TIMEOUT,
168                     () -> !rootView.getRootWindowInsets().isVisible(statusBars()));
169 
170             // Add a non-fullscreen window on top of the fullscreen window.
171             // The new focused window doesn't hide status bar.
172             getInstrumentation().runOnMainSync(
173                     () -> activity.getWindowManager().addView(
174                             new View(activity),
175                             new WindowManager.LayoutParams(1 /* w */, 1 /* h */, TYPE_APPLICATION,
176                                     0 /* flags */, TRANSLUCENT)));
177 
178             // Check if status bar stays invisible.
179             for (long time = TIMEOUT; time >= 0; time -= TIME_SLICE) {
180                 assertFalse(rootView.getRootWindowInsets().isVisible(statusBars()));
181                 SystemClock.sleep(TIME_SLICE);
182             }
183         }
184     }
185 
186     @Test
testTopAppHidesStatusBarByMethod()187     public void testTopAppHidesStatusBarByMethod() {
188         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
189 
190         final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
191         final View rootView = activity.getWindow().getDecorView();
192 
193         testTopAppHidesStatusBarInternal(activity, rootView,
194                 () -> rootView.getWindowInsetsController().hide(statusBars()));
195     }
196 
197     @Test
testTopAppHidesStatusBarByWindowFlag()198     public void testTopAppHidesStatusBarByWindowFlag() {
199         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
200 
201         final TestActivity activity = startActivity(TestActivity.class);
202         final View rootView = activity.getWindow().getDecorView();
203 
204         testTopAppHidesStatusBarInternal(activity, rootView,
205                 () -> activity.getWindow().addFlags(FLAG_FULLSCREEN));
206     }
207 
208     @Test
testTopAppHidesStatusBarBySystemUiFlag()209     public void testTopAppHidesStatusBarBySystemUiFlag() {
210         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
211 
212         final TestActivity activity = startActivity(TestActivity.class);
213         final View rootView = activity.getWindow().getDecorView();
214 
215         testTopAppHidesStatusBarInternal(activity, rootView,
216                 () -> rootView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN));
217     }
218 
219     @Test
testImeShowAndHide()220     public void testImeShowAndHide() throws Exception {
221         final Instrumentation instrumentation = getInstrumentation();
222         assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
223                 nullValue());
224         final MockImeSession imeSession = MockImeHelper.createManagedMockImeSession(this);
225         final ImeEventStream stream = imeSession.openEventStream();
226         final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
227         expectEvent(stream, editorMatcher("onStartInput", activity.mEditTextMarker), TIMEOUT);
228 
229         final View rootView = activity.getWindow().getDecorView();
230         getInstrumentation().runOnMainSync(() -> rootView.getWindowInsetsController().show(ime()));
231         PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(ime()));
232         getInstrumentation().runOnMainSync(() -> rootView.getWindowInsetsController().hide(ime()));
233         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(ime()));
234     }
235 
236     @Test
testImeForceShowingNavigationBar()237     public void testImeForceShowingNavigationBar() throws Exception {
238         final Instrumentation instrumentation = getInstrumentation();
239         assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
240                 nullValue());
241         final Resources resources = instrumentation.getContext().getResources();
242         final boolean isHideNavBarForKeyboardEnabled = resources.getBoolean(
243                 resources.getIdentifier("config_hideNavBarForKeyboard", "bool", "android"));
244         assumeFalse("Device is configured to not show navigation bar for keyboard",
245                 isHideNavBarForKeyboardEnabled);
246         final MockImeSession imeSession = MockImeHelper.createManagedMockImeSession(this);
247         final ImeEventStream stream = imeSession.openEventStream();
248         final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
249         expectEvent(stream, editorMatcher("onStartInput", activity.mEditTextMarker), TIMEOUT);
250 
251         final View rootView = activity.getWindow().getDecorView();
252         assumeTrue(rootView.getRootWindowInsets().isVisible(navigationBars()));
253         getInstrumentation().runOnMainSync(
254                 () -> rootView.getWindowInsetsController().hide(navigationBars()));
255         PollingCheck.waitFor(TIMEOUT,
256                 () -> !rootView.getRootWindowInsets().isVisible(navigationBars()));
257         getInstrumentation().runOnMainSync(() -> rootView.getWindowInsetsController().show(ime()));
258         PollingCheck.waitFor(TIMEOUT,
259                 () -> rootView.getRootWindowInsets().isVisible(ime() | navigationBars()));
260         getInstrumentation().runOnMainSync(() -> rootView.getWindowInsetsController().hide(ime()));
261         PollingCheck.waitFor(TIMEOUT,
262                 () -> !rootView.getRootWindowInsets().isVisible(ime())
263                         && !rootView.getRootWindowInsets().isVisible(navigationBars()));
264     }
265 
266     @Test
testSetSystemBarsAppearance()267     public void testSetSystemBarsAppearance() {
268         final TestActivity activity = startActivity(TestActivity.class);
269         final View rootView = activity.getWindow().getDecorView();
270         final WindowInsetsController controller = rootView.getWindowInsetsController();
271         getInstrumentation().runOnMainSync(() -> {
272             // Set APPEARANCE_LIGHT_STATUS_BARS.
273             controller.setSystemBarsAppearance(
274                     APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS);
275 
276             // Clear APPEARANCE_LIGHT_NAVIGATION_BARS.
277             controller.setSystemBarsAppearance(
278                     0 /* appearance */, APPEARANCE_LIGHT_NAVIGATION_BARS);
279         });
280         waitForIdle();
281 
282         // We must have APPEARANCE_LIGHT_STATUS_BARS, but not APPEARANCE_LIGHT_NAVIGATION_BARS.
283         assertEquals(APPEARANCE_LIGHT_STATUS_BARS,
284                 controller.getSystemBarsAppearance()
285                         & (APPEARANCE_LIGHT_STATUS_BARS | APPEARANCE_LIGHT_NAVIGATION_BARS));
286 
287         final boolean[] onPreDrawCalled = { false };
288         rootView.getViewTreeObserver().addOnPreDrawListener(() -> {
289             onPreDrawCalled[0] = true;
290             return true;
291         });
292 
293         // Clear APPEARANCE_LIGHT_NAVIGATION_BARS again.
294         getInstrumentation().runOnMainSync(() -> controller.setSystemBarsAppearance(
295                 0 /* appearance */, APPEARANCE_LIGHT_NAVIGATION_BARS));
296         waitForIdle();
297 
298         assertFalse("Setting the same appearance must not cause a new traversal",
299                 onPreDrawCalled[0]);
300     }
301 
302     @Test
testSetSystemBarsBehavior_default()303     public void testSetSystemBarsBehavior_default() throws InterruptedException {
304         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
305 
306         final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
307         final View rootView = activity.getWindow().getDecorView();
308 
309         // Assume we have the bars and they can be visible.
310         final int types = statusBars();
311         assumeTrue(rootView.getRootWindowInsets().isVisible(types));
312 
313         rootView.getWindowInsetsController().setSystemBarsBehavior(BEHAVIOR_DEFAULT);
314 
315         hideInsets(rootView, types);
316 
317         // Tapping on display cannot show bars.
318         tapOnDisplay(rootView.getWidth() / 2f, rootView.getHeight() / 2f);
319         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
320 
321         // Wait for status bar invisible from InputDispatcher. Otherwise, the following
322         // dragFromTopToCenter might expand notification shade.
323         SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW);
324 
325         // Swiping from top of display can show bars.
326         dragFromTopToCenter(rootView);
327         PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
328     }
329 
330     @Test
testSetSystemBarsBehavior_showTransientBarsBySwipe()331     public void testSetSystemBarsBehavior_showTransientBarsBySwipe() throws InterruptedException {
332         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
333 
334         final TestActivity activity = startActivity(TestActivity.class);
335         final View rootView = activity.getWindow().getDecorView();
336 
337         // Assume we have the bars and they can be visible.
338         final int types = statusBars();
339         assumeTrue(rootView.getRootWindowInsets().isVisible(types));
340 
341         rootView.getWindowInsetsController().setSystemBarsBehavior(
342                 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
343 
344         hideInsets(rootView, types);
345 
346         // Tapping on display cannot show bars.
347         tapOnDisplay(rootView.getWidth() / 2f, rootView.getHeight() / 2f);
348         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
349 
350         // Wait for status bar invisible from InputDispatcher. Otherwise, the following
351         // dragFromTopToCenter might expand notification shade.
352         SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW);
353 
354         // Swiping from top of display can show transient bars, but apps cannot detect that.
355         dragFromTopToCenter(rootView);
356         // Make sure status bar stays invisible.
357         for (long time = TIMEOUT; time >= 0; time -= TIME_SLICE) {
358             assertFalse(rootView.getRootWindowInsets().isVisible(types));
359             SystemClock.sleep(TIME_SLICE);
360         }
361     }
362 
363     @Test
testSetSystemBarsBehavior_systemGesture_default()364     public void testSetSystemBarsBehavior_systemGesture_default() throws InterruptedException {
365         final TestActivity activity = startActivity(TestActivity.class);
366         final View rootView = activity.getWindow().getDecorView();
367 
368         // Assume the current navigation mode has the back gesture.
369         assumeTrue(rootView.getRootWindowInsets().getInsets(systemGestures()).left > 0);
370         assumeTrue(canTriggerBackGesture(rootView));
371 
372         rootView.getWindowInsetsController().setSystemBarsBehavior(BEHAVIOR_DEFAULT);
373         hideInsets(rootView, systemBars());
374 
375         // Test if the back gesture can be triggered while system bars are hidden with the behavior.
376         assertTrue(canTriggerBackGesture(rootView));
377     }
378 
379     @Test
testSetSystemBarsBehavior_systemGesture_showTransientBarsBySwipe()380     public void testSetSystemBarsBehavior_systemGesture_showTransientBarsBySwipe()
381             throws InterruptedException {
382         final TestActivity activity = startActivity(TestActivity.class);
383         final View rootView = activity.getWindow().getDecorView();
384 
385         // Assume the current navigation mode has the back gesture.
386         assumeTrue(rootView.getRootWindowInsets().getInsets(systemGestures()).left > 0);
387         assumeTrue(canTriggerBackGesture(rootView));
388 
389         rootView.getWindowInsetsController().setSystemBarsBehavior(
390                 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
391         hideInsets(rootView, systemBars());
392 
393         // Test if the back gesture can be triggered while system bars are hidden with the behavior.
394         assertFalse(canTriggerBackGesture(rootView));
395     }
396 
canTriggerBackGesture(View rootView)397     private boolean canTriggerBackGesture(View rootView) throws InterruptedException {
398         final boolean[] hasBack = { false };
399         final CountDownLatch latch = new CountDownLatch(1);
400         rootView.findFocus().setOnKeyListener((v, keyCode, event) -> {
401             if (keyCode == KEYCODE_BACK && event.getAction() == ACTION_DOWN) {
402                 hasBack[0] = true;
403                 latch.countDown();
404                 return true;
405             }
406             return false;
407         });
408         dragFromLeftToCenter(rootView);
409         latch.await(1, TimeUnit.SECONDS);
410         return hasBack[0];
411     }
412 
413     @Test
testSystemUiVisibilityCallbackCausedByInsets()414     public void testSystemUiVisibilityCallbackCausedByInsets() {
415         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
416 
417         final TestActivity activity = startActivity(TestActivity.class);
418         final View controlTarget = activity.getWindow().getDecorView();
419         final int[] targetSysUiVis = new int[1];
420         final View nonControlTarget = new View(mTargetContext);
421         final int[] nonTargetSysUiVis = new int[1];
422         if (isTablet()) {
423             return;
424         }
425         final WindowManager.LayoutParams nonTargetAttrs =
426                 new WindowManager.LayoutParams(TYPE_APPLICATION);
427         nonTargetAttrs.flags = FLAG_NOT_FOCUSABLE;
428         getInstrumentation().runOnMainSync(() -> {
429             controlTarget.setOnSystemUiVisibilityChangeListener(
430                     visibility -> targetSysUiVis[0] = visibility);
431             nonControlTarget.setOnSystemUiVisibilityChangeListener(
432                     visibility -> nonTargetSysUiVis[0] = visibility);
433             activity.getWindowManager().addView(nonControlTarget, nonTargetAttrs);
434         });
435         waitForIdle();
436         testSysUiVisCallbackCausedByInsets(statusBars(), SYSTEM_UI_FLAG_FULLSCREEN,
437                 controlTarget, targetSysUiVis, nonTargetSysUiVis);
438         testSysUiVisCallbackCausedByInsets(navigationBars(), SYSTEM_UI_FLAG_HIDE_NAVIGATION,
439                 controlTarget, targetSysUiVis, nonTargetSysUiVis);
440     }
441 
testSysUiVisCallbackCausedByInsets(int insetsType, int sysUiFlag, View target, int[] targetSysUiVis, int[] nonTargetSysUiVis)442     private void testSysUiVisCallbackCausedByInsets(int insetsType, int sysUiFlag, View target,
443             int[] targetSysUiVis, int[] nonTargetSysUiVis) {
444         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
445         if (target.getRootWindowInsets().isVisible(insetsType)) {
446 
447             // Controlled by methods
448             getInstrumentation().runOnMainSync(
449                     () -> target.getWindowInsetsController().hide(insetsType));
450             PollingCheck.waitFor(TIMEOUT, () ->
451                     targetSysUiVis[0] == sysUiFlag && targetSysUiVis[0] == nonTargetSysUiVis[0]);
452             getInstrumentation().runOnMainSync(
453                     () -> target.getWindowInsetsController().show(insetsType));
454             PollingCheck.waitFor(TIMEOUT, () ->
455                     targetSysUiVis[0] == 0 && targetSysUiVis[0] == nonTargetSysUiVis[0]);
456 
457             // Controlled by legacy flags
458             getInstrumentation().runOnMainSync(
459                     () -> target.setSystemUiVisibility(sysUiFlag));
460             PollingCheck.waitFor(TIMEOUT, () ->
461                     targetSysUiVis[0] == sysUiFlag && targetSysUiVis[0] == nonTargetSysUiVis[0]);
462             getInstrumentation().runOnMainSync(
463                     () -> target.setSystemUiVisibility(0));
464             PollingCheck.waitFor(TIMEOUT, () ->
465                     targetSysUiVis[0] == 0 && targetSysUiVis[0] == nonTargetSysUiVis[0]);
466         }
467     }
468 
469     @Test
testSystemUiVisibilityCallbackCausedByAppearance()470     public void testSystemUiVisibilityCallbackCausedByAppearance() {
471         final TestActivity activity = startActivity(TestActivity.class);
472         final View controlTarget = activity.getWindow().getDecorView();
473 
474         // Assume we have at least one visible system bar.
475         assumeTrue(controlTarget.getRootWindowInsets().isVisible(statusBars()) ||
476                 controlTarget.getRootWindowInsets().isVisible(navigationBars()));
477 
478         final int[] targetSysUiVis = new int[1];
479         getInstrumentation().runOnMainSync(() -> {
480             controlTarget.setOnSystemUiVisibilityChangeListener(
481                     visibility -> targetSysUiVis[0] = visibility);
482         });
483         waitForIdle();
484         final int sysUiFlag = SYSTEM_UI_FLAG_LOW_PROFILE;
485         getInstrumentation().runOnMainSync(() -> controlTarget.setSystemUiVisibility(sysUiFlag));
486         PollingCheck.waitFor(TIMEOUT, () -> targetSysUiVis[0] == sysUiFlag);
487         getInstrumentation().runOnMainSync(() -> controlTarget.setSystemUiVisibility(0));
488         PollingCheck.waitFor(TIMEOUT, () -> targetSysUiVis[0] == 0);
489     }
490 
491     @Test
testSetSystemUiVisibilityAfterCleared_showBarsBySwipe()492     public void testSetSystemUiVisibilityAfterCleared_showBarsBySwipe() throws Exception {
493         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
494 
495         final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
496         final View rootView = activity.getWindow().getDecorView();
497 
498         // Assume we have the bars and they can be visible.
499         final int types = statusBars();
500         assumeTrue(rootView.getRootWindowInsets().isVisible(types));
501 
502         final int targetFlags = SYSTEM_UI_FLAG_IMMERSIVE | SYSTEM_UI_FLAG_FULLSCREEN;
503 
504         // Use flags to hide status bar.
505         ANIMATION_CALLBACK.reset();
506         getInstrumentation().runOnMainSync(() -> {
507             rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
508             rootView.setSystemUiVisibility(targetFlags);
509         });
510         ANIMATION_CALLBACK.waitForFinishing();
511         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
512 
513         // Wait for status bar invisible from InputDispatcher. Otherwise, the following
514         // dragFromTopToCenter might expand notification shade.
515         SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW);
516 
517         // Swiping from top of display can show bars.
518         ANIMATION_CALLBACK.reset();
519         dragFromTopToCenter(rootView);
520         ANIMATION_CALLBACK.waitForFinishing();
521         PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types)
522             && rootView.getSystemUiVisibility() != targetFlags);
523 
524         // Use flags to hide status bar again.
525         ANIMATION_CALLBACK.reset();
526         getInstrumentation().runOnMainSync(() -> {
527             rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
528             rootView.setSystemUiVisibility(targetFlags);
529         });
530         ANIMATION_CALLBACK.waitForFinishing();
531         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
532 
533         // Wait for status bar invisible from InputDispatcher. Otherwise, the following
534         // dragFromTopToCenter might expand notification shade.
535         SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW);
536 
537         // Swiping from top of display can show bars.
538         ANIMATION_CALLBACK.reset();
539         dragFromTopToCenter(rootView);
540         ANIMATION_CALLBACK.waitForFinishing();
541         PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
542 
543         // The swipe action brings down the notification shade which causes subsequent tests to
544         // fail.
545         if (isAutomotive(mContext)) {
546             // Bring system to a known state before requesting to close system dialogs.
547             launchHomeActivity();
548             broadcastCloseSystemDialogs();
549         }
550     }
551 
552     @Test
testSetSystemUiVisibilityAfterCleared_showBarsByApp()553     public void testSetSystemUiVisibilityAfterCleared_showBarsByApp() throws Exception {
554         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
555 
556         final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
557         final View rootView = activity.getWindow().getDecorView();
558 
559         // Assume we have the bars and they can be visible.
560         final int types = statusBars();
561         assumeTrue(rootView.getRootWindowInsets().isVisible(types));
562 
563         // Use the flag to hide status bar.
564         ANIMATION_CALLBACK.reset();
565         getInstrumentation().runOnMainSync(() -> {
566             rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
567             rootView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN);
568         });
569         ANIMATION_CALLBACK.waitForFinishing();
570         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
571 
572         // Clearing the flag can show status bar.
573         getInstrumentation().runOnMainSync(() -> {
574             rootView.setSystemUiVisibility(0);
575         });
576         PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
577 
578         // Use the flag to hide status bar again.
579         ANIMATION_CALLBACK.reset();
580         getInstrumentation().runOnMainSync(() -> {
581             rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
582             rootView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN);
583         });
584         ANIMATION_CALLBACK.waitForFinishing();
585         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
586 
587         // Clearing the flag can show status bar.
588         getInstrumentation().runOnMainSync(() -> {
589             rootView.setSystemUiVisibility(0);
590         });
591         PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
592     }
593 
594     @Test
testHideOnCreate()595     public void testHideOnCreate() throws Exception {
596         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
597 
598         final TestHideOnCreateActivity activity =
599                 startActivityInWindowingModeFullScreen(TestHideOnCreateActivity.class);
600         final View rootView = activity.getWindow().getDecorView();
601         ANIMATION_CALLBACK.waitForFinishing();
602         PollingCheck.waitFor(TIMEOUT,
603                 () -> !rootView.getRootWindowInsets().isVisible(statusBars())
604                         && !rootView.getRootWindowInsets().isVisible(navigationBars()));
605     }
606 
607     @Test
testShowImeOnCreate()608     public void testShowImeOnCreate() throws Exception {
609         final Instrumentation instrumentation = getInstrumentation();
610         assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
611                 nullValue());
612         MockImeHelper.createManagedMockImeSession(this);
613         final TestShowOnCreateActivity activity = startActivity(TestShowOnCreateActivity.class);
614         final View rootView = activity.getWindow().getDecorView();
615         ANIMATION_CALLBACK.waitForFinishing();
616         PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(ime()));
617     }
618 
619     @Test
testShowImeOnCreate_doesntCauseImeToReappearWhenDialogIsShown()620     public void testShowImeOnCreate_doesntCauseImeToReappearWhenDialogIsShown() throws Exception {
621         final Instrumentation instrumentation = getInstrumentation();
622         assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
623                 nullValue());
624         try (MockImeSession imeSession = MockImeSession.create(instrumentation.getContext(),
625                 instrumentation.getUiAutomation(), new ImeSettings.Builder())) {
626             final TestShowOnCreateActivity activity =
627                     startActivityInWindowingModeFullScreen(TestShowOnCreateActivity.class);
628             final View rootView = activity.getWindow().getDecorView();
629             PollingCheck.waitFor(TIMEOUT,
630                     () -> rootView.getRootWindowInsets().isVisible(ime()));
631             ANIMATION_CALLBACK.waitForFinishing();
632             ANIMATION_CALLBACK.reset();
633             getInstrumentation().runOnMainSync(() ->  {
634                 rootView.getWindowInsetsController().hide(ime());
635             });
636             PollingCheck.waitFor(TIMEOUT,
637                     () -> !rootView.getRootWindowInsets().isVisible(ime()));
638             ANIMATION_CALLBACK.waitForFinishing();
639             getInstrumentation().runOnMainSync(() ->  {
640                 activity.showAltImDialog();
641             });
642 
643             for (long time = TIMEOUT; time >= 0; time -= TIME_SLICE) {
644                 assertFalse("IME visible when it shouldn't be",
645                         rootView.getRootWindowInsets().isVisible(ime()));
646                 SystemClock.sleep(TIME_SLICE);
647             }
648         }
649     }
650 
651     @Test
testShowIme_immediatelyAfterDetachAndReattach()652     public void testShowIme_immediatelyAfterDetachAndReattach() throws Exception {
653         final Instrumentation instrumentation = getInstrumentation();
654         assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
655                 nullValue());
656         MockImeHelper.createManagedMockImeSession(this);
657         final TestActivity activity = startActivity(TestActivity.class);
658         final View rootView = activity.getWindow().getDecorView();
659 
660         PollingCheck.waitFor(TIMEOUT, () -> getOnMainSync(rootView::hasWindowFocus));
661 
662         View editor = getOnMainSync(rootView::findFocus);
663         ViewGroup parent = (ViewGroup) getOnMainSync(editor::getParent);
664 
665         getInstrumentation().runOnMainSync(() -> {
666             parent.removeView(editor);
667         });
668 
669         // Wait until checkFocus() is dispatched
670         getInstrumentation().waitForIdleSync();
671 
672         getInstrumentation().runOnMainSync(() -> {
673             parent.addView(editor);
674             editor.requestFocus();
675             editor.getWindowInsetsController().show(ime());
676         });
677 
678         PollingCheck.waitFor(TIMEOUT, () -> getOnMainSync(
679                 () -> rootView.getRootWindowInsets().isVisible(ime())),
680                 "Expected IME to become visible but didn't.");
681     }
682 
683     @Test
testInsetsDispatch()684     public void testInsetsDispatch() throws Exception {
685         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
686 
687         // Start an activity which hides system bars in fullscreen mode,
688         // otherwise, it might not be able to hide system bars in other windowing modes.
689         final TestHideOnCreateActivity activity = startActivityInWindowingModeFullScreen(
690                 TestHideOnCreateActivity.class);
691         final View rootView = activity.getWindow().getDecorView();
692         ANIMATION_CALLBACK.waitForFinishing();
693         PollingCheck.waitFor(TIMEOUT,
694                 () -> !rootView.getRootWindowInsets().isVisible(statusBars())
695                         && !rootView.getRootWindowInsets().isVisible(navigationBars()));
696 
697         // Add a dialog which hides system bars before the dialog is added to the system while the
698         // system bar was hidden previously, and collect the window insets that the dialog receives.
699         final ArrayList<WindowInsets> windowInsetsList = new ArrayList<>();
700         getInstrumentation().runOnMainSync(() -> {
701             final AlertDialog dialog = new AlertDialog.Builder(activity).create();
702             final Window dialogWindow = dialog.getWindow();
703             dialogWindow.getDecorView().setOnApplyWindowInsetsListener((view, insets) -> {
704                 windowInsetsList.add(insets);
705                 return view.onApplyWindowInsets(insets);
706             });
707             dialogWindow.getInsetsController().hide(statusBars() | navigationBars());
708             dialog.show();
709         });
710         getInstrumentation().waitForIdleSync();
711 
712         // The dialog must never receive any of visible insets of system bars.
713         for (WindowInsets windowInsets : windowInsetsList) {
714             assertFalse(windowInsets.isVisible(statusBars()));
715             assertFalse(windowInsets.isVisible(navigationBars()));
716         }
717     }
718 
719     @Test
testWindowInsetsController_availableAfterAddView()720     public void testWindowInsetsController_availableAfterAddView() throws Exception {
721         assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars());
722 
723         final TestHideOnCreateActivity activity =
724                 startActivityInWindowingModeFullScreen(TestHideOnCreateActivity.class);
725         final View rootView = activity.getWindow().getDecorView();
726         ANIMATION_CALLBACK.waitForFinishing();
727         PollingCheck.waitFor(TIMEOUT,
728                 () -> !rootView.getRootWindowInsets().isVisible(statusBars())
729                         && !rootView.getRootWindowInsets().isVisible(navigationBars()));
730 
731         final View childWindow = new View(activity);
732         getInstrumentation().runOnMainSync(() -> {
733             activity.getWindowManager().addView(childWindow,
734                     new WindowManager.LayoutParams(TYPE_APPLICATION));
735             mErrorCollector.checkThat(childWindow.getWindowInsetsController(), is(notNullValue()));
736         });
737         getInstrumentation().waitForIdleSync();
738         getInstrumentation().runOnMainSync(() -> {
739             activity.getWindowManager().removeView(childWindow);
740         });
741 
742     }
743 
744     @Test
testDispatchApplyWindowInsetsCount_systemBars()745     public void testDispatchApplyWindowInsetsCount_systemBars() throws InterruptedException {
746         final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
747         final View rootView = activity.getWindow().getDecorView();
748         getInstrumentation().waitForIdleSync();
749 
750         // Assume we have at least one visible system bar.
751         assumeTrue(rootView.getRootWindowInsets().isVisible(statusBars())
752                 || rootView.getRootWindowInsets().isVisible(navigationBars()));
753 
754         getInstrumentation().runOnMainSync(() -> {
755             // This makes the window frame stable while changing the system bar visibility.
756             final WindowManager.LayoutParams attrs = activity.getWindow().getAttributes();
757             attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
758             activity.getWindow().setAttributes(attrs);
759         });
760         getInstrumentation().waitForIdleSync();
761 
762         final int[] dispatchApplyWindowInsetsCount = {0};
763         rootView.setOnApplyWindowInsetsListener((v, insets) -> {
764             dispatchApplyWindowInsetsCount[0]++;
765             return v.onApplyWindowInsets(insets);
766         });
767 
768         // One hide-system-bar call...
769         ANIMATION_CALLBACK.reset();
770         getInstrumentation().runOnMainSync(() -> {
771             rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
772             rootView.getWindowInsetsController().hide(systemBars());
773         });
774         ANIMATION_CALLBACK.waitForFinishing();
775 
776         // ... should only trigger one dispatchApplyWindowInsets
777         assertEquals(1, dispatchApplyWindowInsetsCount[0]);
778 
779         // One show-system-bar call...
780         dispatchApplyWindowInsetsCount[0] = 0;
781         ANIMATION_CALLBACK.reset();
782         getInstrumentation().runOnMainSync(() -> {
783             rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
784             rootView.getWindowInsetsController().show(systemBars());
785         });
786         ANIMATION_CALLBACK.waitForFinishing();
787 
788         // ... should only trigger one dispatchApplyWindowInsets
789         assertEquals(1, dispatchApplyWindowInsetsCount[0]);
790     }
791 
792     @Test
testDispatchApplyWindowInsetsCount_ime()793     public void testDispatchApplyWindowInsetsCount_ime() throws Exception {
794         assumeFalse("Automotive is to skip this test until showing and hiding certain insets "
795                 + "simultaneously in a single request is supported", isAutomotive(mContext));
796         assumeThat(MockImeSession.getUnavailabilityReason(getInstrumentation().getContext()),
797                 nullValue());
798 
799         MockImeHelper.createManagedMockImeSession(this);
800         final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class);
801         final View rootView = activity.getWindow().getDecorView();
802         getInstrumentation().waitForIdleSync();
803 
804         final int[] dispatchApplyWindowInsetsCount = {0};
805         rootView.setOnApplyWindowInsetsListener((v, insets) -> {
806             dispatchApplyWindowInsetsCount[0]++;
807             return v.onApplyWindowInsets(insets);
808         });
809 
810         // One show-ime call...
811         ANIMATION_CALLBACK.reset();
812         getInstrumentation().runOnMainSync(() -> {
813             rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
814             rootView.getWindowInsetsController().show(ime());
815         });
816         ANIMATION_CALLBACK.waitForFinishing();
817 
818         // ... should only trigger one dispatchApplyWindowInsets
819         assertEquals(1, dispatchApplyWindowInsetsCount[0]);
820 
821         // One hide-ime call...
822         dispatchApplyWindowInsetsCount[0] = 0;
823         ANIMATION_CALLBACK.reset();
824         getInstrumentation().runOnMainSync(() -> {
825             rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
826             rootView.getWindowInsetsController().hide(ime());
827         });
828         ANIMATION_CALLBACK.waitForFinishing();
829 
830         // ... should only trigger one dispatchApplyWindowInsets
831         assertEquals(1, dispatchApplyWindowInsetsCount[0]);
832     }
833 
broadcastCloseSystemDialogs()834     private static void broadcastCloseSystemDialogs() {
835         executeShellCommand(AM_BROADCAST_CLOSE_SYSTEM_DIALOGS);
836     }
837 
isAutomotive(Context context)838     private static boolean isAutomotive(Context context) {
839         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
840     }
841 
hideInsets(View view, int types)842     private static void hideInsets(View view, int types) throws InterruptedException {
843         ANIMATION_CALLBACK.reset();
844         getInstrumentation().runOnMainSync(() -> {
845             view.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
846             view.getWindowInsetsController().hide(types);
847         });
848         ANIMATION_CALLBACK.waitForFinishing();
849         PollingCheck.waitFor(TIMEOUT, () -> !view.getRootWindowInsets().isVisible(types));
850     }
851 
tapOnDisplay(float x, float y)852     private void tapOnDisplay(float x, float y) {
853         dragOnDisplay(x, y, x, y);
854     }
855 
dragFromTopToCenter(View view)856     private void dragFromTopToCenter(View view) {
857         dragOnDisplay(view.getWidth() / 2f, 0 /* downY */,
858                 view.getWidth() / 2f, view.getHeight() / 2f);
859     }
860 
dragFromLeftToCenter(View view)861     private void dragFromLeftToCenter(View view) {
862         dragOnDisplay(0 /* downX */, view.getHeight() / 2f,
863                 view.getWidth() / 2f, view.getHeight() / 2f);
864     }
865 
dragOnDisplay(float downX, float downY, float upX, float upY)866     private void dragOnDisplay(float downX, float downY, float upX, float upY) {
867         final long downTime = SystemClock.elapsedRealtime();
868 
869         // down event
870         MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,
871                 downX, downY, 0 /* metaState */);
872         sendPointerSync(event);
873         event.recycle();
874 
875         // move event
876         event = MotionEvent.obtain(downTime, downTime + 1, MotionEvent.ACTION_MOVE,
877                 (downX + upX) / 2f, (downY + upY) / 2f, 0 /* metaState */);
878         sendPointerSync(event);
879         event.recycle();
880 
881         // up event
882         event = MotionEvent.obtain(downTime, downTime + 2, MotionEvent.ACTION_UP,
883                 upX, upY, 0 /* metaState */);
884         sendPointerSync(event);
885         event.recycle();
886     }
887 
sendPointerSync(MotionEvent event)888     private void sendPointerSync(MotionEvent event) {
889         event.setSource(event.getSource() | InputDevice.SOURCE_CLASS_POINTER);
890         // Use UiAutomation to inject into TestActivity because it is started and owned by the
891         // Shell, which has a different uid than this instrumentation.
892         getInstrumentation().getUiAutomation().injectInputEvent(event, true);
893     }
894 
895     private static class AnimationCallback extends WindowInsetsAnimation.Callback {
896 
897         private static final long ANIMATION_TIMEOUT = 5000; // milliseconds
898 
899         private boolean mFinished = false;
900 
AnimationCallback()901         AnimationCallback() {
902             super(DISPATCH_MODE_CONTINUE_ON_SUBTREE);
903         }
904 
905         @Override
onProgress(WindowInsets insets, List<WindowInsetsAnimation> runningAnimations)906         public WindowInsets onProgress(WindowInsets insets,
907                 List<WindowInsetsAnimation> runningAnimations) {
908             return insets;
909         }
910 
911         @Override
onEnd(WindowInsetsAnimation animation)912         public void onEnd(WindowInsetsAnimation animation) {
913             synchronized (this) {
914                 mFinished = true;
915                 notify();
916             }
917         }
918 
waitForFinishing()919         void waitForFinishing() throws InterruptedException {
920             synchronized (this) {
921                 if (!mFinished) {
922                     wait(ANIMATION_TIMEOUT);
923                 }
924             }
925         }
926 
reset()927         void reset() {
928             synchronized (this) {
929                 mFinished = false;
930             }
931         }
932     }
933 
setViews(Activity activity, @Nullable String privateImeOptions)934     private static View setViews(Activity activity, @Nullable String privateImeOptions) {
935         LinearLayout layout = new LinearLayout(activity);
936         View text = new TextView(activity);
937         EditText editor = new EditText(activity);
938         editor.setPrivateImeOptions(privateImeOptions);
939         layout.addView(text);
940         layout.addView(editor);
941         activity.setContentView(layout);
942         editor.requestFocus();
943         return layout;
944     }
945 
946     public static class TestActivity extends FocusableActivity {
947         final String mEditTextMarker =
948                 getClass().getName() + "/" + SystemClock.elapsedRealtimeNanos();
949 
950         @Override
onCreate(Bundle savedInstanceState)951         protected void onCreate(Bundle savedInstanceState) {
952             super.onCreate(savedInstanceState);
953             setViews(this, mEditTextMarker);
954             getWindow().setSoftInputMode(SOFT_INPUT_STATE_HIDDEN);
955         }
956     }
957 
958     public static class TestHideOnCreateActivity extends FocusableActivity {
959 
960         @Override
onCreate(Bundle savedInstanceState)961         protected void onCreate(Bundle savedInstanceState) {
962             super.onCreate(savedInstanceState);
963             View layout = setViews(this, null /* privateImeOptions */);
964             ANIMATION_CALLBACK.reset();
965             getWindow().getDecorView().setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
966             getWindow().getInsetsController().hide(statusBars());
967             layout.getWindowInsetsController().hide(navigationBars());
968         }
969     }
970 
971     public static class TestShowOnCreateActivity extends FocusableActivity {
972         @Override
onCreate(Bundle savedInstanceState)973         protected void onCreate(Bundle savedInstanceState) {
974             super.onCreate(savedInstanceState);
975             setViews(this, null /* privateImeOptions */);
976             ANIMATION_CALLBACK.reset();
977             getWindow().getDecorView().setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
978             getWindow().getInsetsController().show(ime());
979         }
980 
showAltImDialog()981         void showAltImDialog() {
982             AlertDialog dialog = new AlertDialog.Builder(this)
983                     .setTitle("TestDialog")
984                     .create();
985             dialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
986             dialog.show();
987         }
988     }
989 
getOnMainSync(Supplier<R> f)990     private <R> R getOnMainSync(Supplier<R> f) {
991         final Object[] result = new Object[1];
992         getInstrumentation().runOnMainSync(() -> result[0] = f.get());
993         //noinspection unchecked
994         return (R) result[0];
995     }
996 }
997