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