1 /* 2 * Copyright (C) 2015 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.systemui.cts; 18 19 import static android.Manifest.permission.POST_NOTIFICATIONS; 20 import static android.Manifest.permission.REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL; 21 import static android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS; 22 import static android.server.wm.BarTestUtils.assumeHasColoredNavigationBar; 23 import static android.server.wm.BarTestUtils.assumeHasColoredStatusBar; 24 25 import static androidx.test.InstrumentationRegistry.getInstrumentation; 26 27 import static org.junit.Assert.assertTrue; 28 29 import android.app.Notification; 30 import android.app.NotificationChannel; 31 import android.app.NotificationManager; 32 import android.app.UiAutomation; 33 import android.content.Context; 34 import android.graphics.Bitmap; 35 import android.graphics.Color; 36 import android.graphics.Insets; 37 import android.os.Process; 38 import android.os.SystemClock; 39 import android.permission.PermissionManager; 40 import android.permission.cts.PermissionUtils; 41 import android.platform.test.annotations.AppModeFull; 42 import android.platform.test.annotations.PlatinumTest; 43 import android.server.wm.IgnoreOrientationRequestSession; 44 import android.view.Gravity; 45 import android.view.InputDevice; 46 import android.view.MotionEvent; 47 import android.view.WindowInsets.Type; 48 import android.view.WindowManager; 49 import android.view.WindowMetrics; 50 51 import androidx.test.rule.ActivityTestRule; 52 import androidx.test.runner.AndroidJUnit4; 53 54 import com.android.compatibility.common.util.SystemUtil; 55 import com.android.compatibility.common.util.ThrowingRunnable; 56 import com.android.settingslib.flags.Flags; 57 58 import org.junit.After; 59 import org.junit.Before; 60 import org.junit.Rule; 61 import org.junit.Test; 62 import org.junit.rules.TestName; 63 import org.junit.runner.RunWith; 64 65 /** 66 * Test for light status bar. 67 * 68 * atest CtsSystemUiTestCases:LightBarTests 69 */ 70 @RunWith(AndroidJUnit4.class) 71 public class LightBarTests extends LightBarTestBase { 72 73 public static final String TAG = "LightStatusBarTests"; 74 75 /** 76 * Color may be slightly off-spec when resources are resized for lower densities. Use this error 77 * margin to accommodate for that when comparing colors. 78 */ 79 private static final int COLOR_COMPONENT_ERROR_MARGIN = 20; 80 81 /** 82 * It's possible for the device to have color sampling enabled in the nav bar -- in that 83 * case we need to pick a background color that would result in the same dark icon tint 84 * that matches the default visibility flags used when color sampling is not enabled. 85 */ 86 private static final int LIGHT_BG_COLOR = Color.rgb(255, 128, 128); 87 88 /** 89 * Flags.newStatusBarIcons() changes the default light mode tint (i.e., dark icons) to 100% 90 * black. If the flag is on we need to change the foreground color we're looking for. 91 */ 92 private static final int DARK_ICON_TINT_LEGACY = 0x99000000; 93 private static final int DARK_ICON_TINT = 0xff000000; 94 95 private final String NOTIFICATION_TAG = "TEST_TAG"; 96 private final String NOTIFICATION_CHANNEL_ID = "test_channel"; 97 private final String NOTIFICATION_GROUP_KEY = "test_group"; 98 private NotificationManager mNm; 99 private IgnoreOrientationRequestSession mOrientationRequestSession; 100 101 @Rule 102 public ActivityTestRule<LightBarActivity> mActivityRule = new ActivityTestRule<>( 103 LightBarActivity.class); 104 @Rule 105 public TestName mTestName = new TestName(); 106 107 108 109 @Before setUp()110 public void setUp() { 111 // We need to prevent letterboxing because when an activity is letterboxed, then the status 112 // bar icons are outside the activity space so our verification will fail. See b/246515090. 113 // 114 // When ignore_orientation_request is set to true and the device is in landscape but the 115 // activity is in portrait, then the device remains in landscape but letterboxes the 116 // activity (so the activity is *not* full screen). Setting ignore_orientation_request to 117 // false will cause the device to instead rotate to portrait to match the activity, thus 118 // preventing letterboxing. 119 mOrientationRequestSession = new IgnoreOrientationRequestSession(false /* enable */); 120 } 121 122 @After tearDown()123 public void tearDown() { 124 if (mOrientationRequestSession != null) { 125 mOrientationRequestSession.close(); 126 } 127 } 128 129 @Test 130 @AppModeFull // Instant apps cannot create notifications 131 @PlatinumTest(focusArea = "sysui") testLightStatusBarIcons()132 public void testLightStatusBarIcons() throws Throwable { 133 assumeHasColoredStatusBar(mActivityRule); 134 135 runInNotificationSession(() -> { 136 requestLightBars(LIGHT_BG_COLOR); 137 Thread.sleep(WAIT_TIME); 138 139 Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity()); 140 Stats s = evaluateLightBarBitmap(bitmap, LIGHT_BG_COLOR, 0); 141 assertStats(bitmap, s, true /* light */); 142 }); 143 } 144 145 @Test 146 @AppModeFull // Instant apps cannot create notifications 147 @PlatinumTest(focusArea = "sysui") testAppearanceCanOverwriteLegacyFlags()148 public void testAppearanceCanOverwriteLegacyFlags() throws Throwable { 149 assumeHasColoredStatusBar(mActivityRule); 150 151 runInNotificationSession(() -> { 152 final LightBarActivity activity = mActivityRule.getActivity(); 153 activity.runOnUiThread(() -> { 154 activity.getWindow().setStatusBarColor(LIGHT_BG_COLOR); 155 activity.getWindow().setNavigationBarColor(LIGHT_BG_COLOR); 156 157 activity.setLightStatusBarLegacy(true); 158 activity.setLightNavigationBarLegacy(true); 159 160 // The new appearance APIs can overwrite the appearance specified by the legacy 161 // flags. 162 activity.setLightStatusBarAppearance(false); 163 activity.setLightNavigationBarAppearance(false); 164 }); 165 Thread.sleep(WAIT_TIME); 166 167 Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity()); 168 Stats s = evaluateDarkBarBitmap(bitmap, LIGHT_BG_COLOR, 0); 169 assertStats(bitmap, s, false /* light */); 170 }); 171 } 172 173 @Test 174 @AppModeFull // Instant apps cannot create notifications 175 @PlatinumTest(focusArea = "sysui") testLegacyFlagsCannotOverwriteAppearance()176 public void testLegacyFlagsCannotOverwriteAppearance() throws Throwable { 177 assumeHasColoredStatusBar(mActivityRule); 178 179 runInNotificationSession(() -> { 180 final LightBarActivity activity = mActivityRule.getActivity(); 181 activity.runOnUiThread(() -> { 182 activity.getWindow().setStatusBarColor(LIGHT_BG_COLOR); 183 activity.getWindow().setNavigationBarColor(LIGHT_BG_COLOR); 184 185 activity.setLightStatusBarAppearance(false); 186 activity.setLightNavigationBarAppearance(false); 187 188 // Once the client starts using the new appearance APIs, the legacy flags won't 189 // change the appearance anymore. 190 activity.setLightStatusBarLegacy(true); 191 activity.setLightNavigationBarLegacy(true); 192 }); 193 Thread.sleep(WAIT_TIME); 194 195 Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity()); 196 Stats s = evaluateDarkBarBitmap(bitmap, LIGHT_BG_COLOR, 0); 197 assertStats(bitmap, s, false /* light */); 198 }); 199 } 200 201 @Test testLightNavigationBar()202 public void testLightNavigationBar() throws Throwable { 203 assumeHasColoredNavigationBar(mActivityRule); 204 205 requestLightBars(LIGHT_BG_COLOR); 206 Thread.sleep(WAIT_TIME); 207 208 // Inject a cancelled interaction with the nav bar to ensure it is at full opacity. 209 int x = mActivityRule.getActivity().getWidth() / 2; 210 int y = mActivityRule.getActivity().getBottom() + 10; 211 injectCanceledTap(x, y); 212 Thread.sleep(WAIT_TIME); 213 214 LightBarActivity activity = mActivityRule.getActivity(); 215 Bitmap bitmap = takeNavigationBarScreenshot(activity); 216 Stats s = evaluateLightBarBitmap(bitmap, LIGHT_BG_COLOR, activity.getBottom()); 217 assertStats(bitmap, s, true /* light */); 218 } 219 220 @Test testNavigationBarDivider()221 public void testNavigationBarDivider() throws Throwable { 222 assumeHasColoredNavigationBar(mActivityRule); 223 224 mActivityRule.runOnUiThread(() -> { 225 mActivityRule.getActivity().getWindow().setNavigationBarColor(Color.RED); 226 mActivityRule.getActivity().getWindow().setNavigationBarDividerColor(Color.WHITE); 227 }); 228 Thread.sleep(WAIT_TIME); 229 230 checkNavigationBarDivider(mActivityRule.getActivity(), Color.WHITE, Color.RED, 231 mTestName.getMethodName()); 232 } 233 234 @Test 235 @AppModeFull // Instant apps cannot create notifications testLightBarIsNotAllowed_fitStatusBar()236 public void testLightBarIsNotAllowed_fitStatusBar() throws Throwable { 237 assumeHasColoredStatusBar(mActivityRule); 238 239 runInNotificationSession(() -> { 240 final LightBarActivity activity = mActivityRule.getActivity(); 241 activity.runOnUiThread(() -> { 242 final WindowMetrics metrics = activity.getWindowManager().getCurrentWindowMetrics(); 243 final Insets insets = metrics.getWindowInsets().getInsets(Type.statusBars()); 244 final WindowManager.LayoutParams attrs = activity.getWindow().getAttributes(); 245 attrs.gravity = Gravity.LEFT | Gravity.TOP; 246 attrs.x = insets.left; 247 attrs.y = insets.top; 248 attrs.width = metrics.getBounds().width() - insets.left - insets.right; 249 attrs.height = metrics.getBounds().height() - insets.top - insets.bottom; 250 activity.getWindow().setAttributes(attrs); 251 activity.getWindow().setStatusBarColor(Color.BLACK); 252 activity.getWindow().setNavigationBarColor(Color.BLACK); 253 activity.setLightStatusBarAppearance(true); 254 activity.setLightNavigationBarAppearance(true); 255 }); 256 Thread.sleep(WAIT_TIME); 257 258 Bitmap bitmap = takeStatusBarScreenshot(activity); 259 Stats s = evaluateDarkBarBitmap(bitmap, Color.TRANSPARENT, 0); 260 assertStats(bitmap, s, false /* light */); 261 }); 262 } 263 runInNotificationSession(ThrowingRunnable task)264 private void runInNotificationSession(ThrowingRunnable task) throws Exception { 265 Context context = getInstrumentation().getContext(); 266 String packageName = getInstrumentation().getTargetContext().getPackageName(); 267 try { 268 PermissionUtils.grantPermission(packageName, POST_NOTIFICATIONS); 269 mNm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 270 NotificationChannel channel1 = new NotificationChannel(NOTIFICATION_CHANNEL_ID, 271 NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW); 272 mNm.createNotificationChannel(channel1); 273 274 // post 10 notifications to ensure enough icons in the status bar 275 for (int i = 0; i < 10; i++) { 276 Notification.Builder noti1 = 277 new Notification.Builder(context, NOTIFICATION_CHANNEL_ID) 278 .setSmallIcon(R.drawable.ic_save) 279 .setChannelId(NOTIFICATION_CHANNEL_ID) 280 .setPriority(Notification.PRIORITY_LOW) 281 .setGroup(NOTIFICATION_GROUP_KEY); 282 mNm.notify(NOTIFICATION_TAG, i, noti1.build()); 283 } 284 285 task.run(); 286 } finally { 287 mNm.cancelAll(); 288 mNm.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID); 289 290 // Use test API to prevent PermissionManager from killing the test process when revoking 291 // permission. 292 SystemUtil.runWithShellPermissionIdentity( 293 () -> context.getSystemService(PermissionManager.class) 294 .revokePostNotificationPermissionWithoutKillForTest( 295 packageName, 296 Process.myUserHandle().getIdentifier()), 297 REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL, 298 REVOKE_RUNTIME_PERMISSIONS); 299 } 300 } 301 injectCanceledTap(int x, int y)302 private void injectCanceledTap(int x, int y) { 303 long downTime = SystemClock.uptimeMillis(); 304 injectEvent(MotionEvent.ACTION_DOWN, x, y, downTime); 305 injectEvent(MotionEvent.ACTION_CANCEL, x, y, downTime); 306 } 307 injectEvent(int action, int x, int y, long downTime)308 private void injectEvent(int action, int x, int y, long downTime) { 309 final UiAutomation automation = getInstrumentation().getUiAutomation(); 310 final long eventTime = SystemClock.uptimeMillis(); 311 MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0); 312 event.setSource(InputDevice.SOURCE_TOUCHSCREEN); 313 assertTrue(automation.injectInputEvent(event, true)); 314 event.recycle(); 315 } 316 assertStats(Bitmap bitmap, Stats s, boolean light)317 private void assertStats(Bitmap bitmap, Stats s, boolean light) { 318 boolean success = false; 319 try { 320 assumeNavigationBarChangesColor(s.backgroundPixels, s.totalPixels()); 321 322 final String spec = light ? "60% black and 24% black" : "100% white and 30% white"; 323 assertMoreThan("Not enough pixels colored as in the spec", 0.3f, 324 (float) s.iconPixels / (float) s.foregroundPixels(), 325 "Are the bar icons colored according to the spec (" + spec + ")?"); 326 327 final String unexpected = light ? "lighter" : "darker"; 328 final String expected = light ? "dark" : "light"; 329 final int sameHuePixels = light ? s.sameHueLightPixels : s.sameHueDarkPixels; 330 assertLessThan("Too many pixels " + unexpected + " than the background", 0.05f, 331 (float) sameHuePixels / (float) s.foregroundPixels(), 332 "Are the bar icons " + expected + "?"); 333 334 // New status bar icons introduce color into the battery icon more regularly. This 335 // value can't be asserted in this way anymore 336 if (!Flags.newStatusBarIcons()) { 337 assertLessThan("Too many pixels with a changed hue", 0.05f, 338 (float) s.unexpectedHuePixels / (float) s.foregroundPixels(), 339 "Are the bar icons color-free?"); 340 } 341 342 success = true; 343 } finally { 344 if (!success) { 345 dumpBitmap(bitmap, mTestName.getMethodName()); 346 } 347 } 348 } 349 requestLightBars(final int background)350 private void requestLightBars(final int background) { 351 final LightBarActivity activity = mActivityRule.getActivity(); 352 activity.runOnUiThread(() -> { 353 activity.getWindow().setStatusBarColor(background); 354 activity.getWindow().setNavigationBarColor(background); 355 activity.setLightStatusBarLegacy(true); 356 activity.setLightNavigationBarLegacy(true); 357 }); 358 } 359 360 private static class Stats { 361 int backgroundPixels; 362 int iconPixels; 363 int sameHueDarkPixels; 364 int sameHueLightPixels; 365 int unexpectedHuePixels; 366 totalPixels()367 int totalPixels() { 368 return backgroundPixels + iconPixels + sameHueDarkPixels 369 + sameHueLightPixels + unexpectedHuePixels; 370 } 371 foregroundPixels()372 int foregroundPixels() { 373 return iconPixels + sameHueDarkPixels 374 + sameHueLightPixels + unexpectedHuePixels; 375 } 376 377 @Override toString()378 public String toString() { 379 return String.format("{bg=%d, ic=%d, dark=%d, light=%d, bad=%d}", 380 backgroundPixels, iconPixels, sameHueDarkPixels, sameHueLightPixels, 381 unexpectedHuePixels); 382 } 383 } 384 evaluateLightBarBitmap(Bitmap bitmap, int background, int shiftY)385 private Stats evaluateLightBarBitmap(Bitmap bitmap, int background, int shiftY) { 386 if (Flags.newStatusBarIcons()) { 387 return evaluateBarBitmap( 388 bitmap, 389 background, 390 shiftY, 391 DARK_ICON_TINT, 392 0x3d000000 393 ); 394 } else { 395 return evaluateBarBitmap( 396 bitmap, 397 background, 398 shiftY, 399 DARK_ICON_TINT_LEGACY, 400 0x3d000000 401 ); 402 } 403 } 404 evaluateDarkBarBitmap(Bitmap bitmap, int background, int shiftY)405 private Stats evaluateDarkBarBitmap(Bitmap bitmap, int background, int shiftY) { 406 return evaluateBarBitmap(bitmap, background, shiftY, 0xffffffff, 0x4dffffff); 407 } 408 evaluateBarBitmap(Bitmap bitmap, int background, int shiftY, int iconColor, int iconPartialColor)409 private Stats evaluateBarBitmap(Bitmap bitmap, int background, int shiftY, int iconColor, 410 int iconPartialColor) { 411 412 int mixedIconColor = mixSrcOver(background, iconColor); 413 int mixedIconPartialColor = mixSrcOver(background, iconPartialColor); 414 float [] hsvMixedIconColor = new float[3]; 415 float [] hsvMixedPartialColor = new float[3]; 416 Color.RGBToHSV(Color.red(mixedIconColor), Color.green(mixedIconColor), 417 Color.blue(mixedIconColor), hsvMixedIconColor); 418 Color.RGBToHSV(Color.red(mixedIconPartialColor), Color.green(mixedIconPartialColor), 419 Color.blue(mixedIconPartialColor), hsvMixedPartialColor); 420 421 float maxHsvValue = Math.max(hsvMixedIconColor[2], hsvMixedPartialColor[2]); 422 float minHsvValue = Math.min(hsvMixedIconColor[2], hsvMixedPartialColor[2]); 423 424 int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()]; 425 bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); 426 427 Stats s = new Stats(); 428 float eps = 0.005f; 429 430 loadCutout(mActivityRule.getActivity()); 431 float [] hsvPixel = new float[3]; 432 int i = 0; 433 for (int c : pixels) { 434 int x = i % bitmap.getWidth(); 435 int y = i / bitmap.getWidth(); 436 i++; 437 if (isInsideCutout(x, shiftY + y)) { 438 continue; 439 } 440 441 if (isColorSame(c, background)) { 442 s.backgroundPixels++; 443 continue; 444 } 445 446 // What we expect the icons to be colored according to the spec. 447 Color.RGBToHSV(Color.red(c), Color.green(c), Color.blue(c), hsvPixel); 448 if (isColorSame(c, mixedIconColor) || isColorSame(c, mixedIconPartialColor) 449 || (hsvPixel[2] >= minHsvValue && hsvPixel[2] <= maxHsvValue)) { 450 s.iconPixels++; 451 continue; 452 } 453 454 // Due to anti-aliasing, there will be deviations from the ideal icon color, but it 455 // should still be mostly the same hue. 456 float hueDiff = Math.abs(ColorUtils.hue(background) - ColorUtils.hue(c)); 457 if (hueDiff < eps || hueDiff > 1 - eps) { 458 // .. it shouldn't be lighter than the original background though. 459 if (ColorUtils.brightness(c) > ColorUtils.brightness(background)) { 460 s.sameHueLightPixels++; 461 } else { 462 s.sameHueDarkPixels++; 463 } 464 continue; 465 } 466 467 s.unexpectedHuePixels++; 468 } 469 470 return s; 471 } 472 mixSrcOver(int background, int foreground)473 private int mixSrcOver(int background, int foreground) { 474 int bgAlpha = Color.alpha(background); 475 int bgRed = Color.red(background); 476 int bgGreen = Color.green(background); 477 int bgBlue = Color.blue(background); 478 479 int fgAlpha = Color.alpha(foreground); 480 int fgRed = Color.red(foreground); 481 int fgGreen = Color.green(foreground); 482 int fgBlue = Color.blue(foreground); 483 484 return Color.argb(fgAlpha + (255 - fgAlpha) * bgAlpha / 255, 485 fgRed + (255 - fgAlpha) * bgRed / 255, 486 fgGreen + (255 - fgAlpha) * bgGreen / 255, 487 fgBlue + (255 - fgAlpha) * bgBlue / 255); 488 } 489 490 /** 491 * Check if two colors' diff is in the error margin as defined in 492 * {@link #COLOR_COMPONENT_ERROR_MARGIN}. 493 */ isColorSame(int c1, int c2)494 private boolean isColorSame(int c1, int c2){ 495 return Math.abs(Color.alpha(c1) - Color.alpha(c2)) < COLOR_COMPONENT_ERROR_MARGIN 496 && Math.abs(Color.red(c1) - Color.red(c2)) < COLOR_COMPONENT_ERROR_MARGIN 497 && Math.abs(Color.green(c1) - Color.green(c2)) < COLOR_COMPONENT_ERROR_MARGIN 498 && Math.abs(Color.blue(c1) - Color.blue(c2)) < COLOR_COMPONENT_ERROR_MARGIN; 499 } 500 } 501