1 package org.robolectric.shadows; 2 3 import static java.nio.charset.StandardCharsets.UTF_8; 4 import static org.robolectric.Shadows.shadowOf; 5 6 import android.graphics.Bitmap; 7 import android.graphics.Matrix; 8 import android.graphics.RectF; 9 import android.os.Parcel; 10 import android.util.DisplayMetrics; 11 import java.io.FileDescriptor; 12 import java.io.IOException; 13 import java.io.InputStream; 14 import java.io.OutputStream; 15 import java.nio.Buffer; 16 import java.nio.ByteBuffer; 17 import org.robolectric.annotation.Implementation; 18 import org.robolectric.annotation.Implements; 19 import org.robolectric.annotation.RealObject; 20 import org.robolectric.util.ReflectionHelpers; 21 22 @SuppressWarnings({"UnusedDeclaration"}) 23 @Implements(Bitmap.class) 24 public class ShadowBitmap { 25 /** Number of bytes used internally to represent each pixel (in the {@link #colors} array) */ 26 private static final int INTERNAL_BYTES_PER_PIXEL = 4; 27 28 @RealObject 29 private Bitmap realBitmap; 30 31 int createdFromResId = -1; 32 String createdFromPath; 33 InputStream createdFromStream; 34 FileDescriptor createdFromFileDescriptor; 35 byte[] createdFromBytes; 36 private Bitmap createdFromBitmap; 37 private int createdFromX = -1; 38 private int createdFromY = -1; 39 private int createdFromWidth = -1; 40 private int createdFromHeight = -1; 41 private int[] createdFromColors; 42 private Matrix createdFromMatrix; 43 private boolean createdFromFilter; 44 private boolean hasAlpha; 45 46 private int width; 47 private int height; 48 private int density; 49 private int[] colors; 50 private Bitmap.Config config; 51 private boolean mutable; 52 private String description = ""; 53 private boolean recycled = false; 54 private boolean hasMipMap; 55 56 /** 57 * Returns a textual representation of the appearance of the object. 58 * 59 * @param bitmap the bitmap to visualize 60 * @return Textual representation of the appearance of the object. 61 */ 62 public static String visualize(Bitmap bitmap) { 63 return shadowOf(bitmap).getDescription(); 64 } 65 66 /** 67 * Reference to original Bitmap from which this Bitmap was created. {@code null} if this Bitmap 68 * was not copied from another instance. 69 * 70 * @return Original Bitmap from which this Bitmap was created. 71 */ 72 public Bitmap getCreatedFromBitmap() { 73 return createdFromBitmap; 74 } 75 76 /** 77 * Resource ID from which this Bitmap was created. {@code 0} if this Bitmap was not created 78 * from a resource. 79 * 80 * @return Resource ID from which this Bitmap was created. 81 */ 82 public int getCreatedFromResId() { 83 return createdFromResId; 84 } 85 86 /** 87 * Path from which this Bitmap was created. {@code null} if this Bitmap was not create from a 88 * path. 89 * 90 * @return Path from which this Bitmap was created. 91 */ 92 public String getCreatedFromPath() { 93 return createdFromPath; 94 } 95 96 /** 97 * {@link InputStream} from which this Bitmap was created. {@code null} if this Bitmap was not 98 * created from a stream. 99 * 100 * @return InputStream from which this Bitmap was created. 101 */ 102 public InputStream getCreatedFromStream() { 103 return createdFromStream; 104 } 105 106 /** 107 * Bytes from which this Bitmap was created. {@code null} if this Bitmap was not created from 108 * bytes. 109 * 110 * @return Bytes from which this Bitmap was created. 111 */ 112 public byte[] getCreatedFromBytes() { 113 return createdFromBytes; 114 } 115 116 /** 117 * Horizontal offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1. 118 * 119 * @return Horizontal offset within {@link #getCreatedFromBitmap()}. 120 */ 121 public int getCreatedFromX() { 122 return createdFromX; 123 } 124 125 /** 126 * Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1. 127 * 128 * @return Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1. 129 */ 130 public int getCreatedFromY() { 131 return createdFromY; 132 } 133 134 /** 135 * Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's 136 * content, or -1. 137 * 138 * @return Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's 139 * content, or -1. 140 */ 141 public int getCreatedFromWidth() { 142 return createdFromWidth; 143 } 144 145 /** 146 * Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's 147 * content, or -1. 148 * @return Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's 149 * content, or -1. 150 */ 151 public int getCreatedFromHeight() { 152 return createdFromHeight; 153 } 154 155 /** 156 * Color array from which this Bitmap was created. {@code null} if this Bitmap was not created 157 * from a color array. 158 * @return Color array from which this Bitmap was created. 159 */ 160 public int[] getCreatedFromColors() { 161 return createdFromColors; 162 } 163 164 /** 165 * Matrix from which this Bitmap's content was transformed, or {@code null}. 166 * @return Matrix from which this Bitmap's content was transformed, or {@code null}. 167 */ 168 public Matrix getCreatedFromMatrix() { 169 return createdFromMatrix; 170 } 171 172 /** 173 * {@code true} if this Bitmap was created with filtering. 174 * @return {@code true} if this Bitmap was created with filtering. 175 */ 176 public boolean getCreatedFromFilter() { 177 return createdFromFilter; 178 } 179 180 @Implementation 181 public boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) { 182 try { 183 stream.write((description + " compressed as " + format + " with quality " + quality).getBytes(UTF_8)); 184 } catch (IOException e) { 185 throw new RuntimeException(e); 186 } 187 188 return true; 189 } 190 191 @Implementation 192 public static Bitmap createBitmap(int width, int height, Bitmap.Config config) { 193 return createBitmap((DisplayMetrics) null, width, height, config); 194 } 195 196 @Implementation 197 public static Bitmap createBitmap(DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config, boolean hasAlpha) { 198 return createBitmap((DisplayMetrics) null, width, height, config); 199 } 200 201 @Implementation 202 public static Bitmap createBitmap(DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config) { 203 if (width <= 0 || height <= 0) { 204 throw new IllegalArgumentException("width and height must be > 0"); 205 } 206 Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 207 ShadowBitmap shadowBitmap = shadowOf(scaledBitmap); 208 shadowBitmap.setDescription("Bitmap (" + width + " x " + height + ")"); 209 210 shadowBitmap.width = width; 211 shadowBitmap.height = height; 212 shadowBitmap.config = config; 213 shadowBitmap.setMutable(true); 214 if (displayMetrics != null) { 215 shadowBitmap.density = displayMetrics.densityDpi; 216 } 217 shadowBitmap.setPixels(new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()], 0, shadowBitmap.getWidth(), 0, 0, shadowBitmap.getWidth(), shadowBitmap.getHeight()); 218 return scaledBitmap; 219 } 220 221 @Implementation 222 public static Bitmap createBitmap(Bitmap src) { 223 ShadowBitmap shadowBitmap = shadowOf(src); 224 shadowBitmap.appendDescription(" created from Bitmap object"); 225 return src; 226 } 227 228 @Implementation 229 public static Bitmap createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter) { 230 if (dstWidth == src.getWidth() && dstHeight == src.getHeight() && !filter) { 231 return src; // Return the original. 232 } 233 234 Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 235 ShadowBitmap shadowBitmap = shadowOf(scaledBitmap); 236 237 shadowBitmap.appendDescription(shadowOf(src).getDescription()); 238 shadowBitmap.appendDescription(" scaled to " + dstWidth + " x " + dstHeight); 239 if (filter) { 240 shadowBitmap.appendDescription(" with filter " + filter); 241 } 242 243 shadowBitmap.createdFromBitmap = src; 244 shadowBitmap.createdFromFilter = filter; 245 shadowBitmap.width = dstWidth; 246 shadowBitmap.height = dstHeight; 247 shadowBitmap.setPixels(new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()], 0, 0, 0, 0, shadowBitmap.getWidth(), shadowBitmap.getHeight()); 248 return scaledBitmap; 249 } 250 251 @Implementation 252 public static Bitmap createBitmap(Bitmap src, int x, int y, int width, int height) { 253 if (x == 0 && y == 0 && width == src.getWidth() && height == src.getHeight()) { 254 return src; // Return the original. 255 } 256 257 Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 258 ShadowBitmap shadowBitmap = shadowOf(newBitmap); 259 260 shadowBitmap.appendDescription(shadowOf(src).getDescription()); 261 shadowBitmap.appendDescription(" at (" + x + "," + y); 262 shadowBitmap.appendDescription(" with width " + width + " and height " + height); 263 264 shadowBitmap.createdFromBitmap = src; 265 shadowBitmap.createdFromX = x; 266 shadowBitmap.createdFromY = y; 267 shadowBitmap.createdFromWidth = width; 268 shadowBitmap.createdFromHeight = height; 269 shadowBitmap.width = width; 270 shadowBitmap.height = height; 271 return newBitmap; 272 } 273 274 @Implementation 275 public void setPixels(int[] pixels, int offset, int stride, 276 int x, int y, int width, int height) { 277 this.colors = pixels; 278 } 279 280 @Implementation 281 public static Bitmap createBitmap(Bitmap src, int x, int y, int width, int height, Matrix matrix, boolean filter) { 282 if (x == 0 && y == 0 && width == src.getWidth() && height == src.getHeight() && (matrix == null || matrix.isIdentity())) { 283 return src; // Return the original. 284 } 285 286 if (x + width > src.getWidth()) { 287 throw new IllegalArgumentException("x + width must be <= bitmap.width()"); 288 } 289 if (y + height > src.getHeight()) { 290 throw new IllegalArgumentException("y + height must be <= bitmap.height()"); 291 } 292 293 Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 294 ShadowBitmap shadowBitmap = shadowOf(newBitmap); 295 296 shadowBitmap.appendDescription(shadowOf(src).getDescription()); 297 shadowBitmap.appendDescription(" at (" + x + "," + y + ")"); 298 shadowBitmap.appendDescription(" with width " + width + " and height " + height); 299 if (matrix != null) { 300 shadowBitmap.appendDescription(" using matrix " + shadowOf(matrix).getDescription()); 301 302 // Adjust width and height by using the matrix. 303 RectF mappedRect = new RectF(); 304 matrix.mapRect(mappedRect, new RectF(0, 0, width, height)); 305 width = Math.round(mappedRect.width()); 306 height = Math.round(mappedRect.height()); 307 } 308 if (filter) { 309 shadowBitmap.appendDescription(" with filter"); 310 } 311 312 shadowBitmap.createdFromBitmap = src; 313 shadowBitmap.createdFromX = x; 314 shadowBitmap.createdFromY = y; 315 shadowBitmap.createdFromWidth = width; 316 shadowBitmap.createdFromHeight = height; 317 shadowBitmap.createdFromMatrix = matrix; 318 shadowBitmap.createdFromFilter = filter; 319 shadowBitmap.width = width; 320 shadowBitmap.height = height; 321 return newBitmap; 322 } 323 324 @Implementation 325 public static Bitmap createBitmap(int[] colors, int width, int height, Bitmap.Config config) { 326 if (colors.length != width * height) { 327 throw new IllegalArgumentException("array length (" + colors.length + ") did not match width * height (" + (width * height) + ")"); 328 } 329 330 Bitmap newBitmap = Bitmap.createBitmap(width, height, config); 331 ShadowBitmap shadowBitmap = shadowOf(newBitmap); 332 333 shadowBitmap.setMutable(false); 334 shadowBitmap.createdFromColors = colors; 335 shadowBitmap.colors = new int[colors.length]; 336 System.arraycopy(colors, 0, shadowBitmap.colors, 0, colors.length); 337 return newBitmap; 338 } 339 340 @Implementation 341 public int getPixel(int x, int y) { 342 internalCheckPixelAccess(x, y); 343 if (colors != null) { 344 // Note that getPixel() returns a non-premultiplied ARGB value; if 345 // config is RGB_565, our return value will likely be more precise than 346 // on a physical device, since it needs to map each color component from 347 // 5 or 6 bits to 8 bits. 348 return colors[y * getWidth() + x]; 349 } else { 350 return 0; 351 } 352 } 353 354 @Implementation 355 public void setPixel(int x, int y, int color) { 356 if (isRecycled()) { 357 throw new IllegalStateException("Can't call setPixel() on a recycled bitmap"); 358 } else if (!isMutable()) { 359 throw new IllegalStateException("Bitmap is immutable"); 360 } 361 internalCheckPixelAccess(x, y); 362 if (colors == null) { 363 colors = new int[getWidth() * getHeight()]; 364 } 365 colors[y * getWidth() + x] = color; 366 } 367 368 /** 369 * Note that this method will return a RuntimeException unless: 370 * - {@code pixels} has the same length as the number of pixels of the bitmap. 371 * - {@code x = 0} 372 * - {@code y = 0} 373 * - {@code width} and {@code height} height match the current bitmap's dimensions. 374 */ 375 @Implementation 376 public void getPixels(int[] pixels, int offset, int stride, int x, int y, int width, int height) { 377 if (x != 0 || 378 y != 0 || 379 width != getWidth() || 380 height != getHeight() || 381 pixels.length != colors.length) { 382 for (int y0 = y; y0 < y + height; y0++) { 383 for (int x0 = x; x0 < x + width; x0++) { 384 pixels[offset + y0 * stride + x0] = colors[(y0 - y) * this.width + (x0 - x)]; 385 } 386 } 387 } else { 388 System.arraycopy(colors, 0, pixels, 0, colors.length); 389 } 390 } 391 392 @Implementation 393 public int getRowBytes() { 394 return getBytesPerPixel(config) * getWidth(); 395 } 396 397 @Implementation 398 public int getByteCount() { 399 return getRowBytes() * getHeight(); 400 } 401 402 @Implementation 403 public void recycle() { 404 recycled = true; 405 } 406 407 @Implementation 408 public final boolean isRecycled() { 409 return recycled; 410 } 411 412 @Implementation 413 public Bitmap copy(Bitmap.Config config, boolean isMutable) { 414 Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 415 ShadowBitmap shadowBitmap = shadowOf(newBitmap); 416 shadowBitmap.createdFromBitmap = realBitmap; 417 shadowBitmap.config = config; 418 shadowBitmap.mutable = isMutable; 419 return newBitmap; 420 } 421 422 @Implementation 423 public final Bitmap.Config getConfig() { 424 return config; 425 } 426 427 @Implementation 428 public void setConfig(Bitmap.Config config) { 429 this.config = config; 430 } 431 432 @Implementation 433 public final boolean isMutable() { 434 return mutable; 435 } 436 437 public void setMutable(boolean mutable) { 438 this.mutable = mutable; 439 } 440 441 public void appendDescription(String s) { 442 description += s; 443 } 444 445 public void setDescription(String s) { 446 description = s; 447 } 448 449 public String getDescription() { 450 return description; 451 } 452 453 @Implementation 454 public final boolean hasAlpha() { 455 return hasAlpha; 456 } 457 458 @Implementation 459 public void setHasAlpha(boolean hasAlpha) { 460 this.hasAlpha = hasAlpha; 461 } 462 463 @Implementation 464 public final boolean hasMipMap() { 465 return hasMipMap; 466 } 467 468 @Implementation 469 public final void setHasMipMap(boolean hasMipMap) { 470 this.hasMipMap = hasMipMap; 471 } 472 473 @Implementation 474 public void setWidth(int width) { 475 this.width = width; 476 } 477 478 @Implementation 479 public int getWidth() { 480 return width; 481 } 482 483 @Implementation 484 public void setHeight(int height) { 485 this.height = height; 486 } 487 488 @Implementation 489 public int getHeight() { 490 return height; 491 } 492 493 @Implementation 494 public void setDensity(int density) { 495 this.density = density; 496 } 497 498 @Implementation 499 public int getDensity() { 500 return density; 501 } 502 503 @Implementation 504 public int getGenerationId() { 505 return 0; 506 } 507 508 @Implementation 509 public Bitmap createAshmemBitmap() { 510 return realBitmap; 511 } 512 513 @Implementation 514 public void eraseColor(int c) { 515 516 } 517 518 @Implementation 519 public void writeToParcel(Parcel p, int flags) { 520 p.writeInt(width); 521 p.writeInt(height); 522 p.writeSerializable(config); 523 p.writeIntArray(colors); 524 } 525 526 @Implementation 527 public static Bitmap nativeCreateFromParcel(Parcel p) { 528 int parceledWidth = p.readInt(); 529 int parceledHeight = p.readInt(); 530 Bitmap.Config parceledConfig = (Bitmap.Config) p.readSerializable(); 531 532 int[] parceledColors = new int[parceledHeight * parceledWidth]; 533 p.readIntArray(parceledColors); 534 535 return createBitmap(parceledColors, parceledWidth, parceledHeight, parceledConfig); 536 } 537 538 @Implementation 539 public void copyPixelsFromBuffer(Buffer dst) { 540 if (isRecycled()) { 541 throw new IllegalStateException("Can't call copyPixelsFromBuffer() on a recycled bitmap"); 542 } 543 544 // See the related comment in #copyPixelsToBuffer(Buffer). 545 if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) { 546 throw new RuntimeException("Not implemented: only Bitmaps with " + INTERNAL_BYTES_PER_PIXEL 547 + " bytes per pixel are supported"); 548 } 549 if (!(dst instanceof ByteBuffer)) { 550 throw new RuntimeException("Not implemented: unsupported Buffer subclass"); 551 } 552 553 ByteBuffer byteBuffer = (ByteBuffer) dst; 554 if (byteBuffer.remaining() < colors.length * INTERNAL_BYTES_PER_PIXEL) { 555 throw new RuntimeException("Buffer not large enough for pixels"); 556 } 557 558 for (int i = 0; i < colors.length; i++) { 559 colors[i] = byteBuffer.getInt(); 560 } 561 } 562 563 @Implementation 564 public void copyPixelsToBuffer(Buffer dst) { 565 // Ensure that the Bitmap uses 4 bytes per pixel, since we always use 4 bytes per pixels 566 // internally. Clients of this API probably expect that the buffer size must be >= 567 // getByteCount(), but if we don't enforce this restriction then for RGB_4444 and other 568 // configs that value would be smaller then the buffer size we actually need. 569 if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) { 570 throw new RuntimeException("Not implemented: only Bitmaps with " + INTERNAL_BYTES_PER_PIXEL 571 + " bytes per pixel are supported"); 572 } 573 574 if (!(dst instanceof ByteBuffer)) { 575 throw new RuntimeException("Not implemented: unsupported Buffer subclass"); 576 } 577 578 ByteBuffer byteBuffer = (ByteBuffer) dst; 579 for (int color : colors) { 580 byteBuffer.putInt(color); 581 } 582 } 583 584 @Override 585 public String toString() { 586 return "Bitmap{description='" + description + '\'' + ", width=" + width + ", height=" + height + '}'; 587 } 588 589 public Bitmap getRealBitmap() { 590 return realBitmap; 591 } 592 593 public static int getBytesPerPixel(Bitmap.Config config) { 594 if (config == null) { 595 throw new NullPointerException("Bitmap config was null."); 596 } 597 switch (config) { 598 case ARGB_8888: 599 return 4; 600 case RGB_565: 601 case ARGB_4444: 602 return 2; 603 case ALPHA_8: 604 return 1; 605 default: 606 throw new IllegalArgumentException("Unknown bitmap config: " + config); 607 } 608 } 609 610 public void setCreatedFromResId(int resId, String description) { 611 this.createdFromResId = resId; 612 appendDescription(" for resource:" + description); 613 } 614 615 private void internalCheckPixelAccess(int x, int y) { 616 if (x < 0) { 617 throw new IllegalArgumentException("x must be >= 0"); 618 } 619 if (y < 0) { 620 throw new IllegalArgumentException("y must be >= 0"); 621 } 622 if (x >= getWidth()) { 623 throw new IllegalArgumentException("x must be < bitmap.width()"); 624 } 625 if (y >= getHeight()) { 626 throw new IllegalArgumentException("y must be < bitmap.height()"); 627 } 628 } 629 } 630