1 /* 2 * Copyright (C) 2016 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.widget.cts.util; 18 19 import static org.junit.Assert.assertNull; 20 import static org.mockito.hamcrest.MockitoHamcrest.argThat; 21 22 import android.content.Context; 23 import android.content.res.ColorStateList; 24 import android.graphics.Bitmap; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.Rect; 28 import android.graphics.drawable.BitmapDrawable; 29 import android.graphics.drawable.Drawable; 30 import androidx.annotation.ColorInt; 31 import androidx.annotation.DrawableRes; 32 import androidx.annotation.NonNull; 33 import android.util.Pair; 34 import android.util.SparseBooleanArray; 35 import android.view.View; 36 import android.view.ViewParent; 37 import android.widget.TextView; 38 39 import com.android.compatibility.common.util.WidgetTestUtils; 40 41 import junit.framework.Assert; 42 43 import org.hamcrest.BaseMatcher; 44 import org.hamcrest.Description; 45 46 import java.util.ArrayList; 47 import java.util.Arrays; 48 import java.util.List; 49 50 public class TestUtils { 51 /** 52 * This method takes a view and returns a single bitmap that is the layered combination 53 * of background drawables of this view and all its ancestors. It can be used to abstract 54 * away the specific implementation of a view hierarchy that is not exposed via class APIs 55 * or a view hierarchy that depends on the platform version. Instead of hard-coded lookups 56 * of particular inner implementations of such a view hierarchy that can break during 57 * refactoring or on newer platform versions, calling this API returns a "combined" background 58 * of the view. 59 * 60 * For example, it is useful to get the combined background of a popup / dropdown without 61 * delving into the inner implementation details of how that popup is implemented on a 62 * particular platform version. 63 */ getCombinedBackgroundBitmap(View view)64 public static Bitmap getCombinedBackgroundBitmap(View view) { 65 final int bitmapWidth = view.getWidth(); 66 final int bitmapHeight = view.getHeight(); 67 68 // Create a bitmap 69 final Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, 70 Bitmap.Config.ARGB_8888); 71 // Create a canvas that wraps the bitmap 72 final Canvas canvas = new Canvas(bitmap); 73 74 // As the draw pass starts at the top of view hierarchy, our first step is to traverse 75 // the ancestor hierarchy of our view and collect a list of all ancestors with non-null 76 // and visible backgrounds. At each step we're keeping track of the combined offsets 77 // so that we can properly combine all of the visuals together in the next pass. 78 List<View> ancestorsWithBackgrounds = new ArrayList<>(); 79 List<Pair<Integer, Integer>> ancestorOffsets = new ArrayList<>(); 80 int offsetX = 0; 81 int offsetY = 0; 82 while (true) { 83 final Drawable backgroundDrawable = view.getBackground(); 84 if ((backgroundDrawable != null) && backgroundDrawable.isVisible()) { 85 ancestorsWithBackgrounds.add(view); 86 ancestorOffsets.add(Pair.create(offsetX, offsetY)); 87 } 88 // Go to the parent 89 ViewParent parent = view.getParent(); 90 if (!(parent instanceof View)) { 91 // We're done traversing the ancestor chain 92 break; 93 } 94 95 // Update the offsets based on the location of current view in its parent's bounds 96 offsetX += view.getLeft(); 97 offsetY += view.getTop(); 98 99 view = (View) parent; 100 } 101 102 // Now we're going to iterate over the collected ancestors in reverse order (starting from 103 // the topmost ancestor) and draw their backgrounds into our combined bitmap. At each step 104 // we are respecting the offsets of our original view in the coordinate system of the 105 // currently drawn ancestor. 106 final int layerCount = ancestorsWithBackgrounds.size(); 107 for (int i = layerCount - 1; i >= 0; i--) { 108 View ancestor = ancestorsWithBackgrounds.get(i); 109 Pair<Integer, Integer> offsets = ancestorOffsets.get(i); 110 111 canvas.translate(offsets.first, offsets.second); 112 ancestor.getBackground().draw(canvas); 113 canvas.translate(-offsets.first, -offsets.second); 114 } 115 116 return bitmap; 117 } 118 119 /** 120 * Checks whether all the pixels in the specified of the {@link View} are 121 * filled with the specific color. 122 * 123 * In case there is a color mismatch, the behavior of this method depends on the 124 * <code>throwExceptionIfFails</code> parameter. If it is <code>true</code>, this method will 125 * throw an <code>Exception</code> describing the mismatch. Otherwise this method will call 126 * <code>Assert.fail</code> with detailed description of the mismatch. 127 */ assertAllPixelsOfColor(String failMessagePrefix, @NonNull View view, @ColorInt int color, int allowedComponentVariance, boolean throwExceptionIfFails)128 public static void assertAllPixelsOfColor(String failMessagePrefix, @NonNull View view, 129 @ColorInt int color, int allowedComponentVariance, boolean throwExceptionIfFails) { 130 assertRegionPixelsOfColor(failMessagePrefix, view, 131 new Rect(0, 0, view.getWidth(), view.getHeight()), 132 color, allowedComponentVariance, throwExceptionIfFails); 133 } 134 135 /** 136 * Checks whether all the pixels in the specific rectangular region of the {@link View} are 137 * filled with the specific color. 138 * 139 * In case there is a color mismatch, the behavior of this method depends on the 140 * <code>throwExceptionIfFails</code> parameter. If it is <code>true</code>, this method will 141 * throw an <code>Exception</code> describing the mismatch. Otherwise this method will call 142 * <code>Assert.fail</code> with detailed description of the mismatch. 143 */ assertRegionPixelsOfColor(String failMessagePrefix, @NonNull View view, @NonNull Rect region, @ColorInt int color, int allowedComponentVariance, boolean throwExceptionIfFails)144 public static void assertRegionPixelsOfColor(String failMessagePrefix, @NonNull View view, 145 @NonNull Rect region, @ColorInt int color, int allowedComponentVariance, 146 boolean throwExceptionIfFails) { 147 // Create a bitmap 148 final int viewWidth = view.getWidth(); 149 final int viewHeight = view.getHeight(); 150 Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), 151 Bitmap.Config.ARGB_8888); 152 // Create a canvas that wraps the bitmap 153 Canvas canvas = new Canvas(bitmap); 154 // And ask the view to draw itself to the canvas / bitmap 155 view.draw(canvas); 156 157 try { 158 assertAllPixelsOfColor(failMessagePrefix, bitmap, region, 159 color, allowedComponentVariance, throwExceptionIfFails); 160 } finally { 161 bitmap.recycle(); 162 } 163 } 164 165 /** 166 * Checks whether all the pixels in the specified {@link Drawable} are filled with the specific 167 * color. 168 * 169 * In case there is a color mismatch, the behavior of this method depends on the 170 * <code>throwExceptionIfFails</code> parameter. If it is <code>true</code>, this method will 171 * throw an <code>Exception</code> describing the mismatch. Otherwise this method will call 172 * <code>Assert.fail</code> with detailed description of the mismatch. 173 */ assertAllPixelsOfColor(String failMessagePrefix, @NonNull Drawable drawable, int drawableWidth, int drawableHeight, boolean callSetBounds, @ColorInt int color, int allowedComponentVariance, boolean throwExceptionIfFails)174 public static void assertAllPixelsOfColor(String failMessagePrefix, @NonNull Drawable drawable, 175 int drawableWidth, int drawableHeight, boolean callSetBounds, @ColorInt int color, 176 int allowedComponentVariance, boolean throwExceptionIfFails) { 177 // Create a bitmap 178 Bitmap bitmap = Bitmap.createBitmap(drawableWidth, drawableHeight, 179 Bitmap.Config.ARGB_8888); 180 // Create a canvas that wraps the bitmap 181 Canvas canvas = new Canvas(bitmap); 182 if (callSetBounds) { 183 // Configure the drawable to have bounds that match the passed size 184 drawable.setBounds(0, 0, drawableWidth, drawableHeight); 185 } else { 186 // Query the current bounds of the drawable for translation 187 Rect drawableBounds = drawable.getBounds(); 188 canvas.translate(-drawableBounds.left, -drawableBounds.top); 189 } 190 // And ask the drawable to draw itself to the canvas / bitmap 191 drawable.draw(canvas); 192 193 try { 194 assertAllPixelsOfColor(failMessagePrefix, bitmap, 195 new Rect(0, 0, drawableWidth, drawableHeight), color, 196 allowedComponentVariance, throwExceptionIfFails); 197 } finally { 198 bitmap.recycle(); 199 } 200 } 201 202 /** 203 * Checks whether all the pixels in the specific rectangular region of the bitmap are filled 204 * with the specific color. 205 * 206 * In case there is a color mismatch, the behavior of this method depends on the 207 * <code>throwExceptionIfFails</code> parameter. If it is <code>true</code>, this method will 208 * throw an <code>Exception</code> describing the mismatch. Otherwise this method will call 209 * <code>Assert.fail</code> with detailed description of the mismatch. 210 */ assertAllPixelsOfColor(String failMessagePrefix, @NonNull Bitmap bitmap, @NonNull Rect region, @ColorInt int color, int allowedComponentVariance, boolean throwExceptionIfFails)211 private static void assertAllPixelsOfColor(String failMessagePrefix, @NonNull Bitmap bitmap, 212 @NonNull Rect region, @ColorInt int color, int allowedComponentVariance, 213 boolean throwExceptionIfFails) { 214 final int bitmapWidth = bitmap.getWidth(); 215 final int bitmapHeight = bitmap.getHeight(); 216 final int[] rowPixels = new int[bitmapWidth]; 217 218 final int startRow = region.top; 219 final int endRow = region.bottom; 220 final int startColumn = region.left; 221 final int endColumn = region.right; 222 223 for (int row = startRow; row < endRow; row++) { 224 bitmap.getPixels(rowPixels, 0, bitmapWidth, 0, row, bitmapWidth, 1); 225 for (int column = startColumn; column < endColumn; column++) { 226 @ColorInt int colorAtCurrPixel = rowPixels[column]; 227 if (!areColorsTheSameWithTolerance(color, colorAtCurrPixel, 228 allowedComponentVariance)) { 229 String mismatchDescription = failMessagePrefix 230 + ": expected all bitmap colors in rectangle [l=" 231 + startColumn + ", t=" + startRow + ", r=" + endColumn 232 + ", b=" + endRow + "] to be " + formatColorToHex(color) 233 + " but at position (" + row + "," + column + ") out of (" 234 + bitmapWidth + "," + bitmapHeight + ") found " 235 + formatColorToHex(colorAtCurrPixel); 236 if (throwExceptionIfFails) { 237 throw new RuntimeException(mismatchDescription); 238 } else { 239 Assert.fail(mismatchDescription); 240 } 241 } 242 } 243 } 244 } 245 246 /** 247 * Checks whether the center pixel in the specified bitmap is of the same specified color. 248 * 249 * In case there is a color mismatch, the behavior of this method depends on the 250 * <code>throwExceptionIfFails</code> parameter. If it is <code>true</code>, this method will 251 * throw an <code>Exception</code> describing the mismatch. Otherwise this method will call 252 * <code>Assert.fail</code> with detailed description of the mismatch. 253 */ assertCenterPixelOfColor(String failMessagePrefix, @NonNull Bitmap bitmap, @ColorInt int color, int allowedComponentVariance, boolean throwExceptionIfFails)254 public static void assertCenterPixelOfColor(String failMessagePrefix, @NonNull Bitmap bitmap, 255 @ColorInt int color, 256 int allowedComponentVariance, boolean throwExceptionIfFails) { 257 final int centerX = bitmap.getWidth() / 2; 258 final int centerY = bitmap.getHeight() / 2; 259 final @ColorInt int colorAtCenterPixel = bitmap.getPixel(centerX, centerY); 260 if (!areColorsTheSameWithTolerance(color, colorAtCenterPixel, 261 allowedComponentVariance)) { 262 String mismatchDescription = failMessagePrefix 263 + ": expected all drawable colors to be " 264 + formatColorToHex(color) 265 + " but at position (" + centerX + "," + centerY + ") out of (" 266 + bitmap.getWidth() + "," + bitmap.getHeight() + ") found" 267 + formatColorToHex(colorAtCenterPixel); 268 if (throwExceptionIfFails) { 269 throw new RuntimeException(mismatchDescription); 270 } else { 271 Assert.fail(mismatchDescription); 272 } 273 } 274 } 275 276 /** 277 * Formats the passed integer-packed color into the #AARRGGBB format. 278 */ formatColorToHex(@olorInt int color)279 public static String formatColorToHex(@ColorInt int color) { 280 return String.format("#%08X", (0xFFFFFFFF & color)); 281 } 282 283 /** 284 * Compares two integer-packed colors to be equal, each component within the specified 285 * allowed variance. Returns <code>true</code> if the two colors are sufficiently equal 286 * and <code>false</code> otherwise. 287 */ areColorsTheSameWithTolerance(@olorInt int expectedColor, @ColorInt int actualColor, int allowedComponentVariance)288 private static boolean areColorsTheSameWithTolerance(@ColorInt int expectedColor, 289 @ColorInt int actualColor, int allowedComponentVariance) { 290 int sourceAlpha = Color.alpha(actualColor); 291 int sourceRed = Color.red(actualColor); 292 int sourceGreen = Color.green(actualColor); 293 int sourceBlue = Color.blue(actualColor); 294 295 int expectedAlpha = Color.alpha(expectedColor); 296 int expectedRed = Color.red(expectedColor); 297 int expectedGreen = Color.green(expectedColor); 298 int expectedBlue = Color.blue(expectedColor); 299 300 int varianceAlpha = Math.abs(sourceAlpha - expectedAlpha); 301 int varianceRed = Math.abs(sourceRed - expectedRed); 302 int varianceGreen = Math.abs(sourceGreen - expectedGreen); 303 int varianceBlue = Math.abs(sourceBlue - expectedBlue); 304 305 boolean isColorMatch = (varianceAlpha <= allowedComponentVariance) 306 && (varianceRed <= allowedComponentVariance) 307 && (varianceGreen <= allowedComponentVariance) 308 && (varianceBlue <= allowedComponentVariance); 309 310 return isColorMatch; 311 } 312 313 /** 314 * Composite two potentially translucent colors over each other and returns the result. 315 */ compositeColors(@olorInt int foreground, @ColorInt int background)316 public static int compositeColors(@ColorInt int foreground, @ColorInt int background) { 317 int bgAlpha = Color.alpha(background); 318 int fgAlpha = Color.alpha(foreground); 319 int a = compositeAlpha(fgAlpha, bgAlpha); 320 321 int r = compositeComponent(Color.red(foreground), fgAlpha, 322 Color.red(background), bgAlpha, a); 323 int g = compositeComponent(Color.green(foreground), fgAlpha, 324 Color.green(background), bgAlpha, a); 325 int b = compositeComponent(Color.blue(foreground), fgAlpha, 326 Color.blue(background), bgAlpha, a); 327 328 return Color.argb(a, r, g, b); 329 } 330 compositeAlpha(int foregroundAlpha, int backgroundAlpha)331 private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) { 332 return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF); 333 } 334 compositeComponent(int fgC, int fgA, int bgC, int bgA, int a)335 private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) { 336 if (a == 0) return 0; 337 return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF); 338 } 339 colorStateListOf(final @ColorInt int color)340 public static ColorStateList colorStateListOf(final @ColorInt int color) { 341 return argThat(new BaseMatcher<ColorStateList>() { 342 @Override 343 public boolean matches(Object o) { 344 if (o instanceof ColorStateList) { 345 final ColorStateList actual = (ColorStateList) o; 346 return (actual.getColors().length == 1) && (actual.getDefaultColor() == color); 347 } 348 return false; 349 } 350 351 @Override 352 public void describeTo(Description description) { 353 description.appendText("doesn't match " + formatColorToHex(color)); 354 } 355 }); 356 } 357 358 public static int dpToPx(Context context, int dp) { 359 final float density = context.getResources().getDisplayMetrics().density; 360 return (int) (dp * density + 0.5f); 361 } 362 363 private static String arrayToString(final long[] array) { 364 final StringBuffer buffer = new StringBuffer(); 365 if (array == null) { 366 buffer.append("null"); 367 } else { 368 buffer.append("["); 369 for (int i = 0; i < array.length; i++) { 370 if (i > 0) { 371 buffer.append(", "); 372 } 373 buffer.append(array[i]); 374 } 375 buffer.append("]"); 376 } 377 return buffer.toString(); 378 } 379 380 public static void assertIdentical(final long[] expected, final long[] actual) { 381 if (!Arrays.equals(expected, actual)) { 382 Assert.fail("Expected " + arrayToString(expected) + ", actual " 383 + arrayToString(actual)); 384 } 385 } 386 387 public static void assertTrueValuesAtPositions(final long[] expectedIndexesForTrueValues, 388 final SparseBooleanArray array) { 389 if (array == null) { 390 if ((expectedIndexesForTrueValues != null) 391 && (expectedIndexesForTrueValues.length > 0)) { 392 Assert.fail("Expected " + arrayToString(expectedIndexesForTrueValues) 393 + ", actual [null]"); 394 } 395 return; 396 } 397 398 final int totalValuesCount = array.size(); 399 // "Convert" the input array into a long[] array that has indexes of true values 400 int trueValuesCount = 0; 401 for (int i = 0; i < totalValuesCount; i++) { 402 if (array.valueAt(i)) { 403 trueValuesCount++; 404 } 405 } 406 407 final long[] trueValuePositions = new long[trueValuesCount]; 408 int position = 0; 409 for (int i = 0; i < totalValuesCount; i++) { 410 if (array.valueAt(i)) { 411 trueValuePositions[position++] = array.keyAt(i); 412 } 413 } 414 415 Arrays.sort(trueValuePositions); 416 assertIdentical(expectedIndexesForTrueValues, trueValuePositions); 417 } 418 419 public static Drawable getDrawable(Context context, @DrawableRes int resid) { 420 return context.getResources().getDrawable(resid); 421 } 422 423 public static Bitmap getBitmap(Context context, @DrawableRes int resid) { 424 return ((BitmapDrawable) getDrawable(context, resid)).getBitmap(); 425 } 426 427 public static void verifyCompoundDrawables(@NonNull TextView textView, 428 @DrawableRes int expectedLeftDrawableId, @DrawableRes int expectedRightDrawableId, 429 @DrawableRes int expectedTopDrawableId, @DrawableRes int expectedBottomDrawableId) { 430 final Context context = textView.getContext(); 431 final Drawable[] compoundDrawables = textView.getCompoundDrawables(); 432 if (expectedLeftDrawableId < 0) { 433 assertNull(compoundDrawables[0]); 434 } else { 435 WidgetTestUtils.assertEquals(getBitmap(context, expectedLeftDrawableId), 436 ((BitmapDrawable) compoundDrawables[0]).getBitmap()); 437 } 438 if (expectedTopDrawableId < 0) { 439 assertNull(compoundDrawables[1]); 440 } else { 441 WidgetTestUtils.assertEquals(getBitmap(context, expectedTopDrawableId), 442 ((BitmapDrawable) compoundDrawables[1]).getBitmap()); 443 } 444 if (expectedRightDrawableId < 0) { 445 assertNull(compoundDrawables[2]); 446 } else { 447 WidgetTestUtils.assertEquals(getBitmap(context, expectedRightDrawableId), 448 ((BitmapDrawable) compoundDrawables[2]).getBitmap()); 449 } 450 if (expectedBottomDrawableId < 0) { 451 assertNull(compoundDrawables[3]); 452 } else { 453 WidgetTestUtils.assertEquals(getBitmap(context, expectedBottomDrawableId), 454 ((BitmapDrawable) compoundDrawables[3]).getBitmap()); 455 } 456 } 457 458 public static void verifyCompoundDrawablesRelative(@NonNull TextView textView, 459 @DrawableRes int expectedStartDrawableId, @DrawableRes int expectedEndDrawableId, 460 @DrawableRes int expectedTopDrawableId, @DrawableRes int expectedBottomDrawableId) { 461 final Context context = textView.getContext(); 462 final Drawable[] compoundDrawablesRelative = textView.getCompoundDrawablesRelative(); 463 if (expectedStartDrawableId < 0) { 464 assertNull(compoundDrawablesRelative[0]); 465 } else { 466 WidgetTestUtils.assertEquals(getBitmap(context, expectedStartDrawableId), 467 ((BitmapDrawable) compoundDrawablesRelative[0]).getBitmap()); 468 } 469 if (expectedTopDrawableId < 0) { 470 assertNull(compoundDrawablesRelative[1]); 471 } else { 472 WidgetTestUtils.assertEquals(getBitmap(context, expectedTopDrawableId), 473 ((BitmapDrawable) compoundDrawablesRelative[1]).getBitmap()); 474 } 475 if (expectedEndDrawableId < 0) { 476 assertNull(compoundDrawablesRelative[2]); 477 } else { 478 WidgetTestUtils.assertEquals(getBitmap(context, expectedEndDrawableId), 479 ((BitmapDrawable) compoundDrawablesRelative[2]).getBitmap()); 480 } 481 if (expectedBottomDrawableId < 0) { 482 assertNull(compoundDrawablesRelative[3]); 483 } else { 484 WidgetTestUtils.assertEquals(getBitmap(context, expectedBottomDrawableId), 485 ((BitmapDrawable) compoundDrawablesRelative[3]).getBitmap()); 486 } 487 } 488 489 } 490