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