1 /*
2  * Copyright (C) 2019 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.content.pm.PackageManager.FEATURE_AUTOMOTIVE;
20 import static android.server.wm.EnsureBarContrastTest.TestActivity.EXTRA_ENSURE_CONTRAST;
21 import static android.server.wm.EnsureBarContrastTest.TestActivity.EXTRA_LIGHT_BARS;
22 import static android.server.wm.EnsureBarContrastTest.TestActivity.backgroundForBar;
23 import static android.server.wm.BarTestUtils.assumeHasColoredBars;
24 import static android.server.wm.BarTestUtils.assumeHasColoredNavigationBar;
25 import static android.server.wm.BarTestUtils.assumeHasColoredStatusBar;
26 import static android.server.wm.BarTestUtils.isAssumptionViolated;
27 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
28 
29 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
30 import static org.junit.Assume.assumeFalse;
31 
32 import android.app.Activity;
33 import android.content.Intent;
34 import android.graphics.Bitmap;
35 import android.graphics.Color;
36 import android.graphics.Insets;
37 import android.graphics.Rect;
38 import android.graphics.drawable.ColorDrawable;
39 import android.os.Bundle;
40 import android.platform.test.annotations.Presubmit;
41 import android.util.SparseIntArray;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.view.WindowInsets;
45 
46 import androidx.test.rule.ActivityTestRule;
47 
48 import com.android.compatibility.common.util.PollingCheck;
49 
50 import org.hamcrest.CustomTypeSafeMatcher;
51 import org.hamcrest.Description;
52 import org.hamcrest.Matcher;
53 import org.junit.AssumptionViolatedException;
54 import org.junit.Rule;
55 import org.junit.Test;
56 import org.junit.rules.ErrorCollector;
57 import org.junit.rules.RuleChain;
58 
59 import java.util.function.Supplier;
60 
61 /**
62  * Tests for Window's setEnsureStatusBarContrastWhenTransparent and
63  * setEnsureNavigationBarContrastWhenTransparent.
64  */
65 @Presubmit
66 public class EnsureBarContrastTest {
67 
68     private final ErrorCollector mErrorCollector = new ErrorCollector();
69     private final DumpOnFailure mDumper = new DumpOnFailure();
70     private final ActivityTestRule<TestActivity> mTestActivity =
71             new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */,
72                     false /* launchActivity */);
73 
74     @Rule
75     public final RuleChain mRuleChain = RuleChain
76             .outerRule(mDumper)
77             .around(mErrorCollector)
78             .around(mTestActivity);
79 
80     @Test
test_ensureContrast_darkBars()81     public void test_ensureContrast_darkBars() {
82         final boolean lightBars = false;
83         runTestEnsureContrast(lightBars);
84     }
85 
86     @Test
test_ensureContrast_lightBars()87     public void test_ensureContrast_lightBars() {
88         final boolean lightBars = true;
89         runTestEnsureContrast(lightBars);
90     }
91 
runTestEnsureContrast(boolean lightBars)92     public void runTestEnsureContrast(boolean lightBars) {
93         assumeHasColoredBars();
94         TestActivity activity = launchAndWait(mTestActivity, lightBars, true /* ensureContrast */);
95         for (Bar bar : Bar.BARS) {
96             if (isAssumptionViolated(() -> bar.checkAssumptions(mTestActivity))) {
97                 continue;
98             }
99 
100             Bitmap bitmap = getOnMainSync(() -> activity.screenshotBar(bar, mDumper));
101 
102             if (getOnMainSync(() -> activity.barIsTapThrough(bar))) {
103                 assertThat(bar.name + "Bar is tap through, therefore must NOT be scrimmed.", bitmap,
104                         hasNoScrim(lightBars));
105             } else {
106                 // Bar is NOT tap through, may therefore have a scrim.
107             }
108             assertThat(bar.name + "Bar: Ensure contrast was requested, therefore contrast " +
109                     "must be ensured", bitmap, hasContrast(lightBars));
110         }
111     }
112 
113     @Test
test_dontEnsureContrast_darkBars()114     public void test_dontEnsureContrast_darkBars() {
115         final boolean lightBars = false;
116         runTestDontEnsureContrast(lightBars);
117     }
118 
119     @Test
test_dontEnsureContrast_lightBars()120     public void test_dontEnsureContrast_lightBars() {
121         final boolean lightBars = true;
122         runTestDontEnsureContrast(lightBars);
123     }
124 
runTestDontEnsureContrast(boolean lightBars)125     public void runTestDontEnsureContrast(boolean lightBars) {
126         assumeHasColoredBars();
127         TestActivity activity = launchAndWait(mTestActivity, lightBars, false /* ensureContrast */);
128         for (Bar bar : Bar.BARS) {
129             if (isAssumptionViolated(() -> bar.checkAssumptions(mTestActivity))) {
130                 continue;
131             }
132 
133             Bitmap bitmap = getOnMainSync(() -> activity.screenshotBar(bar, mDumper));
134 
135             assertThat(bar.name + "Bar: contrast NOT requested, therefore must NOT be scrimmed.",
136                     bitmap, hasNoScrim(lightBars));
137         }
138     }
139 
hasNoScrim(boolean light)140     private static Matcher<Bitmap> hasNoScrim(boolean light) {
141         return new CustomTypeSafeMatcher<Bitmap>(
142                 "must not have a " + (light ? "light" : "dark") + " scrim") {
143             @Override
144             protected boolean matchesSafely(Bitmap actual) {
145                 int mostFrequentColor = getMostFrequentColor(actual);
146                 return mostFrequentColor == expectedMostFrequentColor();
147             }
148 
149             @Override
150             protected void describeMismatchSafely(Bitmap item, Description mismatchDescription) {
151                 super.describeMismatchSafely(item, mismatchDescription);
152                 mismatchDescription.appendText(" mostFrequentColor: expected #" +
153                         Integer.toHexString(expectedMostFrequentColor()) + ", but was #" +
154                         Integer.toHexString(getMostFrequentColor(item)));
155             }
156 
157             private int expectedMostFrequentColor() {
158                 return backgroundForBar(light);
159             }
160         };
161     }
162 
163     private static Matcher<Bitmap> hasContrast(boolean light) {
164         return new CustomTypeSafeMatcher<Bitmap>(
165                 (light ? "light" : "dark") + " bar must have contrast") {
166             @Override
167             protected boolean matchesSafely(Bitmap actual) {
168                 int[] ps = getPixels(actual);
169                 int bg = backgroundForBar(light);
170 
171                 for (int p : ps) {
172                     if (!sameColor(p, bg)) {
173                         return true;
174                     }
175                 }
176                 return false;
177             }
178 
179             @Override
180             protected void describeMismatchSafely(Bitmap item, Description mismatchDescription) {
181                 super.describeMismatchSafely(item, mismatchDescription);
182                 mismatchDescription.appendText(" expected some color different from " +
183                         backgroundForBar(light));
184             }
185         };
186     }
187 
188     private static int[] getPixels(Bitmap bitmap) {
189         int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()];
190         bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
191         return pixels;
192     }
193 
194     private static int getMostFrequentColor(Bitmap bitmap) {
195         final int[] ps = getPixels(bitmap);
196         final SparseIntArray count = new SparseIntArray();
197         for (int p : ps) {
198             count.put(p, count.get(p) + 1);
199         }
200         int max = 0;
201         for (int i = 0; i < count.size(); i++) {
202             if (count.valueAt(i) > count.valueAt(max)) {
203                 max = i;
204             }
205         }
206         return count.keyAt(max);
207     }
208 
209     private <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) {
210         mErrorCollector.checkThat(reason, actual, matcher);
211     }
212 
213     private <R> R getOnMainSync(Supplier<R> f) {
214         final Object[] result = new Object[1];
215         runOnMainSync(() -> result[0] = f.get());
216         //noinspection unchecked
217         return (R) result[0];
218     }
219 
220     private void runOnMainSync(Runnable runnable) {
221         getInstrumentation().runOnMainSync(runnable);
222     }
223 
224     private <T extends TestActivity> T launchAndWait(ActivityTestRule<T> rule, boolean lightBars,
225             boolean ensureContrast) {
226         final T activity = rule.launchActivity(new Intent()
227                 .putExtra(EXTRA_LIGHT_BARS, lightBars)
228                 .putExtra(EXTRA_ENSURE_CONTRAST, ensureContrast));
229         PollingCheck.waitFor(activity::isReady);
230         activity.onEnterAnimationComplete();
231         return activity;
232     }
233 
234     private static boolean sameColor(int a, int b) {
235         return Math.abs(Color.alpha(a) - Color.alpha(b)) +
236                 Math.abs(Color.red(a) - Color.red(b)) +
237                 Math.abs(Color.green(a) - Color.green(b)) +
238                 Math.abs(Color.blue(a) - Color.blue(b)) < 10;
239     }
240 
241     public static class TestActivity extends Activity {
242 
243         static final String EXTRA_LIGHT_BARS = "extra.light_bars";
244         static final String EXTRA_ENSURE_CONTRAST = "extra.ensure_contrast";
245 
246         private boolean mReady = false;
247 
248         @Override
249         protected void onCreate(Bundle savedInstanceState) {
250             super.onCreate(savedInstanceState);
251 
252             View view = new View(this);
253             view.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
254 
255             if (getIntent() != null) {
256                 boolean lightBars = getIntent().getBooleanExtra(EXTRA_LIGHT_BARS, false);
257                 boolean ensureContrast = getIntent().getBooleanExtra(EXTRA_ENSURE_CONTRAST, false);
258 
259                 // Install the decor
260                 getWindow().getDecorView();
261 
262                 getWindow().setStatusBarContrastEnforced(ensureContrast);
263                 getWindow().setNavigationBarContrastEnforced(ensureContrast);
264 
265                 getWindow().setStatusBarColor(Color.TRANSPARENT);
266                 getWindow().setNavigationBarColor(Color.TRANSPARENT);
267                 getWindow().setBackgroundDrawable(new ColorDrawable(backgroundForBar(lightBars)));
268 
269                 view.setSystemUiVisibility(lightBars ? (View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
270                         | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) : 0);
271             }
272             setContentView(view);
273         }
274 
275         @Override
276         public void onEnterAnimationComplete() {
277             super.onEnterAnimationComplete();
278             mReady = true;
279         }
280 
281         public boolean isReady() {
282             return mReady && hasWindowFocus();
283         }
284 
285         static int backgroundForBar(boolean lightBar) {
286             return lightBar ? Color.BLACK : Color.WHITE;
287         }
288 
289         boolean barIsTapThrough(Bar bar) {
290             final WindowInsets insets = getWindow().getDecorView().getRootWindowInsets();
291 
292             return bar.getInset(insets.getTappableElementInsets())
293                     < bar.getInset(insets.getSystemWindowInsets());
294         }
295 
296         Bitmap screenshotBar(Bar bar, DumpOnFailure dumper) {
297             final View dv = getWindow().getDecorView();
298             final Insets insets = dv.getRootWindowInsets().getSystemWindowInsets();
299 
300             Rect r = bar.getLocation(insets,
301                     new Rect(dv.getLeft(), dv.getTop(), dv.getRight(), dv.getBottom()));
302 
303             Bitmap fullBitmap = getInstrumentation().getUiAutomation().takeScreenshot();
304             dumper.dumpOnFailure("full" + bar.name, fullBitmap);
305             Bitmap barBitmap = Bitmap.createBitmap(fullBitmap, r.left, r.top, r.width(),
306                     r.height());
307             dumper.dumpOnFailure("bar" + bar.name, barBitmap);
308             return barBitmap;
309         }
310     }
311 
312     abstract static class Bar {
313 
314         static final Bar STATUS = new Bar("Status") {
315             @Override
316             int getInset(Insets insets) {
317                 return insets.top;
318             }
319 
320             @Override
321             Rect getLocation(Insets insets, Rect screen) {
322                 final Rect r = new Rect(screen);
323                 r.bottom = r.top + getInset(insets);
324                 return r;
325             }
326 
327             @Override
328             void checkAssumptions(ActivityTestRule<?> rule) throws AssumptionViolatedException {
329                 assumeHasColoredStatusBar(rule);
330             }
331         };
332 
333         static final Bar NAVIGATION = new Bar("Navigation") {
334             @Override
335             int getInset(Insets insets) {
336                 return insets.bottom;
337             }
338 
339             @Override
340             Rect getLocation(Insets insets, Rect screen) {
341                 final Rect r = new Rect(screen);
342                 r.top = r.bottom - getInset(insets);
343                 return r;
344             }
345 
346             @Override
347             void checkAssumptions(ActivityTestRule<?> rule) throws AssumptionViolatedException {
348                 assumeHasColoredNavigationBar(rule);
349             }
350         };
351 
352         static final Bar[] BARS = {STATUS, NAVIGATION};
353 
354         final String name;
355 
356         public Bar(String name) {
357             this.name = name;
358         }
359 
360         abstract int getInset(Insets insets);
361 
362         abstract Rect getLocation(Insets insets, Rect screen);
363 
364         abstract void checkAssumptions(ActivityTestRule<?> rule) throws AssumptionViolatedException;
365     }
366 }
367