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