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