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