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.server.wm.LocationOnScreenTests.TestActivity.COLOR_TOLERANCE; 20 import static android.server.wm.LocationOnScreenTests.TestActivity.EXTRA_LAYOUT_PARAMS; 21 import static android.server.wm.LocationOnScreenTests.TestActivity.TEST_COLOR_1; 22 import static android.server.wm.LocationOnScreenTests.TestActivity.TEST_COLOR_2; 23 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 24 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; 25 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; 26 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; 27 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; 28 29 import static androidx.test.InstrumentationRegistry.getInstrumentation; 30 31 import static org.hamcrest.Matchers.is; 32 33 import android.app.Activity; 34 import android.content.Context; 35 import android.content.Intent; 36 import android.graphics.Bitmap; 37 import android.graphics.Canvas; 38 import android.graphics.Color; 39 import android.graphics.Paint; 40 import android.graphics.PixelFormat; 41 import android.graphics.Point; 42 import android.os.Bundle; 43 import android.platform.test.annotations.Presubmit; 44 import android.view.Gravity; 45 import android.view.View; 46 import android.view.ViewGroup; 47 import android.view.Window; 48 import android.view.WindowManager.LayoutParams; 49 import android.widget.FrameLayout; 50 51 import androidx.test.filters.FlakyTest; 52 import androidx.test.filters.SmallTest; 53 import androidx.test.rule.ActivityTestRule; 54 55 import com.android.compatibility.common.util.BitmapUtils; 56 import com.android.compatibility.common.util.PollingCheck; 57 58 import org.hamcrest.Matcher; 59 import org.junit.Assert; 60 import org.junit.Before; 61 import org.junit.Rule; 62 import org.junit.Test; 63 import org.junit.rules.ErrorCollector; 64 65 import java.util.function.Supplier; 66 67 @SmallTest 68 @Presubmit 69 public class LocationOnScreenTests { 70 71 @Rule 72 public final ErrorCollector mErrorCollector = new ErrorCollector(); 73 74 @Rule 75 public final ActivityTestRule<TestActivity> mDisplayCutoutActivity = 76 new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */, 77 false /* launchActivity */); 78 79 private LayoutParams mLayoutParams; 80 private Context mContext; 81 82 @Before setUp()83 public void setUp() { 84 mContext = getInstrumentation().getContext(); 85 mLayoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT, LayoutParams.TYPE_APPLICATION, 86 LayoutParams.FLAG_LAYOUT_IN_SCREEN | LayoutParams.FLAG_LAYOUT_INSET_DECOR, 87 PixelFormat.TRANSLUCENT); 88 } 89 90 @Test testLocationOnDisplay_appWindow()91 public void testLocationOnDisplay_appWindow() { 92 runTest(mLayoutParams); 93 } 94 95 @Test testLocationOnDisplay_appWindow_fullscreen()96 public void testLocationOnDisplay_appWindow_fullscreen() { 97 mLayoutParams.flags |= LayoutParams.FLAG_FULLSCREEN; 98 runTest(mLayoutParams); 99 } 100 101 @Test testLocationOnDisplay_floatingWindow()102 public void testLocationOnDisplay_floatingWindow() { 103 mLayoutParams.height = 50; 104 mLayoutParams.width = 50; 105 mLayoutParams.gravity = Gravity.CENTER; 106 mLayoutParams.flags &= ~(FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR); 107 runTest(mLayoutParams); 108 } 109 110 @Test testLocationOnDisplay_appWindow_displayCutoutNever()111 public void testLocationOnDisplay_appWindow_displayCutoutNever() { 112 mLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; 113 runTest(mLayoutParams); 114 } 115 116 @Test testLocationOnDisplay_appWindow_displayCutoutShortEdges()117 public void testLocationOnDisplay_appWindow_displayCutoutShortEdges() { 118 mLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; 119 runTest(mLayoutParams); 120 } 121 runTest(LayoutParams lp)122 private void runTest(LayoutParams lp) { 123 final TestActivity activity = launchAndWait(mDisplayCutoutActivity, lp); 124 PollingCheck.waitFor(() -> getOnMainSync(activity::isEnterAnimationComplete)); 125 126 Point actual = getOnMainSync(activity::getViewLocationOnScreen); 127 Point expected = findTestColorsInScreenshot(actual); 128 129 assertThat("View.locationOnScreen returned incorrect value", actual, is(expected)); 130 } 131 assertThat(String reason, T actual, Matcher<? super T> matcher)132 private <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) { 133 mErrorCollector.checkThat(reason, actual, matcher); 134 } 135 getOnMainSync(Supplier<R> f)136 private <R> R getOnMainSync(Supplier<R> f) { 137 final Object[] result = new Object[1]; 138 runOnMainSync(() -> result[0] = f.get()); 139 //noinspection unchecked 140 return (R) result[0]; 141 } 142 runOnMainSync(Runnable runnable)143 private void runOnMainSync(Runnable runnable) { 144 getInstrumentation().runOnMainSync(runnable); 145 } 146 launchAndWait(ActivityTestRule<T> rule, LayoutParams lp)147 private <T extends Activity> T launchAndWait(ActivityTestRule<T> rule, 148 LayoutParams lp) { 149 final T activity = rule.launchActivity( 150 new Intent().putExtra(EXTRA_LAYOUT_PARAMS, lp)); 151 PollingCheck.waitFor(activity::hasWindowFocus); 152 return activity; 153 } 154 findTestColorsInScreenshot(Point guess)155 private Point findTestColorsInScreenshot(Point guess) { 156 final Bitmap screenshot = getInstrumentation().getUiAutomation().takeScreenshot(); 157 158 // We have a good guess from locationOnScreen - check there first to avoid having to go over 159 // the entire bitmap. Also increases robustness in the extremely unlikely case that those 160 // colors are visible elsewhere. 161 if (isTestColors(screenshot, guess.x, guess.y)) { 162 return guess; 163 } 164 165 for (int y = 0; y < screenshot.getHeight(); y++) { 166 for (int x = 0; x < screenshot.getWidth() - 1; x++) { 167 if (isTestColors(screenshot, x, y)) { 168 return new Point(x, y); 169 } 170 } 171 } 172 String path = mContext.getExternalFilesDir(null).getPath(); 173 String file = "location_on_screen_failure.png"; 174 BitmapUtils.saveBitmap(screenshot, path, file); 175 Assert.fail("No match found for TEST_COLOR_1 and TEST_COLOR_2 pixels. Check " 176 + path + "/" + file); 177 return null; 178 } 179 isTestColors(Bitmap screenshot, int x, int y)180 private boolean isTestColors(Bitmap screenshot, int x, int y) { 181 return sameColorWithinTolerance(screenshot.getPixel(x, y), TEST_COLOR_1) 182 && sameColorWithinTolerance(screenshot.getPixel(x + 1, y), TEST_COLOR_2); 183 } 184 185 /** 186 * Returns whether two colors are considered the same. 187 * 188 * Some tolerance is allowed to compensate for errors introduced when screenshots are scaled. 189 */ sameColorWithinTolerance(int pixelColor, int testColor)190 private static boolean sameColorWithinTolerance(int pixelColor, int testColor) { 191 final Color pColor = Color.valueOf(pixelColor); 192 final Color tColor = Color.valueOf(testColor); 193 return pColor.alpha() == tColor.alpha() 194 && Math.abs(pColor.red() - tColor.red()) <= COLOR_TOLERANCE 195 && Math.abs(pColor.blue() - tColor.blue()) <= COLOR_TOLERANCE 196 && Math.abs(pColor.green() - tColor.green()) <= COLOR_TOLERANCE; 197 } 198 199 public static class TestActivity extends Activity { 200 201 static final int TEST_COLOR_1 = 0xff123456; 202 static final int TEST_COLOR_2 = 0xfffedcba; 203 static final int COLOR_TOLERANCE = 4; 204 static final String EXTRA_LAYOUT_PARAMS = "extra.layout_params"; 205 private View mView; 206 private boolean mEnterAnimationComplete; 207 208 @Override onCreate(Bundle savedInstanceState)209 protected void onCreate(Bundle savedInstanceState) { 210 super.onCreate(savedInstanceState); 211 getWindow().requestFeature(Window.FEATURE_NO_TITLE); 212 213 FrameLayout frame = new FrameLayout(this); 214 frame.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 215 setContentView(frame); 216 217 mView = new TestView(this); 218 frame.addView(mView, new FrameLayout.LayoutParams(2, 1, Gravity.CENTER)); 219 220 if (getIntent() != null 221 && getIntent().getParcelableExtra(EXTRA_LAYOUT_PARAMS) != null) { 222 getWindow().setAttributes(getIntent().getParcelableExtra(EXTRA_LAYOUT_PARAMS)); 223 } 224 } 225 getViewLocationOnScreen()226 public Point getViewLocationOnScreen() { 227 final int[] location = new int[2]; 228 mView.getLocationOnScreen(location); 229 return new Point(location[0], location[1]); 230 } 231 isEnterAnimationComplete()232 public boolean isEnterAnimationComplete() { 233 return mEnterAnimationComplete; 234 } 235 236 @Override onEnterAnimationComplete()237 public void onEnterAnimationComplete() { 238 super.onEnterAnimationComplete(); 239 mEnterAnimationComplete = true; 240 } 241 } 242 243 private static class TestView extends View { 244 private Paint mPaint = new Paint(); 245 TestView(Context context)246 public TestView(Context context) { 247 super(context); 248 } 249 250 @Override onDraw(Canvas canvas)251 protected void onDraw(Canvas canvas) { 252 super.onDraw(canvas); 253 254 mPaint.setColor(TEST_COLOR_1); 255 canvas.drawRect(0, 0, 1, 1, mPaint); 256 mPaint.setColor(TEST_COLOR_2); 257 canvas.drawRect(1, 0, 2, 1, mPaint); 258 } 259 } 260 } 261