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