Add light status bar CTS

Bug: 23427621

Change-Id: I9867f0924ca7c80e1146bc675ff7b4f736539ac4
diff --git a/CtsTestCaseList.mk b/CtsTestCaseList.mk
index 41f9dc3..975ac47 100644
--- a/CtsTestCaseList.mk
+++ b/CtsTestCaseList.mk
@@ -194,6 +194,7 @@
     CtsSecurityTestCases \
     CtsSignatureTestCases \
     CtsSpeechTestCases \
+    CtsSystemUiTestCases \
     CtsTelecomTestCases \
     CtsTelecomTestCases2 \
     CtsTelephonyTestCases \
diff --git a/tests/expectations/knownfailures.txt b/tests/expectations/knownfailures.txt
index 345279f..f15f6b0 100644
--- a/tests/expectations/knownfailures.txt
+++ b/tests/expectations/knownfailures.txt
@@ -262,6 +262,13 @@
   bug: 23008511
 },
 {
+  description: "Light status bar CTS coming in late",
+  names: [
+    "com.android.cts.systemui.LightStatusBarTests#testLightStatusBarIcons"
+  ],
+  bug: 23427621
+},
+{
   description: "known failures",
   names: [
     "android.hardware.cts.SensorBatchingTests#testAccelerometer_50hz_batching",
diff --git a/tests/tests/systemui/Android.mk b/tests/tests/systemui/Android.mk
new file mode 100644
index 0000000..1a15fd2
--- /dev/null
+++ b/tests/tests/systemui/Android.mk
@@ -0,0 +1,30 @@
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# don't include this package in any target
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_STATIC_JAVA_LIBRARIES := ctstestrunner
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CtsSystemUiTestCases
+
+include $(BUILD_CTS_PACKAGE)
diff --git a/tests/tests/systemui/AndroidManifest.xml b/tests/tests/systemui/AndroidManifest.xml
new file mode 100644
index 0000000..bf5df5b
--- /dev/null
+++ b/tests/tests/systemui/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.cts.systemui">
+    <uses-permission android:name="android.permission.INJECT_EVENTS" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <application>
+        <activity android:name=".LightStatusBarActivity"
+                android:theme="@android:style/Theme.Material.NoActionBar"></activity>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.cts.systemui">
+    </instrumentation>
+
+</manifest>
+
diff --git a/tests/tests/systemui/src/com/android/cts/systemui/LightStatusBarActivity.java b/tests/tests/systemui/src/com/android/cts/systemui/LightStatusBarActivity.java
new file mode 100644
index 0000000..3722320
--- /dev/null
+++ b/tests/tests/systemui/src/com/android/cts/systemui/LightStatusBarActivity.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.cts.systemui;
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+
+
+/**
+ * An activity that exercises SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.
+ */
+public class LightStatusBarActivity extends Activity {
+
+    private View mContent;
+
+    public void onCreate(Bundle bundle){
+        super.onCreate(bundle);
+
+        mContent = new View(this);
+        mContent.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
+                LayoutParams.MATCH_PARENT));
+        setContentView(mContent);
+    }
+
+    public void setLightStatusBar(boolean lightStatusBar) {
+        int vis = getWindow().getDecorView().getSystemUiVisibility();
+        if (lightStatusBar) {
+            vis |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+        } else {
+            vis &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+        }
+        getWindow().getDecorView().setSystemUiVisibility(vis);
+    }
+
+    public int getTop() {
+        return mContent.getLocationOnScreen()[1];
+    }
+
+    public int getWidth() {
+        return mContent.getWidth();
+    }
+}
diff --git a/tests/tests/systemui/src/com/android/cts/systemui/LightStatusBarTests.java b/tests/tests/systemui/src/com/android/cts/systemui/LightStatusBarTests.java
new file mode 100644
index 0000000..cf3bd94
--- /dev/null
+++ b/tests/tests/systemui/src/com/android/cts/systemui/LightStatusBarTests.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.cts.systemui;
+
+import android.app.ActivityManager;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.support.test.InstrumentationRegistry;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Test for light status bar.
+ */
+public class LightStatusBarTests extends ActivityInstrumentationTestCase2<LightStatusBarActivity> {
+
+    public static final String TAG = "LightStatusBarTests";
+
+    public static final String DUMP_PATH = "/sdcard/lightstatustest.png";
+
+    public LightStatusBarTests() {
+        super(LightStatusBarActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        // As the way to access Instrumentation is changed in the new runner, we need to inject it
+        // manually into ActivityInstrumentationTestCase2. ActivityInstrumentationTestCase2 will
+        // be marked as deprecated and replaced with ActivityTestRule.
+        injectInstrumentation(InstrumentationRegistry.getInstrumentation());
+    }
+
+    public void testLightStatusBarIcons() throws Throwable {
+        PackageManager pm = getInstrumentation().getContext().getPackageManager();
+        if (pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
+                || pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION)
+                || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+            // No status bar on TVs and watches.
+            return;
+        }
+
+        if (!ActivityManager.isHighEndGfx()) {
+            // non-highEndGfx devices don't do colored system bars.
+            return;
+        }
+
+        requestLightStatusBar(Color.RED /* background */);
+        Thread.sleep(1000);
+
+        Bitmap bitmap = takeStatusBarScreenshot();
+        Stats s = evaluateLightStatusBarBitmap(bitmap, Color.RED /* background */);
+        boolean success = false;
+
+        try {
+            assertMoreThan("Not enough background pixels", 0.3f,
+                    (float) s.backgroundPixels / s.totalPixels(),
+                    "Is the status bar background showing correctly (solid red)?");
+
+            assertMoreThan("Not enough pixels colored as in the spec", 0.1f,
+                    (float) s.iconPixels / s.foregroundPixels(),
+                    "Are the status bar icons colored according to the spec "
+                            + "(60% black and 24% black)?");
+
+            assertLessThan("Too many lighter pixels lighter than the background", 0.05f,
+                    (float) s.sameHueLightPixels / s.foregroundPixels(),
+                    "Are the status bar icons dark?");
+
+            assertLessThan("Too many pixels with a changed hue", 0.05f,
+                    (float) s.unexpectedHuePixels / s.foregroundPixels(),
+                    "Are the status bar icons color-free?");
+
+            success = true;
+        } finally {
+            if (!success) {
+                Log.e(TAG, "Dumping failed bitmap to " + DUMP_PATH);
+                dumpBitmap(bitmap);
+            }
+        }
+    }
+
+    private void assertMoreThan(String what, float expected, float actual, String hint) {
+        if (!(actual > expected)) {
+            fail(what + ": expected more than " + expected * 100 + "%, but only got " + actual * 100
+                    + "%; " + hint);
+        }
+    }
+
+    private void assertLessThan(String what, float expected, float actual, String hint) {
+        if (!(actual < expected)) {
+            fail(what + ": expected less than " + expected * 100 + "%, but got " + actual * 100
+                    + "%; " + hint);
+        }
+    }
+
+    private void requestLightStatusBar(final int background) throws Throwable {
+        final LightStatusBarActivity activity = getActivity();
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                activity.getWindow().setStatusBarColor(background);
+                activity.setLightStatusBar(true);
+            }
+        });
+    }
+
+    private static class Stats {
+        int backgroundPixels;
+        int iconPixels;
+        int sameHueDarkPixels;
+        int sameHueLightPixels;
+        int unexpectedHuePixels;
+
+        int totalPixels() {
+            return backgroundPixels + iconPixels + sameHueDarkPixels
+                    + sameHueLightPixels + unexpectedHuePixels;
+        }
+
+        int foregroundPixels() {
+            return iconPixels + sameHueDarkPixels
+                    + sameHueLightPixels + unexpectedHuePixels;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("{bg=%d, ic=%d, dark=%d, light=%d, bad=%d}",
+                    backgroundPixels, iconPixels, sameHueDarkPixels, sameHueLightPixels,
+                    unexpectedHuePixels);
+        }
+    }
+
+    private Stats evaluateLightStatusBarBitmap(Bitmap bitmap, int background) {
+        int iconColor = 0x99000000;
+        int iconPartialColor = 0x3d000000;
+
+        int mixedIconColor = mixSrcOver(background, iconColor);
+        int mixedIconPartialColor = mixSrcOver(background, iconPartialColor);
+
+        int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()];
+        bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
+
+        Stats s = new Stats();
+        float eps = 0.005f;
+
+        for (int c : pixels) {
+            if (c == background) {
+                s.backgroundPixels++;
+                continue;
+            }
+
+            // What we expect the icons to be colored according to the spec.
+            if (c == mixedIconColor || c == mixedIconPartialColor) {
+                s.iconPixels++;
+                continue;
+            }
+
+            // Due to anti-aliasing, there will be deviations from the ideal icon color, but it
+            // should still be mostly the same hue.
+            float hueDiff = Math.abs(Color.hue(background) - Color.hue(c));
+            if (hueDiff < eps || hueDiff > 1 - eps) {
+                // .. it shouldn't be lighter than the original background though.
+                if (Color.brightness(c) > Color.brightness(background)) {
+                    s.sameHueLightPixels++;
+                } else {
+                    s.sameHueDarkPixels++;
+                }
+                continue;
+            }
+
+            s.unexpectedHuePixels++;
+        }
+
+        return s;
+    }
+
+    private void dumpBitmap(Bitmap bitmap) {
+        FileOutputStream fileStream = null;
+        try {
+            fileStream = new FileOutputStream(DUMP_PATH);
+            bitmap.compress(Bitmap.CompressFormat.PNG, 85, fileStream);
+            fileStream.flush();
+        } catch (Exception e) {
+            Log.e(TAG, "Dumping bitmap failed.", e);
+        } finally {
+            if (fileStream != null) {
+                try {
+                    fileStream.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    private int mixSrcOver(int background, int foreground) {
+        int bgAlpha = Color.alpha(background);
+        int bgRed = Color.red(background);
+        int bgGreen = Color.green(background);
+        int bgBlue = Color.blue(background);
+
+        int fgAlpha = Color.alpha(foreground);
+        int fgRed = Color.red(foreground);
+        int fgGreen = Color.green(foreground);
+        int fgBlue = Color.blue(foreground);
+
+        return Color.argb(fgAlpha + (255 - fgAlpha) * bgAlpha / 255,
+                    fgRed + (255 - fgAlpha) * bgRed / 255,
+                    fgGreen + (255 - fgAlpha) * bgGreen / 255,
+                    fgBlue + (255 - fgAlpha) * bgBlue / 255);
+    }
+
+    private Bitmap takeStatusBarScreenshot() {
+        Bitmap fullBitmap = getInstrumentation().getUiAutomation().takeScreenshot();
+        return Bitmap.createBitmap(fullBitmap, 0, 0,
+                getActivity().getWidth(), getActivity().getTop());
+    }
+}
diff --git a/tools/utils/buildCts.py b/tools/utils/buildCts.py
index a048ddc..adb6a4e 100755
--- a/tools/utils/buildCts.py
+++ b/tools/utils/buildCts.py
@@ -492,6 +492,9 @@
       'android.voicesettings' : [
           'android.voicesettings.cts.ZenModeTest#testAll',
       ],
+      'com.android.cts.systemui' : [
+          'com.android.cts.systemui.LightStatusBarTests#testLightStatusBarIcons',
+      ],
       'com.android.cts.app.os' : [
           'com.android.cts.app.os.OsHostTests#testNonExportedActivities',
       ],