• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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             activity.addWindow(view, new LayoutParams());
159         });
160 
161         // Wait for the possible failure.
162         synchronized (activity) {
163             activity.wait(TIMEOUT_SYSTEM_UI_VISIBILITY_CHANGE);
164         }
165 
166         // Test if flags got cleared.
167         assertFalse("System UI flags must not be cleared.", systemUiFlagsGotCleared[0]);
168     }
169 
170     @Test
testChangingFocusableFlag()171     public void testChangingFocusableFlag() throws Exception {
172         final View[] view = new View[1];
173         final LayoutParams attrs = new LayoutParams(TYPE_APPLICATION_PANEL, FLAG_NOT_FOCUSABLE);
174         final boolean[] childWindowHasFocus = { false };
175         final boolean[] childWindowGotKeyEvent = { false };
176         final TestActivity activity = startActivity(TestActivity.class);
177 
178         // Add a not-focusable window.
179         getInstrumentation().runOnMainSync(() -> {
180             view[0] = new View(activity) {
181                 public void onWindowFocusChanged(boolean hasWindowFocus) {
182                     super.onWindowFocusChanged(hasWindowFocus);
183                     childWindowHasFocus[0] = hasWindowFocus;
184                     synchronized (activity) {
185                         activity.notify();
186                     }
187                 }
188 
189                 public boolean onKeyDown(int keyCode, KeyEvent event) {
190                     synchronized (activity) {
191                         childWindowGotKeyEvent[0] = true;
192                     }
193                     return super.onKeyDown(keyCode, event);
194                 }
195             };
196             activity.addWindow(view[0], attrs);
197         });
198         getInstrumentation().waitForIdleSync();
199 
200         // Make the window focusable.
201         getInstrumentation().runOnMainSync(() -> {
202             attrs.flags &= ~FLAG_NOT_FOCUSABLE;
203             activity.getWindowManager().updateViewLayout(view[0], attrs);
204         });
205         synchronized (activity) {
206             activity.wait(TIMEOUT_WINDOW_FOCUS_CHANGED);
207         }
208 
209         // The window must have focus.
210         assertTrue("Child window must have focus.", childWindowHasFocus[0]);
211 
212         // Ensure the window can receive keys.
213         PollingCheck.check("Child window must get key event.", TIMEOUT_RECEIVE_KEY, () -> {
214             getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_0);
215             synchronized (activity) {
216                 return childWindowGotKeyEvent[0];
217             }
218         });
219     }
220 
221     @Test
testSysuiFlagLayoutFullscreen()222     public void testSysuiFlagLayoutFullscreen() {
223         final TestActivity activity = startActivity(TestActivity.class);
224 
225         final View[] views = new View[2];
226         getInstrumentation().runOnMainSync(() -> {
227             views[0] = new View(activity);
228             final LayoutParams attrs = new LayoutParams();
229             attrs.setFitInsetsTypes(attrs.getFitInsetsTypes() & ~Type.statusBars());
230             activity.addWindow(views[0], attrs);
231 
232             views[1] = new View(activity);
233             views[1].setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
234             activity.addWindow(views[1], new LayoutParams());
235         });
236         getInstrumentation().waitForIdleSync();
237 
238         assertLayoutEquals(views[0], views[1]);
239     }
240 
241     @Test
testSysuiFlagLayoutHideNavigation()242     public void testSysuiFlagLayoutHideNavigation() {
243         final TestActivity activity = startActivity(TestActivity.class);
244 
245         final View[] views = new View[2];
246         getInstrumentation().runOnMainSync(() -> {
247             views[0] = new View(activity);
248             final LayoutParams attrs = new LayoutParams();
249             attrs.setFitInsetsTypes(attrs.getFitInsetsTypes() & ~Type.systemBars());
250             activity.addWindow(views[0], attrs);
251 
252             views[1] = new View(activity);
253             views[1].setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
254             activity.addWindow(views[1], new LayoutParams());
255         });
256         getInstrumentation().waitForIdleSync();
257 
258         assertLayoutEquals(views[0], views[1]);
259     }
260 
assertLayoutEquals(View view1, View view2)261     private static void assertLayoutEquals(View view1, View view2) {
262         final int[][] locations = new int[2][2];
263         view1.getLocationOnScreen(locations[0]);
264         view2.getLocationOnScreen(locations[1]);
265         assertArrayEquals("Location must be the same.", locations[0], locations[1]);
266         assertEquals("Width must be the same.", view1.getWidth(), view2.getWidth());
267         assertEquals("Height must be the same.", view1.getHeight(), view2.getHeight());
268     }
269 
270     public static class TestActivity extends FocusableActivity {
271         private static final long TIMEOUT_LAYOUT = 200; // milliseconds
272 
273         private final Object mLockGlobalLayout = new Object();
274         private ArrayList<View> mViews = new ArrayList<>();
275 
276         @Override
onCreate(Bundle savedInstanceState)277         protected void onCreate(Bundle savedInstanceState) {
278             super.onCreate(savedInstanceState);
279             getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(() -> {
280                 synchronized (mLockGlobalLayout) {
281                     mLockGlobalLayout.notify();
282                 }
283             });
284         }
285 
waitForGlobalLayout()286         void waitForGlobalLayout() throws InterruptedException {
287             synchronized (mLockGlobalLayout) {
288                 mLockGlobalLayout.wait(TIMEOUT_LAYOUT);
289             }
290         }
291 
addWindow(View view, LayoutParams attrs)292         void addWindow(View view, LayoutParams attrs) {
293             getWindowManager().addView(view, attrs);
294             mViews.add(view);
295         }
296 
removeAllWindows()297         void removeAllWindows() {
298             for (View view : mViews) {
299                 getWindowManager().removeViewImmediate(view);
300             }
301             mViews.clear();
302         }
303 
304         @Override
onPause()305         protected void onPause() {
306             super.onPause();
307             removeAllWindows();
308         }
309     }
310 }
311