Home | History | Annotate | Download | only in cts
      1 /*
      2  * Copyright (C) 2017 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 androidx.test.InstrumentationRegistry.getInstrumentation;
     20 
     21 import static org.junit.Assert.fail;
     22 import static org.junit.Assume.assumeFalse;
     23 import static org.junit.Assume.assumeTrue;
     24 
     25 import android.app.ActivityManager;
     26 import android.content.Context;
     27 import android.content.pm.PackageManager;
     28 import android.content.res.Configuration;
     29 import android.graphics.Bitmap;
     30 import android.graphics.Color;
     31 import android.graphics.Rect;
     32 import android.util.Log;
     33 import android.view.DisplayCutout;
     34 import android.view.WindowInsets;
     35 
     36 import androidx.test.InstrumentationRegistry;
     37 import androidx.test.rule.ActivityTestRule;
     38 
     39 import java.io.File;
     40 import java.io.FileOutputStream;
     41 import java.io.IOException;
     42 import java.nio.file.FileSystems;
     43 import java.nio.file.Path;
     44 import java.util.ArrayList;
     45 import java.util.Locale;
     46 
     47 public class LightBarTestBase {
     48 
     49     private static final String TAG = "LightBarTestBase";
     50 
     51     public static final Path DUMP_PATH = FileSystems.getDefault()
     52             .getPath("/sdcard/LightBarTestBase/");
     53 
     54     public static final int WAIT_TIME = 2000;
     55 
     56     private static final int COLOR_DIFF_THESHOLDS = 2;
     57 
     58     private ArrayList<Rect> mCutouts;
     59 
     60     protected Bitmap takeStatusBarScreenshot(LightBarBaseActivity activity) {
     61         Bitmap fullBitmap = getInstrumentation().getUiAutomation().takeScreenshot();
     62         return Bitmap.createBitmap(fullBitmap, 0, 0, activity.getWidth(), activity.getTop());
     63     }
     64 
     65     protected Bitmap takeNavigationBarScreenshot(LightBarBaseActivity activity) {
     66         Bitmap fullBitmap = getInstrumentation().getUiAutomation().takeScreenshot();
     67         return Bitmap.createBitmap(fullBitmap, 0, activity.getBottom(), activity.getWidth(),
     68                 fullBitmap.getHeight() - activity.getBottom());
     69     }
     70 
     71     protected void dumpBitmap(Bitmap bitmap, String name) {
     72         File dumpDir = DUMP_PATH.toFile();
     73         if (!dumpDir.exists()) {
     74             dumpDir.mkdirs();
     75         }
     76 
     77         Path filePath = DUMP_PATH.resolve(name + ".png");
     78         Log.e(TAG, "Dumping failed bitmap to " + filePath);
     79         FileOutputStream fileStream = null;
     80         try {
     81             fileStream = new FileOutputStream(filePath.toFile());
     82             bitmap.compress(Bitmap.CompressFormat.PNG, 85, fileStream);
     83             fileStream.flush();
     84         } catch (Exception e) {
     85             Log.e(TAG, "Dumping bitmap failed.", e);
     86         } finally {
     87             if (fileStream != null) {
     88                 try {
     89                     fileStream.close();
     90                 } catch (IOException e) {
     91                     e.printStackTrace();
     92                 }
     93             }
     94         }
     95     }
     96 
     97     private boolean hasVirtualNavigationBar(ActivityTestRule<? extends LightBarBaseActivity> rule)
     98             throws Throwable {
     99         final WindowInsets[] inset = new WindowInsets[1];
    100         rule.runOnUiThread(()-> {
    101             inset[0] = rule.getActivity().getRootWindowInsets();
    102         });
    103         return inset[0].getStableInsetBottom() > 0;
    104     }
    105 
    106     private boolean isRunningInVr() {
    107         final Context context = InstrumentationRegistry.getContext();
    108         final Configuration config = context.getResources().getConfiguration();
    109         return (config.uiMode & Configuration.UI_MODE_TYPE_MASK)
    110                 == Configuration.UI_MODE_TYPE_VR_HEADSET;
    111     }
    112 
    113     private void assumeBasics() {
    114         final PackageManager pm = getInstrumentation().getContext().getPackageManager();
    115 
    116         // No bars on embedded devices.
    117         assumeFalse(getInstrumentation().getContext().getPackageManager().hasSystemFeature(
    118                 PackageManager.FEATURE_EMBEDDED));
    119 
    120         // No bars on TVs and watches.
    121         // Automotive navigation bar is not transparent
    122         assumeFalse(pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
    123                 || pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION)
    124                 || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
    125                 || pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE));
    126 
    127 
    128         // Non-highEndGfx devices don't do colored system bars.
    129         assumeTrue(ActivityManager.isHighEndGfx());
    130     }
    131 
    132     protected void assumeHasColoredStatusBar(ActivityTestRule<? extends LightBarBaseActivity> rule)
    133             throws Throwable {
    134         assumeBasics();
    135 
    136         // No status bar when running in Vr
    137         assumeFalse(isRunningInVr());
    138 
    139         // Status bar exists only when top stable inset is positive
    140         final WindowInsets[] inset = new WindowInsets[1];
    141         rule.runOnUiThread(()-> {
    142             inset[0] = rule.getActivity().getRootWindowInsets();
    143         });
    144         assumeTrue("Top stable inset is non-positive.", inset[0].getStableInsetTop() > 0);
    145     }
    146 
    147     protected void assumeHasColoredNavigationBar(
    148             ActivityTestRule<? extends LightBarBaseActivity> rule) throws Throwable {
    149         assumeBasics();
    150 
    151         // No virtual navigation bar, so no effect.
    152         assumeTrue(hasVirtualNavigationBar(rule));
    153     }
    154 
    155     protected void checkNavigationBarDivider(LightBarBaseActivity activity, int dividerColor,
    156             int backgroundColor, String methodName) {
    157         final Bitmap bitmap = takeNavigationBarScreenshot(activity);
    158         int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()];
    159         bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
    160 
    161         loadCutout(activity);
    162         int backgroundColorPixelCount = 0;
    163         int shiftY = activity.getBottom();
    164         for (int i = 0; i < pixels.length; i++) {
    165             int x = i % bitmap.getWidth();
    166             int y = i / bitmap.getWidth();
    167 
    168             if (pixels[i] == backgroundColor
    169                     || isInsideCutout(x, shiftY + y)) {
    170                 backgroundColorPixelCount++;
    171             }
    172         }
    173         assumeNavigationBarChangesColor(backgroundColorPixelCount, pixels.length);
    174 
    175         int diffCount = 0;
    176         for (int col = 0; col < bitmap.getWidth(); col++) {
    177             if (isInsideCutout(col, shiftY)) {
    178                 continue;
    179             }
    180 
    181             if (!isColorSame(dividerColor, pixels[col])) {
    182                 diffCount++;
    183             }
    184         }
    185 
    186         boolean success = false;
    187         try {
    188             assertLessThan(String.format(Locale.ENGLISH,
    189                     "There are invalid color pixels. expected= 0x%08x", dividerColor),
    190                     0.3f, (float) diffCount / (float)bitmap.getWidth(),
    191                     "Is the divider colored according to android:navigationBarDividerColor "
    192                             + " in the theme?");
    193             success = true;
    194         } finally {
    195             if (!success) {
    196                 dumpBitmap(bitmap, methodName);
    197             }
    198         }
    199     }
    200 
    201     private static boolean isColorSame(int c1, int c2) {
    202         return Math.abs(Color.alpha(c1) - Color.alpha(c2)) < COLOR_DIFF_THESHOLDS
    203                 && Math.abs(Color.red(c1) - Color.red(c2)) < COLOR_DIFF_THESHOLDS
    204                 && Math.abs(Color.green(c1) - Color.green(c2)) < COLOR_DIFF_THESHOLDS
    205                 && Math.abs(Color.blue(c1) - Color.blue(c2)) < COLOR_DIFF_THESHOLDS;
    206     }
    207 
    208     protected void assumeNavigationBarChangesColor(int backgroundColorPixelCount, int totalPixel) {
    209         assumeTrue("Not enough background pixels. The navigation bar may not be able to change "
    210                 + "color.", backgroundColorPixelCount > 0.3f * totalPixel);
    211     }
    212 
    213     protected ArrayList loadCutout(LightBarBaseActivity activity) {
    214         mCutouts = new ArrayList<>();
    215         InstrumentationRegistry.getInstrumentation().runOnMainSync(()-> {
    216             WindowInsets windowInsets = activity.getRootWindowInsets();
    217             DisplayCutout displayCutout = windowInsets.getDisplayCutout();
    218             if (displayCutout != null) {
    219                 mCutouts.addAll(displayCutout.getBoundingRects());
    220             }
    221         });
    222         return mCutouts;
    223     }
    224 
    225     protected boolean isInsideCutout(int x, int y) {
    226         for (Rect cutout : mCutouts) {
    227             if (cutout.contains(x, y)) {
    228                 return true;
    229             }
    230         }
    231         return false;
    232     }
    233 
    234     protected void assertMoreThan(String what, float expected, float actual, String hint) {
    235         if (!(actual > expected)) {
    236             fail(what + ": expected more than " + expected * 100 + "%, but only got " + actual * 100
    237                     + "%; " + hint);
    238         }
    239     }
    240 
    241     protected void assertLessThan(String what, float expected, float actual, String hint) {
    242         if (!(actual < expected)) {
    243             fail(what + ": expected less than " + expected * 100 + "%, but got " + actual * 100
    244                     + "%; " + hint);
    245         }
    246     }
    247 }
    248