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