/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.server.wm; import static android.server.wm.LocationOnScreenTests.TestActivity.COLOR_TOLERANCE; import static android.server.wm.LocationOnScreenTests.TestActivity.EXTRA_LAYOUT_PARAMS; import static android.server.wm.LocationOnScreenTests.TestActivity.TEST_COLOR_1; import static android.server.wm.LocationOnScreenTests.TestActivity.TEST_COLOR_2; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; import static androidx.test.InstrumentationRegistry.getInstrumentation; import static org.hamcrest.Matchers.is; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Point; import android.os.Bundle; import android.platform.test.annotations.Presubmit; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager.LayoutParams; import android.widget.FrameLayout; import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; import androidx.test.rule.ActivityTestRule; import com.android.compatibility.common.util.BitmapUtils; import com.android.compatibility.common.util.PollingCheck; import org.hamcrest.Matcher; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ErrorCollector; import java.util.function.Supplier; @SmallTest @Presubmit public class LocationOnScreenTests { @Rule public final ErrorCollector mErrorCollector = new ErrorCollector(); @Rule public final ActivityTestRule mDisplayCutoutActivity = new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */, false /* launchActivity */); private LayoutParams mLayoutParams; private Context mContext; @Before public void setUp() { mContext = getInstrumentation().getContext(); mLayoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT, LayoutParams.TYPE_APPLICATION, LayoutParams.FLAG_LAYOUT_IN_SCREEN | LayoutParams.FLAG_LAYOUT_INSET_DECOR, PixelFormat.TRANSLUCENT); } @Test public void testLocationOnDisplay_appWindow() { runTest(mLayoutParams); } @Test public void testLocationOnDisplay_appWindow_fullscreen() { mLayoutParams.flags |= LayoutParams.FLAG_FULLSCREEN; runTest(mLayoutParams); } @Test public void testLocationOnDisplay_floatingWindow() { mLayoutParams.height = 50; mLayoutParams.width = 50; mLayoutParams.gravity = Gravity.CENTER; mLayoutParams.flags &= ~(FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR); runTest(mLayoutParams); } @Test public void testLocationOnDisplay_appWindow_displayCutoutNever() { mLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; runTest(mLayoutParams); } @Test public void testLocationOnDisplay_appWindow_displayCutoutShortEdges() { mLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; runTest(mLayoutParams); } private void runTest(LayoutParams lp) { final TestActivity activity = launchAndWait(mDisplayCutoutActivity, lp); PollingCheck.waitFor(() -> getOnMainSync(activity::isEnterAnimationComplete)); Point actual = getOnMainSync(activity::getViewLocationOnScreen); Point expected = findTestColorsInScreenshot(actual); assertThat("View.locationOnScreen returned incorrect value", actual, is(expected)); } private void assertThat(String reason, T actual, Matcher matcher) { mErrorCollector.checkThat(reason, actual, matcher); } private R getOnMainSync(Supplier f) { final Object[] result = new Object[1]; runOnMainSync(() -> result[0] = f.get()); //noinspection unchecked return (R) result[0]; } private void runOnMainSync(Runnable runnable) { getInstrumentation().runOnMainSync(runnable); } private T launchAndWait(ActivityTestRule rule, LayoutParams lp) { final T activity = rule.launchActivity( new Intent().putExtra(EXTRA_LAYOUT_PARAMS, lp)); PollingCheck.waitFor(activity::hasWindowFocus); return activity; } private Point findTestColorsInScreenshot(Point guess) { final Bitmap screenshot = getInstrumentation().getUiAutomation().takeScreenshot(); // We have a good guess from locationOnScreen - check there first to avoid having to go over // the entire bitmap. Also increases robustness in the extremely unlikely case that those // colors are visible elsewhere. if (isTestColors(screenshot, guess.x, guess.y)) { return guess; } for (int y = 0; y < screenshot.getHeight(); y++) { for (int x = 0; x < screenshot.getWidth() - 1; x++) { if (isTestColors(screenshot, x, y)) { return new Point(x, y); } } } String path = mContext.getExternalFilesDir(null).getPath(); String file = "location_on_screen_failure.png"; BitmapUtils.saveBitmap(screenshot, path, file); Assert.fail("No match found for TEST_COLOR_1 and TEST_COLOR_2 pixels. Check " + path + "/" + file); return null; } private boolean isTestColors(Bitmap screenshot, int x, int y) { return sameColorWithinTolerance(screenshot.getPixel(x, y), TEST_COLOR_1) && sameColorWithinTolerance(screenshot.getPixel(x + 1, y), TEST_COLOR_2); } /** * Returns whether two colors are considered the same. * * Some tolerance is allowed to compensate for errors introduced when screenshots are scaled. */ private static boolean sameColorWithinTolerance(int pixelColor, int testColor) { final Color pColor = Color.valueOf(pixelColor); final Color tColor = Color.valueOf(testColor); return pColor.alpha() == tColor.alpha() && Math.abs(pColor.red() - tColor.red()) <= COLOR_TOLERANCE && Math.abs(pColor.blue() - tColor.blue()) <= COLOR_TOLERANCE && Math.abs(pColor.green() - tColor.green()) <= COLOR_TOLERANCE; } public static class TestActivity extends Activity { static final int TEST_COLOR_1 = 0xff123456; static final int TEST_COLOR_2 = 0xfffedcba; static final int COLOR_TOLERANCE = 4; static final String EXTRA_LAYOUT_PARAMS = "extra.layout_params"; private View mView; private boolean mEnterAnimationComplete; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().requestFeature(Window.FEATURE_NO_TITLE); FrameLayout frame = new FrameLayout(this); frame.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); setContentView(frame); mView = new TestView(this); frame.addView(mView, new FrameLayout.LayoutParams(2, 1, Gravity.CENTER)); if (getIntent() != null && getIntent().getParcelableExtra(EXTRA_LAYOUT_PARAMS) != null) { getWindow().setAttributes(getIntent().getParcelableExtra(EXTRA_LAYOUT_PARAMS)); } } public Point getViewLocationOnScreen() { final int[] location = new int[2]; mView.getLocationOnScreen(location); return new Point(location[0], location[1]); } public boolean isEnterAnimationComplete() { return mEnterAnimationComplete; } @Override public void onEnterAnimationComplete() { super.onEnterAnimationComplete(); mEnterAnimationComplete = true; } } private static class TestView extends View { private Paint mPaint = new Paint(); public TestView(Context context) { super(context); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint.setColor(TEST_COLOR_1); canvas.drawRect(0, 0, 1, 1, mPaint); mPaint.setColor(TEST_COLOR_2); canvas.drawRect(1, 0, 2, 1, mPaint); } } }