1/* 2 * Copyright (C) 2015 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 17package android.support.v7.testutils; 18 19import android.app.Instrumentation; 20import android.content.Context; 21import android.graphics.Bitmap; 22import android.graphics.Canvas; 23import android.graphics.Color; 24import android.graphics.drawable.Drawable; 25import android.os.SystemClock; 26import android.support.annotation.ColorInt; 27import android.support.annotation.NonNull; 28import android.support.v4.util.Pair; 29import android.support.v7.widget.TintTypedArray; 30import android.view.InputDevice; 31import android.view.MotionEvent; 32import android.view.View; 33import android.view.ViewConfiguration; 34import android.view.ViewParent; 35 36import junit.framework.Assert; 37 38import java.util.ArrayList; 39import java.util.List; 40 41public class TestUtils { 42 /** 43 * This method takes a view and returns a single bitmap that is the layered combination 44 * of background drawables of this view and all its ancestors. It can be used to abstract 45 * away the specific implementation of a view hierarchy that is not exposed via class APIs 46 * or a view hierarchy that depends on the platform version. Instead of hard-coded lookups 47 * of particular inner implementations of such a view hierarchy that can break during 48 * refactoring or on newer platform versions, calling this API returns a "combined" background 49 * of the view. 50 * 51 * For example, it is useful to get the combined background of a popup / dropdown without 52 * delving into the inner implementation details of how that popup is implemented on a 53 * particular platform version. 54 */ 55 public static Bitmap getCombinedBackgroundBitmap(View view) { 56 final int bitmapWidth = view.getWidth(); 57 final int bitmapHeight = view.getHeight(); 58 59 // Create a bitmap 60 final Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, 61 Bitmap.Config.ARGB_8888); 62 // Create a canvas that wraps the bitmap 63 final Canvas canvas = new Canvas(bitmap); 64 65 // As the draw pass starts at the top of view hierarchy, our first step is to traverse 66 // the ancestor hierarchy of our view and collect a list of all ancestors with non-null 67 // and visible backgrounds. At each step we're keeping track of the combined offsets 68 // so that we can properly combine all of the visuals together in the next pass. 69 List<View> ancestorsWithBackgrounds = new ArrayList<>(); 70 List<Pair<Integer, Integer>> ancestorOffsets = new ArrayList<>(); 71 int offsetX = 0; 72 int offsetY = 0; 73 while (true) { 74 final Drawable backgroundDrawable = view.getBackground(); 75 if ((backgroundDrawable != null) && backgroundDrawable.isVisible()) { 76 ancestorsWithBackgrounds.add(view); 77 ancestorOffsets.add(Pair.create(offsetX, offsetY)); 78 } 79 // Go to the parent 80 ViewParent parent = view.getParent(); 81 if (!(parent instanceof View)) { 82 // We're done traversing the ancestor chain 83 break; 84 } 85 86 // Update the offsets based on the location of current view in its parent's bounds 87 offsetX += view.getLeft(); 88 offsetY += view.getTop(); 89 90 view = (View) parent; 91 } 92 93 // Now we're going to iterate over the collected ancestors in reverse order (starting from 94 // the topmost ancestor) and draw their backgrounds into our combined bitmap. At each step 95 // we are respecting the offsets of our original view in the coordinate system of the 96 // currently drawn ancestor. 97 final int layerCount = ancestorsWithBackgrounds.size(); 98 for (int i = layerCount - 1; i >= 0; i--) { 99 View ancestor = ancestorsWithBackgrounds.get(i); 100 Pair<Integer, Integer> offsets = ancestorOffsets.get(i); 101 102 canvas.translate(offsets.first, offsets.second); 103 ancestor.getBackground().draw(canvas); 104 canvas.translate(-offsets.first, -offsets.second); 105 } 106 107 return bitmap; 108 } 109 110 /** 111 * Checks whether all the pixels in the specified drawable are of the same specified color. 112 * 113 * In case there is a color mismatch, the behavior of this method depends on the 114 * <code>throwExceptionIfFails</code> parameter. If it is <code>true</code>, this method will 115 * throw an <code>Exception</code> describing the mismatch. Otherwise this method will call 116 * <code>Assert.fail</code> with detailed description of the mismatch. 117 */ 118 public static void assertAllPixelsOfColor(String failMessagePrefix, @NonNull Drawable drawable, 119 int drawableWidth, int drawableHeight, boolean callSetBounds, @ColorInt int color, 120 int allowedComponentVariance, boolean throwExceptionIfFails) { 121 // Create a bitmap 122 Bitmap bitmap = Bitmap.createBitmap(drawableWidth, drawableHeight, 123 Bitmap.Config.ARGB_8888); 124 // Create a canvas that wraps the bitmap 125 Canvas canvas = new Canvas(bitmap); 126 if (callSetBounds) { 127 // Configure the drawable to have bounds that match the passed size 128 drawable.setBounds(0, 0, drawableWidth, drawableHeight); 129 } 130 // And ask the drawable to draw itself to the canvas / bitmap 131 drawable.draw(canvas); 132 133 try { 134 assertAllPixelsOfColor(failMessagePrefix, bitmap, drawableWidth, drawableHeight, color, 135 allowedComponentVariance, throwExceptionIfFails); 136 } finally { 137 bitmap.recycle(); 138 } 139 } 140 141 /** 142 * Checks whether all the pixels in the specified bitmap are of the same specified color. 143 * 144 * In case there is a color mismatch, the behavior of this method depends on the 145 * <code>throwExceptionIfFails</code> parameter. If it is <code>true</code>, this method will 146 * throw an <code>Exception</code> describing the mismatch. Otherwise this method will call 147 * <code>Assert.fail</code> with detailed description of the mismatch. 148 */ 149 public static void assertAllPixelsOfColor(String failMessagePrefix, @NonNull Bitmap bitmap, 150 int bitmapWidth, int bitmapHeight, @ColorInt int color, 151 int allowedComponentVariance, boolean throwExceptionIfFails) { 152 int[] rowPixels = new int[bitmapWidth]; 153 for (int row = 0; row < bitmapHeight; row++) { 154 bitmap.getPixels(rowPixels, 0, bitmapWidth, 0, row, bitmapWidth, 1); 155 for (int column = 0; column < bitmapWidth; column++) { 156 @ColorInt int colorAtCurrPixel = rowPixels[column]; 157 if (!areColorsTheSameWithTolerance(color, colorAtCurrPixel, 158 allowedComponentVariance)) { 159 String mismatchDescription = failMessagePrefix 160 + ": expected all drawable colors to be " 161 + formatColorToHex(color) 162 + " but at position (" + row + "," + column + ") out of (" 163 + bitmapWidth + "," + bitmapHeight + ") found " 164 + formatColorToHex(colorAtCurrPixel); 165 if (throwExceptionIfFails) { 166 throw new RuntimeException(mismatchDescription); 167 } else { 168 Assert.fail(mismatchDescription); 169 } 170 } 171 } 172 } 173 } 174 175 /** 176 * Checks whether the center pixel in the specified drawable is of the same specified color. 177 * 178 * In case there is a color mismatch, the behavior of this method depends on the 179 * <code>throwExceptionIfFails</code> parameter. If it is <code>true</code>, this method will 180 * throw an <code>Exception</code> describing the mismatch. Otherwise this method will call 181 * <code>Assert.fail</code> with detailed description of the mismatch. 182 */ 183 public static void assertCenterPixelOfColor(String failMessagePrefix, @NonNull Drawable drawable, 184 int drawableWidth, int drawableHeight, boolean callSetBounds, @ColorInt int color, 185 int allowedComponentVariance, boolean throwExceptionIfFails) { 186 // Create a bitmap 187 Bitmap bitmap = Bitmap.createBitmap(drawableWidth, drawableHeight, Bitmap.Config.ARGB_8888); 188 // Create a canvas that wraps the bitmap 189 Canvas canvas = new Canvas(bitmap); 190 if (callSetBounds) { 191 // Configure the drawable to have bounds that match the passed size 192 drawable.setBounds(0, 0, drawableWidth, drawableHeight); 193 } 194 // And ask the drawable to draw itself to the canvas / bitmap 195 drawable.draw(canvas); 196 197 try { 198 assertCenterPixelOfColor(failMessagePrefix, bitmap, color, allowedComponentVariance, 199 throwExceptionIfFails); 200 } finally { 201 bitmap.recycle(); 202 } 203 } 204 205 /** 206 * Checks whether the center pixel in the specified bitmap is of the same specified color. 207 * 208 * In case there is a color mismatch, the behavior of this method depends on the 209 * <code>throwExceptionIfFails</code> parameter. If it is <code>true</code>, this method will 210 * throw an <code>Exception</code> describing the mismatch. Otherwise this method will call 211 * <code>Assert.fail</code> with detailed description of the mismatch. 212 */ 213 public static void assertCenterPixelOfColor(String failMessagePrefix, @NonNull Bitmap bitmap, 214 @ColorInt int color, int allowedComponentVariance, boolean throwExceptionIfFails) { 215 final int centerX = bitmap.getWidth() / 2; 216 final int centerY = bitmap.getHeight() / 2; 217 final @ColorInt int colorAtCenterPixel = bitmap.getPixel(centerX, centerY); 218 if (!areColorsTheSameWithTolerance(color, colorAtCenterPixel, 219 allowedComponentVariance)) { 220 String mismatchDescription = failMessagePrefix 221 + ": expected all drawable colors to be " 222 + formatColorToHex(color) 223 + " but at position (" + centerX + "," + centerY + ") out of (" 224 + bitmap.getWidth() + "," + bitmap.getHeight() + ") found" 225 + formatColorToHex(colorAtCenterPixel); 226 if (throwExceptionIfFails) { 227 throw new RuntimeException(mismatchDescription); 228 } else { 229 Assert.fail(mismatchDescription); 230 } 231 } 232 } 233 234 /** 235 * Formats the passed integer-packed color into the #AARRGGBB format. 236 */ 237 private static String formatColorToHex(@ColorInt int color) { 238 return String.format("#%08X", (0xFFFFFFFF & color)); 239 } 240 241 /** 242 * Compares two integer-packed colors to be equal, each component within the specified 243 * allowed variance. Returns <code>true</code> if the two colors are sufficiently equal 244 * and <code>false</code> otherwise. 245 */ 246 private static boolean areColorsTheSameWithTolerance(@ColorInt int expectedColor, 247 @ColorInt int actualColor, int allowedComponentVariance) { 248 int sourceAlpha = Color.alpha(actualColor); 249 int sourceRed = Color.red(actualColor); 250 int sourceGreen = Color.green(actualColor); 251 int sourceBlue = Color.blue(actualColor); 252 253 int expectedAlpha = Color.alpha(expectedColor); 254 int expectedRed = Color.red(expectedColor); 255 int expectedGreen = Color.green(expectedColor); 256 int expectedBlue = Color.blue(expectedColor); 257 258 int varianceAlpha = Math.abs(sourceAlpha - expectedAlpha); 259 int varianceRed = Math.abs(sourceRed - expectedRed); 260 int varianceGreen = Math.abs(sourceGreen - expectedGreen); 261 int varianceBlue = Math.abs(sourceBlue - expectedBlue); 262 263 boolean isColorMatch = (varianceAlpha <= allowedComponentVariance) 264 && (varianceRed <= allowedComponentVariance) 265 && (varianceGreen <= allowedComponentVariance) 266 && (varianceBlue <= allowedComponentVariance); 267 268 return isColorMatch; 269 } 270 271 public static void waitForActivityDestroyed(BaseTestActivity activity) { 272 while (!activity.isDestroyed()) { 273 SystemClock.sleep(30); 274 } 275 } 276 277 public static int getThemeAttrColor(Context context, int attr) { 278 final int[] attrs = { attr }; 279 TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, null, attrs); 280 try { 281 return a.getColor(0, 0); 282 } finally { 283 a.recycle(); 284 } 285 } 286 287 /** 288 * Emulates a tap on a point relative to the top-left corner of the passed {@link View}. Offset 289 * parameters are used to compute the final screen coordinates of the tap point. 290 * 291 * @param instrumentation the instrumentation used to run the test 292 * @param anchorView the anchor view to determine the tap location on the screen 293 * @param offsetX extra X offset for the tap 294 * @param offsetY extra Y offset for the tap 295 */ 296 public static void emulateTapOnView(Instrumentation instrumentation, View anchorView, 297 int offsetX, int offsetY) { 298 final int touchSlop = ViewConfiguration.get(anchorView.getContext()).getScaledTouchSlop(); 299 // Get anchor coordinates on the screen 300 final int[] viewOnScreenXY = new int[2]; 301 anchorView.getLocationOnScreen(viewOnScreenXY); 302 int xOnScreen = viewOnScreenXY[0] + offsetX; 303 int yOnScreen = viewOnScreenXY[1] + offsetY; 304 final long downTime = SystemClock.uptimeMillis(); 305 306 injectDownEvent(instrumentation, downTime, xOnScreen, yOnScreen); 307 injectMoveEventForTap(instrumentation, downTime, touchSlop, xOnScreen, yOnScreen); 308 injectUpEvent(instrumentation, downTime, false, xOnScreen, yOnScreen); 309 310 // Wait for the system to process all events in the queue 311 instrumentation.waitForIdleSync(); 312 } 313 314 private static long injectDownEvent(Instrumentation instrumentation, long downTime, 315 int xOnScreen, int yOnScreen) { 316 MotionEvent eventDown = MotionEvent.obtain( 317 downTime, downTime, MotionEvent.ACTION_DOWN, xOnScreen, yOnScreen, 1); 318 eventDown.setSource(InputDevice.SOURCE_TOUCHSCREEN); 319 instrumentation.sendPointerSync(eventDown); 320 eventDown.recycle(); 321 return downTime; 322 } 323 324 private static void injectMoveEventForTap(Instrumentation instrumentation, long downTime, 325 int touchSlop, int xOnScreen, int yOnScreen) { 326 MotionEvent eventMove = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_MOVE, 327 xOnScreen + (touchSlop / 2.0f), yOnScreen + (touchSlop / 2.0f), 1); 328 eventMove.setSource(InputDevice.SOURCE_TOUCHSCREEN); 329 instrumentation.sendPointerSync(eventMove); 330 eventMove.recycle(); 331 } 332 333 334 private static void injectUpEvent(Instrumentation instrumentation, long downTime, 335 boolean useCurrentEventTime, int xOnScreen, int yOnScreen) { 336 long eventTime = useCurrentEventTime ? SystemClock.uptimeMillis() : downTime; 337 MotionEvent eventUp = MotionEvent.obtain( 338 downTime, eventTime, MotionEvent.ACTION_UP, xOnScreen, yOnScreen, 1); 339 eventUp.setSource(InputDevice.SOURCE_TOUCHSCREEN); 340 instrumentation.sendPointerSync(eventUp); 341 eventUp.recycle(); 342 } 343}