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