| /* |
| * 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 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.InstrumentationRegistry; |
| import androidx.test.filters.FlakyTest; |
| import androidx.test.filters.SmallTest; |
| import androidx.test.rule.ActivityTestRule; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| 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 org.junit.runner.RunWith; |
| |
| import java.util.function.Supplier; |
| |
| @RunWith(AndroidJUnit4.class) |
| @SmallTest |
| @Presubmit |
| @FlakyTest(detail = "until proven non-flaky") |
| public class LocationOnScreenTests { |
| |
| @Rule |
| public final ErrorCollector mErrorCollector = new ErrorCollector(); |
| |
| @Rule |
| public final ActivityTestRule<TestActivity> mDisplayCutoutActivity = |
| new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */, |
| false /* launchActivity */); |
| |
| private LayoutParams mLayoutParams; |
| private Context mContext; |
| |
| @Before |
| public void setUp() { |
| mContext = InstrumentationRegistry.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 <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) { |
| mErrorCollector.checkThat(reason, actual, matcher); |
| } |
| |
| private <R> R getOnMainSync(Supplier<R> f) { |
| final Object[] result = new Object[1]; |
| runOnMainSync(() -> result[0] = f.get()); |
| //noinspection unchecked |
| return (R) result[0]; |
| } |
| |
| private void runOnMainSync(Runnable runnable) { |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable); |
| } |
| |
| private <T extends Activity> T launchAndWait(ActivityTestRule<T> 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 = |
| InstrumentationRegistry.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); |
| } |
| } |
| } |