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