Home | History | Annotate | Download | only in cts
      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