Home | History | Annotate | Download | only in util
      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      */
     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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    331     private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
    332         return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
    333     }
    334 
    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 
    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