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 android.app.ActivityManager; 20 import android.content.pm.PackageManager; 21 import android.graphics.Bitmap; 22 import android.graphics.Color; 23 import android.support.test.InstrumentationRegistry; 24 import android.test.ActivityInstrumentationTestCase2; 25 import android.util.Log; 26 27 import java.io.FileOutputStream; 28 import java.io.IOException; 29 30 /** 31 * Test for light status bar. 32 */ 33 public class LightStatusBarTests extends ActivityInstrumentationTestCase2<LightStatusBarActivity> { 34 35 public static final String TAG = "LightStatusBarTests"; 36 37 public static final String DUMP_PATH = "/sdcard/lightstatustest.png"; 38 LightStatusBarTests()39 public LightStatusBarTests() { 40 super(LightStatusBarActivity.class); 41 } 42 43 @Override setUp()44 protected void setUp() throws Exception { 45 super.setUp(); 46 // As the way to access Instrumentation is changed in the new runner, we need to inject it 47 // manually into ActivityInstrumentationTestCase2. ActivityInstrumentationTestCase2 will 48 // be marked as deprecated and replaced with ActivityTestRule. 49 injectInstrumentation(InstrumentationRegistry.getInstrumentation()); 50 } 51 testLightStatusBarIcons()52 public void testLightStatusBarIcons() throws Throwable { 53 PackageManager pm = getInstrumentation().getContext().getPackageManager(); 54 if (pm.hasSystemFeature(PackageManager.FEATURE_WATCH) 55 || pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION) 56 || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { 57 // No status bar on TVs and watches. 58 return; 59 } 60 61 if (!ActivityManager.isHighEndGfx()) { 62 // non-highEndGfx devices don't do colored system bars. 63 return; 64 } 65 66 requestLightStatusBar(Color.RED /* background */); 67 Thread.sleep(1000); 68 69 Bitmap bitmap = takeStatusBarScreenshot(); 70 Stats s = evaluateLightStatusBarBitmap(bitmap, Color.RED /* background */); 71 boolean success = false; 72 73 try { 74 assertMoreThan("Not enough background pixels", 0.3f, 75 (float) s.backgroundPixels / s.totalPixels(), 76 "Is the status bar background showing correctly (solid red)?"); 77 78 assertMoreThan("Not enough pixels colored as in the spec", 0.1f, 79 (float) s.iconPixels / s.foregroundPixels(), 80 "Are the status bar icons colored according to the spec " 81 + "(60% black and 24% black)?"); 82 83 assertLessThan("Too many lighter pixels lighter than the background", 0.05f, 84 (float) s.sameHueLightPixels / s.foregroundPixels(), 85 "Are the status bar icons dark?"); 86 87 assertLessThan("Too many pixels with a changed hue", 0.05f, 88 (float) s.unexpectedHuePixels / s.foregroundPixels(), 89 "Are the status bar icons color-free?"); 90 91 success = true; 92 } finally { 93 if (!success) { 94 Log.e(TAG, "Dumping failed bitmap to " + DUMP_PATH); 95 dumpBitmap(bitmap); 96 } 97 } 98 } 99 assertMoreThan(String what, float expected, float actual, String hint)100 private void assertMoreThan(String what, float expected, float actual, String hint) { 101 if (!(actual > expected)) { 102 fail(what + ": expected more than " + expected * 100 + "%, but only got " + actual * 100 103 + "%; " + hint); 104 } 105 } 106 assertLessThan(String what, float expected, float actual, String hint)107 private void assertLessThan(String what, float expected, float actual, String hint) { 108 if (!(actual < expected)) { 109 fail(what + ": expected less than " + expected * 100 + "%, but got " + actual * 100 110 + "%; " + hint); 111 } 112 } 113 requestLightStatusBar(final int background)114 private void requestLightStatusBar(final int background) throws Throwable { 115 final LightStatusBarActivity activity = getActivity(); 116 runTestOnUiThread(new Runnable() { 117 @Override 118 public void run() { 119 activity.getWindow().setStatusBarColor(background); 120 activity.setLightStatusBar(true); 121 } 122 }); 123 } 124 125 private static class Stats { 126 int backgroundPixels; 127 int iconPixels; 128 int sameHueDarkPixels; 129 int sameHueLightPixels; 130 int unexpectedHuePixels; 131 totalPixels()132 int totalPixels() { 133 return backgroundPixels + iconPixels + sameHueDarkPixels 134 + sameHueLightPixels + unexpectedHuePixels; 135 } 136 foregroundPixels()137 int foregroundPixels() { 138 return iconPixels + sameHueDarkPixels 139 + sameHueLightPixels + unexpectedHuePixels; 140 } 141 142 @Override toString()143 public String toString() { 144 return String.format("{bg=%d, ic=%d, dark=%d, light=%d, bad=%d}", 145 backgroundPixels, iconPixels, sameHueDarkPixels, sameHueLightPixels, 146 unexpectedHuePixels); 147 } 148 } 149 evaluateLightStatusBarBitmap(Bitmap bitmap, int background)150 private Stats evaluateLightStatusBarBitmap(Bitmap bitmap, int background) { 151 int iconColor = 0x99000000; 152 int iconPartialColor = 0x3d000000; 153 154 int mixedIconColor = mixSrcOver(background, iconColor); 155 int mixedIconPartialColor = mixSrcOver(background, iconPartialColor); 156 157 int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()]; 158 bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); 159 160 Stats s = new Stats(); 161 float eps = 0.005f; 162 163 for (int c : pixels) { 164 if (c == background) { 165 s.backgroundPixels++; 166 continue; 167 } 168 169 // What we expect the icons to be colored according to the spec. 170 if (c == mixedIconColor || c == mixedIconPartialColor) { 171 s.iconPixels++; 172 continue; 173 } 174 175 // Due to anti-aliasing, there will be deviations from the ideal icon color, but it 176 // should still be mostly the same hue. 177 float hueDiff = Math.abs(ColorUtils.hue(background) - ColorUtils.hue(c)); 178 if (hueDiff < eps || hueDiff > 1 - eps) { 179 // .. it shouldn't be lighter than the original background though. 180 if (ColorUtils.brightness(c) > ColorUtils.brightness(background)) { 181 s.sameHueLightPixels++; 182 } else { 183 s.sameHueDarkPixels++; 184 } 185 continue; 186 } 187 188 s.unexpectedHuePixels++; 189 } 190 191 return s; 192 } 193 dumpBitmap(Bitmap bitmap)194 private void dumpBitmap(Bitmap bitmap) { 195 FileOutputStream fileStream = null; 196 try { 197 fileStream = new FileOutputStream(DUMP_PATH); 198 bitmap.compress(Bitmap.CompressFormat.PNG, 85, fileStream); 199 fileStream.flush(); 200 } catch (Exception e) { 201 Log.e(TAG, "Dumping bitmap failed.", e); 202 } finally { 203 if (fileStream != null) { 204 try { 205 fileStream.close(); 206 } catch (IOException e) { 207 e.printStackTrace(); 208 } 209 } 210 } 211 } 212 mixSrcOver(int background, int foreground)213 private int mixSrcOver(int background, int foreground) { 214 int bgAlpha = Color.alpha(background); 215 int bgRed = Color.red(background); 216 int bgGreen = Color.green(background); 217 int bgBlue = Color.blue(background); 218 219 int fgAlpha = Color.alpha(foreground); 220 int fgRed = Color.red(foreground); 221 int fgGreen = Color.green(foreground); 222 int fgBlue = Color.blue(foreground); 223 224 return Color.argb(fgAlpha + (255 - fgAlpha) * bgAlpha / 255, 225 fgRed + (255 - fgAlpha) * bgRed / 255, 226 fgGreen + (255 - fgAlpha) * bgGreen / 255, 227 fgBlue + (255 - fgAlpha) * bgBlue / 255); 228 } 229 takeStatusBarScreenshot()230 private Bitmap takeStatusBarScreenshot() { 231 Bitmap fullBitmap = getInstrumentation().getUiAutomation().takeScreenshot(); 232 return Bitmap.createBitmap(fullBitmap, 0, 0, 233 getActivity().getWidth(), getActivity().getTop()); 234 } 235 } 236