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