1package org.robolectric.shadows;
2
3import static java.nio.charset.StandardCharsets.UTF_8;
4import static org.robolectric.Shadows.shadowOf;
5
6import android.graphics.Bitmap;
7import android.graphics.Matrix;
8import android.graphics.RectF;
9import android.os.Parcel;
10import android.util.DisplayMetrics;
11import java.io.FileDescriptor;
12import java.io.IOException;
13import java.io.InputStream;
14import java.io.OutputStream;
15import java.nio.Buffer;
16import java.nio.ByteBuffer;
17import org.robolectric.annotation.Implementation;
18import org.robolectric.annotation.Implements;
19import org.robolectric.annotation.RealObject;
20import org.robolectric.util.ReflectionHelpers;
21
22@SuppressWarnings({"UnusedDeclaration"})
23@Implements(Bitmap.class)
24public 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