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