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