1 /* 2 * Copyright (C) 2020 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.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; 20 import static android.view.WindowInsets.Type.displayCutout; 21 import static android.view.WindowInsets.Type.navigationBars; 22 import static android.view.WindowInsets.Type.statusBars; 23 24 import static org.junit.Assert.assertEquals; 25 import static org.junit.Assert.assertTrue; 26 27 import android.app.Activity; 28 import android.content.Context; 29 import android.graphics.Insets; 30 import android.graphics.Point; 31 import android.graphics.Rect; 32 import android.util.DisplayMetrics; 33 import android.view.Display; 34 import android.view.View; 35 import android.view.WindowInsets; 36 import android.view.WindowManager; 37 import android.view.WindowMetrics; 38 39 import java.util.concurrent.CountDownLatch; 40 import java.util.concurrent.TimeUnit; 41 42 /** Helper class to test {@link WindowMetrics} behaviors. */ 43 public class WindowMetricsTestHelper { assertMetricsMatchesLayout(WindowMetrics currentMetrics, WindowMetrics maxMetrics, Rect layoutBounds, WindowInsets layoutInsets)44 public static void assertMetricsMatchesLayout(WindowMetrics currentMetrics, 45 WindowMetrics maxMetrics, Rect layoutBounds, WindowInsets layoutInsets) { 46 assertMetricsMatchesLayout(currentMetrics, maxMetrics, layoutBounds, layoutInsets, 47 false /* inMultiWindowMode */); 48 } 49 assertMetricsMatchesLayout(WindowMetrics currentMetrics, WindowMetrics maxMetrics, Rect layoutBounds, WindowInsets layoutInsets, boolean inMultiWindowMode)50 public static void assertMetricsMatchesLayout(WindowMetrics currentMetrics, 51 WindowMetrics maxMetrics, Rect layoutBounds, WindowInsets layoutInsets, 52 boolean inMultiWindowMode) { 53 // Only validate the size portion of the bounds, regardless of the position on the screen to 54 // take into consideration multiple screen devices (e.g. the dialog is on another screen) 55 final Rect currentMetricsBounds = currentMetrics.getBounds(); 56 assertEquals(layoutBounds.height(), currentMetricsBounds.height()); 57 assertEquals(layoutBounds.width(), currentMetricsBounds.width()); 58 // Don't verify maxMetrics or insets for an Activity that is in multi-window mode. 59 // This is because: 60 // - Multi-window mode activities doesn't guarantee max window metrics bounds is larger than 61 // the current window metrics bounds. The bounds of a multi-window mode activity is 62 // unlimited except that it must be contained in display bounds. 63 // - WindowMetrics always reports all possible insets, whereas LayoutInsets may not report 64 // insets in multi-window mode. This means that when the Activity is in multi-window mode 65 // (*not* fullscreen), the two insets can in fact be different. 66 if (inMultiWindowMode) { 67 return; 68 } 69 assertTrue(maxMetrics.getBounds().width() 70 >= currentMetrics.getBounds().width()); 71 assertTrue(maxMetrics.getBounds().height() 72 >= currentMetrics.getBounds().height()); 73 final int insetsType = statusBars() | navigationBars() | displayCutout(); 74 assertEquals(layoutInsets.getInsets(insetsType), 75 currentMetrics.getWindowInsets().getInsets(insetsType)); 76 assertEquals(layoutInsets.getDisplayCutout(), 77 currentMetrics.getWindowInsets().getDisplayCutout()); 78 } 79 80 /** 81 * Verifies two scenarios for a {@link Context}. 82 * <ul> 83 * <li>{@link WindowManager#getCurrentWindowMetrics()} matches 84 * {@link Display#getSize(Point)}</li> 85 * <li>{@link WindowManager#getMaximumWindowMetrics()} and {@link Display#getSize(Point)} 86 * either matches DisplayArea bounds which the {@link Context} is attached to, or matches 87 * {@link WindowManager#getCurrentWindowMetrics()} if sandboxing is applied.</li> 88 * </ul> 89 * @param context the context under test 90 * @param displayAreaBounds the bounds of the DisplayArea 91 */ assertMetricsValidity(Context context, Rect displayAreaBounds)92 public static void assertMetricsValidity(Context context, Rect displayAreaBounds) { 93 final boolean isFreeForm = context instanceof Activity 94 && context.getResources().getConfiguration().windowConfiguration 95 .getWindowingMode() == WINDOWING_MODE_FREEFORM; 96 final WindowManager windowManager = context.getSystemService(WindowManager.class); 97 final WindowMetrics currentMetrics = windowManager.getCurrentWindowMetrics(); 98 final WindowMetrics maxMetrics = windowManager.getMaximumWindowMetrics(); 99 final Rect maxBounds = maxMetrics.getBounds(); 100 final Display display = context.getDisplay(); 101 assertMetricsMatchDisplay(maxMetrics, currentMetrics, display, isFreeForm); 102 103 // Max window bounds should match either DisplayArea bounds, or current window bounds. 104 if (maxWindowBoundsSandboxed(displayAreaBounds, maxBounds)) { 105 final Rect currentBounds = isFreeForm ? currentMetrics.getBounds() 106 : getBoundsExcludingNavigationBarAndCutout(currentMetrics); 107 // Max window bounds are sandboxed, so max window bounds and real display size 108 // should match current window bounds. 109 assertEquals("Max window size matches current window size, due to sandboxing", 110 currentBounds, maxBounds); 111 } else { 112 // Max window bounds are not sandboxed, so max window bounds and real display size 113 // should match display area bounds. 114 assertEquals("Display area bounds must match max window size", 115 displayAreaBounds, maxBounds); 116 } 117 } 118 119 /** 120 * Verifies for the provided {@link WindowMetrics} and {@link Display}: 121 * <ul> 122 * <li>Bounds and density from {@link WindowManager#getCurrentWindowMetrics()} matches 123 * {@link Display#getMetrics(DisplayMetrics)}</li> 124 * <li>Bounds and density from {@link WindowManager#getMaximumWindowMetrics()} matches 125 * {@link Display#getRealMetrics(DisplayMetrics)} 126 * 127 * </ul> 128 * @param maxMetrics the {@link WindowMetrics} from 129 * {@link WindowManager#getMaximumWindowMetrics()} 130 * @param currentMetrics the {@link WindowMetrics} from 131 * {@link WindowManager#getCurrentWindowMetrics()} 132 * @param display the display to compare bounds against 133 * @param shouldBoundsIncludeInsets whether the bounds to be verified should include insets 134 */ assertMetricsMatchDisplay(WindowMetrics maxMetrics, WindowMetrics currentMetrics, Display display, boolean shouldBoundsIncludeInsets)135 static void assertMetricsMatchDisplay(WindowMetrics maxMetrics, WindowMetrics currentMetrics, 136 Display display, boolean shouldBoundsIncludeInsets) { 137 // Check window bounds 138 final DisplayMetrics displayMetrics = new DisplayMetrics(); 139 display.getMetrics(displayMetrics); 140 final Rect currentBounds = shouldBoundsIncludeInsets ? currentMetrics.getBounds() 141 : getBoundsExcludingNavigationBarAndCutout(currentMetrics); 142 assertEquals("Reported display width must match window width", 143 displayMetrics.widthPixels, currentBounds.width()); 144 assertEquals("Reported display height must match window height", 145 displayMetrics.heightPixels, currentBounds.height()); 146 assertEquals("Reported display density must match density from window metrics", 147 displayMetrics.density, currentMetrics.getDensity(), 0.0f); 148 149 // Max window bounds should match real display size. 150 final DisplayMetrics realDisplayMetrics = new DisplayMetrics(); 151 display.getRealMetrics(realDisplayMetrics); 152 final Rect maxBounds = maxMetrics.getBounds(); 153 assertEquals("Reported real display width must match max window width", 154 realDisplayMetrics.widthPixels, maxBounds.width()); 155 assertEquals("Reported real display height must match max window height", 156 realDisplayMetrics.heightPixels, maxBounds.height()); 157 assertEquals("Reported real display density must match density from window metrics", 158 realDisplayMetrics.density, currentMetrics.getDensity(), 0.0f); 159 } 160 getBoundsExcludingNavigationBarAndCutout(WindowMetrics windowMetrics)161 public static Rect getBoundsExcludingNavigationBarAndCutout(WindowMetrics windowMetrics) { 162 WindowInsets windowInsets = windowMetrics.getWindowInsets(); 163 final Insets insetsWithCutout = 164 windowInsets.getInsetsIgnoringVisibility(navigationBars() | displayCutout()); 165 166 final Rect bounds = windowMetrics.getBounds(); 167 return inset(bounds, insetsWithCutout); 168 } 169 170 /** 171 * Returns {@code true} if the bounds from {@link WindowManager#getMaximumWindowMetrics()} are 172 * sandboxed, so are smaller than the DisplayArea. 173 */ maxWindowBoundsSandboxed(Rect displayAreaBounds, Rect maxBounds)174 static boolean maxWindowBoundsSandboxed(Rect displayAreaBounds, Rect maxBounds) { 175 return maxBounds.width() < displayAreaBounds.width() 176 || maxBounds.height() < displayAreaBounds.height(); 177 } 178 inset(Rect original, Insets insets)179 private static Rect inset(Rect original, Insets insets) { 180 final int left = original.left + insets.left; 181 final int top = original.top + insets.top; 182 final int right = original.right - insets.right; 183 final int bottom = original.bottom - insets.bottom; 184 return new Rect(left, top, right, bottom); 185 } 186 187 public static class OnLayoutChangeListener implements View.OnLayoutChangeListener { 188 private final CountDownLatch mLayoutLatch = new CountDownLatch(1); 189 190 private volatile Rect mOnLayoutBoundsInScreen; 191 private volatile WindowInsets mOnLayoutInsets; 192 193 @Override onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)194 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 195 int oldTop, int oldRight, int oldBottom) { 196 synchronized (this) { 197 mOnLayoutBoundsInScreen = new Rect(left, top, right, bottom); 198 // Convert decorView's bounds from window coordinates to screen coordinates. 199 final int[] locationOnScreen = new int[2]; 200 v.getLocationOnScreen(locationOnScreen); 201 mOnLayoutBoundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); 202 203 mOnLayoutInsets = v.getRootWindowInsets(); 204 mLayoutLatch.countDown(); 205 } 206 } 207 getLayoutBounds()208 public Rect getLayoutBounds() { 209 synchronized (this) { 210 return mOnLayoutBoundsInScreen; 211 } 212 } 213 getLayoutInsets()214 public WindowInsets getLayoutInsets() { 215 synchronized (this) { 216 return mOnLayoutInsets; 217 } 218 } 219 waitForLayout()220 void waitForLayout() { 221 try { 222 assertTrue("Timed out waiting for layout.", 223 mLayoutLatch.await(4, TimeUnit.SECONDS)); 224 } catch (InterruptedException e) { 225 throw new AssertionError(e); 226 } 227 } 228 } 229 } 230