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