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.support.test.InstrumentationRegistry.getInstrumentation; 20 import static org.junit.Assert.assertTrue; 21 import static org.junit.Assert.fail; 22 23 import android.app.ActivityManager; 24 import android.app.Notification; 25 import android.app.NotificationChannel; 26 import android.app.NotificationManager; 27 import android.app.UiAutomation; 28 import android.content.Context; 29 import android.content.pm.PackageManager; 30 import android.graphics.Bitmap; 31 import android.graphics.Color; 32 import android.os.SystemClock; 33 import android.platform.test.annotations.AppModeFull; 34 import android.support.test.rule.ActivityTestRule; 35 import android.support.test.runner.AndroidJUnit4; 36 import android.view.InputDevice; 37 import android.view.MotionEvent; 38 39 import org.junit.Rule; 40 import org.junit.Test; 41 import org.junit.runner.RunWith; 42 43 /** 44 * Test for light status bar. 45 * 46 * atest CtsSystemUiTestCases:LightBarTests 47 */ 48 @RunWith(AndroidJUnit4.class) 49 public class LightBarTests extends LightBarTestBase { 50 51 public static final String TAG = "LightStatusBarTests"; 52 53 private static final int WAIT_TIME = 2000; 54 55 /** 56 * Color may be slightly off-spec when resources are resized for lower densities. Use this error 57 * margin to accommodate for that when comparing colors. 58 */ 59 private static final int COLOR_COMPONENT_ERROR_MARGIN = 10; 60 61 private final String NOTIFICATION_TAG = "TEST_TAG"; 62 private final String NOTIFICATION_CHANNEL_ID = "test_channel"; 63 private final String NOTIFICATION_GROUP_KEY = "test_group"; 64 private NotificationManager mNm; 65 66 @Rule 67 public ActivityTestRule<LightBarActivity> mActivityRule = new ActivityTestRule<>( 68 LightBarActivity.class); 69 70 @Test 71 @AppModeFull // Instant apps cannot create notifications 72 public void testLightStatusBarIcons() throws Throwable { 73 assumeHasColoredStatusBar(); 74 75 mNm = (NotificationManager) getInstrumentation().getContext() 76 .getSystemService(Context.NOTIFICATION_SERVICE); 77 NotificationChannel channel1 = new NotificationChannel(NOTIFICATION_CHANNEL_ID, 78 NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW); 79 mNm.createNotificationChannel(channel1); 80 81 // post 10 notifications to ensure enough icons in the status bar 82 for (int i = 0; i < 10; i++) { 83 Notification.Builder noti1 = new Notification.Builder(getInstrumentation().getContext(), 84 NOTIFICATION_CHANNEL_ID) 85 .setSmallIcon(R.drawable.ic_save) 86 .setChannelId(NOTIFICATION_CHANNEL_ID) 87 .setPriority(Notification.PRIORITY_LOW) 88 .setGroup(NOTIFICATION_GROUP_KEY); 89 mNm.notify(NOTIFICATION_TAG, i, noti1.build()); 90 } 91 92 requestLightBars(Color.RED /* background */); 93 Thread.sleep(WAIT_TIME); 94 95 Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity()); 96 Stats s = evaluateLightBarBitmap(bitmap, Color.RED /* background */); 97 assertLightStats(bitmap, s); 98 99 mNm.cancelAll(); 100 mNm.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID); 101 } 102 103 @Test 104 public void testLightNavigationBar() throws Throwable { 105 assumeHasColorNavigationBar(); 106 107 requestLightBars(Color.RED /* background */); 108 Thread.sleep(WAIT_TIME); 109 110 // Inject a cancelled interaction with the nav bar to ensure it is at full opacity. 111 int x = mActivityRule.getActivity().getWidth() / 2; 112 int y = mActivityRule.getActivity().getBottom() + 10; 113 injectCanceledTap(x, y); 114 Thread.sleep(WAIT_TIME); 115 116 Bitmap bitmap = takeNavigationBarScreenshot(mActivityRule.getActivity()); 117 Stats s = evaluateLightBarBitmap(bitmap, Color.RED /* background */); 118 assertLightStats(bitmap, s); 119 } 120 121 @Test 122 public void testNavigationBarDivider() throws Throwable { 123 assumeHasColorNavigationBar(); 124 125 mActivityRule.runOnUiThread(() -> { 126 mActivityRule.getActivity().getWindow().setNavigationBarColor(Color.RED); 127 mActivityRule.getActivity().getWindow().setNavigationBarDividerColor(Color.WHITE); 128 }); 129 Thread.sleep(WAIT_TIME); 130 131 checkNavigationBarDivider(mActivityRule.getActivity(), Color.WHITE); 132 } 133 134 private void injectCanceledTap(int x, int y) { 135 long downTime = SystemClock.uptimeMillis(); 136 injectEvent(MotionEvent.ACTION_DOWN, x, y, downTime); 137 injectEvent(MotionEvent.ACTION_CANCEL, x, y, downTime); 138 } 139 140 private void injectEvent(int action, int x, int y, long downTime) { 141 final UiAutomation automation = getInstrumentation().getUiAutomation(); 142 final long eventTime = SystemClock.uptimeMillis(); 143 MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0); 144 event.setSource(InputDevice.SOURCE_TOUCHSCREEN); 145 assertTrue(automation.injectInputEvent(event, true)); 146 event.recycle(); 147 } 148 149 private void assertLightStats(Bitmap bitmap, Stats s) { 150 boolean success = false; 151 try { 152 assertMoreThan("Not enough background pixels", 0.3f, 153 (float) s.backgroundPixels / s.totalPixels(), 154 "Is the bar background showing correctly (solid red)?"); 155 156 assertMoreThan("Not enough pixels colored as in the spec", 0.3f, 157 (float) s.iconPixels / s.foregroundPixels(), 158 "Are the bar icons colored according to the spec " 159 + "(60% black and 24% black)?"); 160 161 assertLessThan("Too many lighter pixels lighter than the background", 0.05f, 162 (float) s.sameHueLightPixels / s.foregroundPixels(), 163 "Are the bar icons dark?"); 164 165 assertLessThan("Too many pixels with a changed hue", 0.05f, 166 (float) s.unexpectedHuePixels / s.foregroundPixels(), 167 "Are the bar icons color-free?"); 168 169 success = true; 170 } finally { 171 if (!success) { 172 dumpBitmap(bitmap); 173 } 174 } 175 } 176 177 private void assertMoreThan(String what, float expected, float actual, String hint) { 178 if (!(actual > expected)) { 179 fail(what + ": expected more than " + expected * 100 + "%, but only got " + actual * 100 180 + "%; " + hint); 181 } 182 } 183 184 private void assertLessThan(String what, float expected, float actual, String hint) { 185 if (!(actual < expected)) { 186 fail(what + ": expected less than " + expected * 100 + "%, but got " + actual * 100 187 + "%; " + hint); 188 } 189 } 190 191 private void requestLightBars(final int background) throws Throwable { 192 final LightBarActivity activity = mActivityRule.getActivity(); 193 activity.runOnUiThread(() -> { 194 activity.getWindow().setStatusBarColor(background); 195 activity.getWindow().setNavigationBarColor(background); 196 activity.setLightStatusBar(true); 197 activity.setLightNavigationBar(true); 198 }); 199 } 200 201 private static class Stats { 202 int backgroundPixels; 203 int iconPixels; 204 int sameHueDarkPixels; 205 int sameHueLightPixels; 206 int unexpectedHuePixels; 207 208 int totalPixels() { 209 return backgroundPixels + iconPixels + sameHueDarkPixels 210 + sameHueLightPixels + unexpectedHuePixels; 211 } 212 213 int foregroundPixels() { 214 return iconPixels + sameHueDarkPixels 215 + sameHueLightPixels + unexpectedHuePixels; 216 } 217 218 @Override 219 public String toString() { 220 return String.format("{bg=%d, ic=%d, dark=%d, light=%d, bad=%d}", 221 backgroundPixels, iconPixels, sameHueDarkPixels, sameHueLightPixels, 222 unexpectedHuePixels); 223 } 224 } 225 226 private Stats evaluateLightBarBitmap(Bitmap bitmap, int background) { 227 int iconColor = 0x99000000; 228 int iconPartialColor = 0x3d000000; 229 230 int mixedIconColor = mixSrcOver(background, iconColor); 231 int mixedIconPartialColor = mixSrcOver(background, iconPartialColor); 232 233 int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()]; 234 bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); 235 236 Stats s = new Stats(); 237 float eps = 0.005f; 238 239 for (int c : pixels) { 240 if (c == background) { 241 s.backgroundPixels++; 242 continue; 243 } 244 245 // What we expect the icons to be colored according to the spec. 246 if (isColorSame(c, mixedIconColor) || isColorSame(c, mixedIconPartialColor)) { 247 s.iconPixels++; 248 continue; 249 } 250 251 // Due to anti-aliasing, there will be deviations from the ideal icon color, but it 252 // should still be mostly the same hue. 253 float hueDiff = Math.abs(ColorUtils.hue(background) - ColorUtils.hue(c)); 254 if (hueDiff < eps || hueDiff > 1 - eps) { 255 // .. it shouldn't be lighter than the original background though. 256 if (ColorUtils.brightness(c) > ColorUtils.brightness(background)) { 257 s.sameHueLightPixels++; 258 } else { 259 s.sameHueDarkPixels++; 260 } 261 continue; 262 } 263 264 s.unexpectedHuePixels++; 265 } 266 267 return s; 268 } 269 270 private int mixSrcOver(int background, int foreground) { 271 int bgAlpha = Color.alpha(background); 272 int bgRed = Color.red(background); 273 int bgGreen = Color.green(background); 274 int bgBlue = Color.blue(background); 275 276 int fgAlpha = Color.alpha(foreground); 277 int fgRed = Color.red(foreground); 278 int fgGreen = Color.green(foreground); 279 int fgBlue = Color.blue(foreground); 280 281 return Color.argb(fgAlpha + (255 - fgAlpha) * bgAlpha / 255, 282 fgRed + (255 - fgAlpha) * bgRed / 255, 283 fgGreen + (255 - fgAlpha) * bgGreen / 255, 284 fgBlue + (255 - fgAlpha) * bgBlue / 255); 285 } 286 287 /** 288 * Check if two colors' diff is in the error margin as defined in 289 * {@link #COLOR_COMPONENT_ERROR_MARGIN}. 290 */ 291 private boolean isColorSame(int c1, int c2){ 292 return Math.abs(Color.alpha(c1) - Color.alpha(c2)) < COLOR_COMPONENT_ERROR_MARGIN 293 && Math.abs(Color.red(c1) - Color.red(c2)) < COLOR_COMPONENT_ERROR_MARGIN 294 && Math.abs(Color.green(c1) - Color.green(c2)) < COLOR_COMPONENT_ERROR_MARGIN 295 && Math.abs(Color.blue(c1) - Color.blue(c2)) < COLOR_COMPONENT_ERROR_MARGIN; 296 } 297 } 298