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