1 /*
2  * Copyright (C) 2018 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.provider.Settings.Global.WINDOW_ANIMATION_SCALE;
20 import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
21 import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
22 import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
23 import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
24 import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
25 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
26 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
27 
28 import static androidx.test.InstrumentationRegistry.getInstrumentation;
29 
30 import static org.junit.Assert.assertArrayEquals;
31 import static org.junit.Assert.assertEquals;
32 import static org.junit.Assert.assertFalse;
33 import static org.junit.Assert.assertTrue;
34 
35 import android.content.ContentResolver;
36 import android.graphics.Rect;
37 import android.os.Bundle;
38 import android.platform.test.annotations.AppModeFull;
39 import android.platform.test.annotations.Presubmit;
40 import android.provider.Settings;
41 import android.view.KeyEvent;
42 import android.view.View;
43 import android.view.WindowInsets.Type;
44 import android.view.WindowManager.LayoutParams;
45 
46 import com.android.compatibility.common.util.PollingCheck;
47 import com.android.compatibility.common.util.SystemUtil;
48 
49 import org.junit.After;
50 import org.junit.Before;
51 import org.junit.Test;
52 
53 import java.util.ArrayList;
54 
55 /**
56  * Test whether WindowManager performs the correct layout after we make some changes to it.
57  *
58  * Build/Install/Run:
59  *     atest CtsWindowManagerDeviceTestCases:LayoutTests
60  */
61 @AppModeFull(reason = "Cannot write global settings as an instant app.")
62 @Presubmit
63 public class LayoutTests extends WindowManagerTestBase {
64     private static final long TIMEOUT_RECEIVE_KEY = 100; // milliseconds
65     private static final long TIMEOUT_SYSTEM_UI_VISIBILITY_CHANGE = 1000;
66     private static final int SYSTEM_UI_FLAG_HIDE_ALL = SYSTEM_UI_FLAG_FULLSCREEN
67             | SYSTEM_UI_FLAG_HIDE_NAVIGATION;
68 
69     private float mWindowAnimationScale;
70 
71     @Before
setup()72     public void setup() {
73         SystemUtil.runWithShellPermissionIdentity(() -> {
74             // The layout will be performed at the end of the animation of hiding status/navigation
75             // bar, which will recover the possible issue, so we disable the animation during the
76             // test.
77             final ContentResolver resolver = getInstrumentation().getContext().getContentResolver();
78             mWindowAnimationScale = Settings.Global.getFloat(resolver, WINDOW_ANIMATION_SCALE, 1f);
79             Settings.Global.putFloat(resolver, WINDOW_ANIMATION_SCALE, 0);
80         });
81     }
82 
83     @After
tearDown()84     public void tearDown() {
85         SystemUtil.runWithShellPermissionIdentity(() -> {
86             // Restore the animation we disabled previously.
87             Settings.Global.putFloat(getInstrumentation().getContext().getContentResolver(),
88                     WINDOW_ANIMATION_SCALE, mWindowAnimationScale);
89         });
90     }
91 
92     @Test
testLayoutAfterRemovingFocus()93     public void testLayoutAfterRemovingFocus() throws InterruptedException {
94         final TestActivity activity = startActivity(TestActivity.class);
95 
96         // Get the visible frame of the main activity before adding any window.
97         final Rect visibleFrame = new Rect();
98         getInstrumentation().runOnMainSync(() ->
99                 activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(visibleFrame));
100         assertFalse("Visible frame must not be empty.", visibleFrame.isEmpty());
101 
102         doTestLayoutAfterRemovingFocus(activity, visibleFrame, SYSTEM_UI_FLAG_FULLSCREEN);
103         doTestLayoutAfterRemovingFocus(activity, visibleFrame, SYSTEM_UI_FLAG_HIDE_NAVIGATION);
104         doTestLayoutAfterRemovingFocus(activity, visibleFrame, SYSTEM_UI_FLAG_HIDE_ALL);
105     }
106 
doTestLayoutAfterRemovingFocus(TestActivity activity, Rect visibleFrameBeforeAddingWindow, int systemUiFlags)107     private void doTestLayoutAfterRemovingFocus(TestActivity activity,
108             Rect visibleFrameBeforeAddingWindow, int systemUiFlags) throws InterruptedException {
109         // Add a window which can affect the global layout.
110         getInstrumentation().runOnMainSync(() -> {
111             final View view = new View(activity);
112             view.setSystemUiVisibility(systemUiFlags);
113             activity.addWindow(view, new LayoutParams());
114         });
115 
116         // Wait for the global layout triggered by adding window.
117         activity.waitForGlobalLayout();
118 
119         // Remove the window we added previously.
120         getInstrumentation().runOnMainSync(activity::removeAllWindows);
121 
122         // Wait for the global layout triggered by removing window.
123         activity.waitForGlobalLayout();
124 
125         // Wait for the activity has focus before get the visible frame
126         activity.waitAndAssertWindowFocusState(true);
127 
128         // Get the visible frame of the main activity after removing the window we added.
129         final Rect visibleFrameAfterRemovingWindow = new Rect();
130         getInstrumentation().runOnMainSync(() ->
131                 activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(
132                         visibleFrameAfterRemovingWindow));
133 
134         // Test whether the visible frame after removing window is the same as one before adding
135         // window. If not, it shows that the layout after removing window has a problem.
136         assertEquals(visibleFrameBeforeAddingWindow, visibleFrameAfterRemovingWindow);
137     }
138 
139     @Test
testAddingImmersiveWindow()140     public void testAddingImmersiveWindow() throws InterruptedException {
141         final boolean[] systemUiFlagsGotCleared = { false };
142         final TestActivity activity = startActivity(TestActivity.class);
143 
144         // Add a window which has clearable system UI flags.
145         getInstrumentation().runOnMainSync(() -> {
146             final View view = new View(activity);
147             view.setSystemUiVisibility(SYSTEM_UI_FLAG_IMMERSIVE_STICKY | SYSTEM_UI_FLAG_HIDE_ALL);
148             view.setOnSystemUiVisibilityChangeListener(
149                     visibility -> {
150                         if ((visibility & SYSTEM_UI_FLAG_HIDE_ALL) != SYSTEM_UI_FLAG_HIDE_ALL) {
151                             systemUiFlagsGotCleared[0] = true;
152                             // Early break because things go wrong already.
153                             synchronized (activity) {
154                                 activity.notify();
155                             }
156                         }
157                     });
158             // Use a sub window type so the test is robust when remote inset controller is used.
159             activity.addWindow(view, new LayoutParams(TYPE_APPLICATION_PANEL));
160         });
161 
162         // Wait for the possible failure.
163         synchronized (activity) {
164             activity.wait(TIMEOUT_SYSTEM_UI_VISIBILITY_CHANGE);
165         }
166 
167         // Test if flags got cleared.
168         assertFalse("System UI flags must not be cleared.", systemUiFlagsGotCleared[0]);
169     }
170 
171     @Test
testChangingFocusableFlag()172     public void testChangingFocusableFlag() throws Exception {
173         final View[] view = new View[1];
174         final LayoutParams attrs = new LayoutParams(TYPE_APPLICATION_PANEL, FLAG_NOT_FOCUSABLE);
175         final boolean[] childWindowHasFocus = { false };
176         final boolean[] childWindowGotKeyEvent = { false };
177         final TestActivity activity = startActivity(TestActivity.class);
178 
179         // Add a not-focusable window.
180         getInstrumentation().runOnMainSync(() -> {
181             view[0] = new View(activity) {
182                 public void onWindowFocusChanged(boolean hasWindowFocus) {
183                     super.onWindowFocusChanged(hasWindowFocus);
184                     childWindowHasFocus[0] = hasWindowFocus;
185                     synchronized (activity) {
186                         activity.notify();
187                     }
188                 }
189 
190                 public boolean onKeyDown(int keyCode, KeyEvent event) {
191                     synchronized (activity) {
192                         childWindowGotKeyEvent[0] = true;
193                     }
194                     return super.onKeyDown(keyCode, event);
195                 }
196             };
197             activity.addWindow(view[0], attrs);
198         });
199         getInstrumentation().waitForIdleSync();
200 
201         // Make the window focusable.
202         getInstrumentation().runOnMainSync(() -> {
203             attrs.flags &= ~FLAG_NOT_FOCUSABLE;
204             activity.getWindowManager().updateViewLayout(view[0], attrs);
205         });
206         synchronized (activity) {
207             activity.wait(TIMEOUT_WINDOW_FOCUS_CHANGED);
208         }
209 
210         // The window must have focus.
211         assertTrue("Child window must have focus.", childWindowHasFocus[0]);
212 
213         // Ensure the window can receive keys.
214         PollingCheck.check("Child window must get key event.", TIMEOUT_RECEIVE_KEY, () -> {
215             getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_0);
216             synchronized (activity) {
217                 return childWindowGotKeyEvent[0];
218             }
219         });
220     }
221 
222     @Test
testSysuiFlagLayoutFullscreen()223     public void testSysuiFlagLayoutFullscreen() {
224         final TestActivity activity = startActivity(TestActivity.class);
225 
226         final View[] views = new View[2];
227         getInstrumentation().runOnMainSync(() -> {
228             views[0] = new View(activity);
229             final LayoutParams attrs = new LayoutParams();
230             attrs.setFitInsetsTypes(attrs.getFitInsetsTypes() & ~Type.statusBars());
231             activity.addWindow(views[0], attrs);
232 
233             views[1] = new View(activity);
234             views[1].setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
235             activity.addWindow(views[1], new LayoutParams());
236         });
237         getInstrumentation().waitForIdleSync();
238 
239         assertLayoutEquals(views[0], views[1]);
240     }
241 
242     @Test
testSysuiFlagLayoutHideNavigation()243     public void testSysuiFlagLayoutHideNavigation() {
244         final TestActivity activity = startActivity(TestActivity.class);
245 
246         final View[] views = new View[2];
247         getInstrumentation().runOnMainSync(() -> {
248             views[0] = new View(activity);
249             final LayoutParams attrs = new LayoutParams();
250             attrs.setFitInsetsTypes(attrs.getFitInsetsTypes() & ~Type.systemBars());
251             activity.addWindow(views[0], attrs);
252 
253             views[1] = new View(activity);
254             views[1].setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
255             activity.addWindow(views[1], new LayoutParams());
256         });
257         getInstrumentation().waitForIdleSync();
258 
259         assertLayoutEquals(views[0], views[1]);
260     }
261 
assertLayoutEquals(View view1, View view2)262     private static void assertLayoutEquals(View view1, View view2) {
263         final int[][] locations = new int[2][2];
264         view1.getLocationOnScreen(locations[0]);
265         view2.getLocationOnScreen(locations[1]);
266         assertArrayEquals("Location must be the same.", locations[0], locations[1]);
267         assertEquals("Width must be the same.", view1.getWidth(), view2.getWidth());
268         assertEquals("Height must be the same.", view1.getHeight(), view2.getHeight());
269     }
270 
271     public static class TestActivity extends FocusableActivity {
272         private static final long TIMEOUT_LAYOUT = 200; // milliseconds
273 
274         private final Object mLockGlobalLayout = new Object();
275         private ArrayList<View> mViews = new ArrayList<>();
276 
277         @Override
onCreate(Bundle savedInstanceState)278         protected void onCreate(Bundle savedInstanceState) {
279             super.onCreate(savedInstanceState);
280             getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(() -> {
281                 synchronized (mLockGlobalLayout) {
282                     mLockGlobalLayout.notify();
283                 }
284             });
285         }
286 
waitForGlobalLayout()287         void waitForGlobalLayout() throws InterruptedException {
288             synchronized (mLockGlobalLayout) {
289                 mLockGlobalLayout.wait(TIMEOUT_LAYOUT);
290             }
291         }
292 
addWindow(View view, LayoutParams attrs)293         void addWindow(View view, LayoutParams attrs) {
294             getWindowManager().addView(view, attrs);
295             mViews.add(view);
296         }
297 
removeAllWindows()298         void removeAllWindows() {
299             for (View view : mViews) {
300                 getWindowManager().removeViewImmediate(view);
301             }
302             mViews.clear();
303         }
304 
305         @Override
onPause()306         protected void onPause() {
307             super.onPause();
308             removeAllWindows();
309         }
310     }
311 }
312