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