1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.M;
4 import static android.os.Build.VERSION_CODES.O;
5 import static android.os.Build.VERSION_CODES.Q;
6 import static android.os.Build.VERSION_CODES.S;
7 import static com.google.common.base.Preconditions.checkArgument;
8 import static com.google.common.base.Preconditions.checkNotNull;
9 import static java.lang.Integer.max;
10 import static java.lang.Integer.min;
11 import static org.robolectric.util.reflector.Reflector.reflector;
12 
13 import android.graphics.Bitmap;
14 import android.graphics.ColorSpace;
15 import android.graphics.Matrix;
16 import android.graphics.Paint;
17 import android.graphics.Rect;
18 import android.graphics.RectF;
19 import android.os.Build;
20 import android.os.Parcel;
21 import android.os.Parcelable;
22 import android.util.DisplayMetrics;
23 import java.awt.Color;
24 import java.awt.Graphics2D;
25 import java.awt.geom.Rectangle2D;
26 import java.awt.image.BufferedImage;
27 import java.awt.image.ColorModel;
28 import java.awt.image.DataBufferInt;
29 import java.awt.image.WritableRaster;
30 import java.io.FileDescriptor;
31 import java.io.InputStream;
32 import java.io.OutputStream;
33 import java.nio.Buffer;
34 import java.nio.ByteBuffer;
35 import java.nio.IntBuffer;
36 import java.util.Arrays;
37 import org.robolectric.RuntimeEnvironment;
38 import org.robolectric.annotation.Implementation;
39 import org.robolectric.annotation.Implements;
40 import org.robolectric.shadow.api.Shadow;
41 import org.robolectric.util.ReflectionHelpers;
42 import org.robolectric.versioning.AndroidVersions.U;
43 
44 @SuppressWarnings({"UnusedDeclaration"})
45 @Implements(value = Bitmap.class, isInAndroidSdk = false)
46 public class ShadowLegacyBitmap extends ShadowBitmap {
47   /** Number of bytes used internally to represent each pixel */
48   private static final int INTERNAL_BYTES_PER_PIXEL = 4;
49 
50   int createdFromResId = -1;
51   String createdFromPath;
52   InputStream createdFromStream;
53   FileDescriptor createdFromFileDescriptor;
54   byte[] createdFromBytes;
55   private Bitmap createdFromBitmap;
56   private Bitmap scaledFromBitmap;
57   private int createdFromX = -1;
58   private int createdFromY = -1;
59   private int createdFromWidth = -1;
60   private int createdFromHeight = -1;
61   private int[] createdFromColors;
62   private Matrix createdFromMatrix;
63   private boolean createdFromFilter;
64 
65   private int width;
66   private int height;
67   private BufferedImage bufferedImage;
68   private Bitmap.Config config;
69   private boolean mutable = true;
70   private String description = "";
71   private boolean recycled = false;
72   private boolean hasMipMap;
73   private boolean requestPremultiplied = true;
74   private boolean hasAlpha;
75   private ColorSpace colorSpace;
76 
77   @Implementation
createBitmap(int width, int height, Bitmap.Config config)78   protected static Bitmap createBitmap(int width, int height, Bitmap.Config config) {
79     return createBitmap((DisplayMetrics) null, width, height, config);
80   }
81 
82   @Implementation
createBitmap( DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config)83   protected static Bitmap createBitmap(
84       DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config) {
85     return createBitmap(displayMetrics, width, height, config, true);
86   }
87 
88   @Implementation
createBitmap( DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config, boolean hasAlpha)89   protected static Bitmap createBitmap(
90       DisplayMetrics displayMetrics,
91       int width,
92       int height,
93       Bitmap.Config config,
94       boolean hasAlpha) {
95     if (width <= 0 || height <= 0) {
96       throw new IllegalArgumentException("width and height must be > 0");
97     }
98     checkNotNull(config);
99     Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
100     ShadowLegacyBitmap shadowBitmap = Shadow.extract(scaledBitmap);
101     shadowBitmap.setDescription("Bitmap (" + width + " x " + height + ")");
102 
103     shadowBitmap.width = width;
104     shadowBitmap.height = height;
105     shadowBitmap.config = config;
106     shadowBitmap.hasAlpha = hasAlpha;
107     shadowBitmap.setMutable(true);
108     if (displayMetrics != null) {
109       scaledBitmap.setDensity(displayMetrics.densityDpi);
110     }
111     shadowBitmap.bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
112     if (RuntimeEnvironment.getApiLevel() >= O) {
113       shadowBitmap.colorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
114     }
115     return scaledBitmap;
116   }
117 
118   @Implementation(minSdk = O)
createBitmap( int width, int height, Bitmap.Config config, boolean hasAlpha, ColorSpace colorSpace)119   protected static Bitmap createBitmap(
120       int width, int height, Bitmap.Config config, boolean hasAlpha, ColorSpace colorSpace) {
121     checkArgument(colorSpace != null || config == Bitmap.Config.ALPHA_8);
122     Bitmap bitmap = createBitmap(null, width, height, config, hasAlpha);
123     ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
124     shadowBitmap.colorSpace = colorSpace;
125     return bitmap;
126   }
127 
128   @Implementation
createBitmap( Bitmap src, int x, int y, int width, int height, Matrix matrix, boolean filter)129   protected static Bitmap createBitmap(
130       Bitmap src, int x, int y, int width, int height, Matrix matrix, boolean filter) {
131     if (x == 0
132         && y == 0
133         && width == src.getWidth()
134         && height == src.getHeight()
135         && (matrix == null || matrix.isIdentity())) {
136       return src; // Return the original.
137     }
138 
139     if (x + width > src.getWidth()) {
140       throw new IllegalArgumentException("x + width must be <= bitmap.width()");
141     }
142     if (y + height > src.getHeight()) {
143       throw new IllegalArgumentException("y + height must be <= bitmap.height()");
144     }
145 
146     Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
147     ShadowLegacyBitmap shadowNewBitmap = Shadow.extract(newBitmap);
148 
149     ShadowLegacyBitmap shadowSrcBitmap = Shadow.extract(src);
150     shadowNewBitmap.appendDescription(shadowSrcBitmap.getDescription());
151     shadowNewBitmap.appendDescription(" at (" + x + "," + y + ")");
152     shadowNewBitmap.appendDescription(" with width " + width + " and height " + height);
153 
154     shadowNewBitmap.createdFromBitmap = src;
155     shadowNewBitmap.createdFromX = x;
156     shadowNewBitmap.createdFromY = y;
157     shadowNewBitmap.createdFromWidth = width;
158     shadowNewBitmap.createdFromHeight = height;
159     shadowNewBitmap.createdFromMatrix = matrix;
160     shadowNewBitmap.createdFromFilter = filter;
161     shadowNewBitmap.config = src.getConfig();
162     if (matrix != null) {
163       ShadowMatrix shadowMatrix = Shadow.extract(matrix);
164       shadowNewBitmap.appendDescription(" using matrix " + shadowMatrix.getDescription());
165 
166       // Adjust width and height by using the matrix.
167       RectF mappedRect = new RectF();
168       matrix.mapRect(mappedRect, new RectF(0, 0, width, height));
169       width = Math.round(mappedRect.width());
170       height = Math.round(mappedRect.height());
171     }
172     if (filter) {
173       shadowNewBitmap.appendDescription(" with filter");
174     }
175 
176     // updated if matrix is non-null
177     shadowNewBitmap.width = width;
178     shadowNewBitmap.height = height;
179     shadowNewBitmap.setMutable(true);
180     newBitmap.setDensity(src.getDensity());
181     if ((matrix == null || matrix.isIdentity()) && shadowSrcBitmap.bufferedImage != null) {
182       // Only simple cases are supported for setting image data to the new Bitmap.
183       shadowNewBitmap.bufferedImage =
184           shadowSrcBitmap.bufferedImage.getSubimage(x, y, width, height);
185     }
186     if (RuntimeEnvironment.getApiLevel() >= O) {
187       shadowNewBitmap.colorSpace = shadowSrcBitmap.colorSpace;
188     }
189     return newBitmap;
190   }
191 
192   @Implementation
createBitmap( int[] colors, int offset, int stride, int width, int height, Bitmap.Config config)193   protected static Bitmap createBitmap(
194       int[] colors, int offset, int stride, int width, int height, Bitmap.Config config) {
195     return createBitmap(null, colors, offset, stride, width, height, config);
196   }
197 
198   @Implementation
createBitmap( DisplayMetrics displayMetrics, int[] colors, int offset, int stride, int width, int height, Bitmap.Config config)199   protected static Bitmap createBitmap(
200       DisplayMetrics displayMetrics,
201       int[] colors,
202       int offset,
203       int stride,
204       int width,
205       int height,
206       Bitmap.Config config) {
207     if (width <= 0) {
208       throw new IllegalArgumentException("width must be > 0");
209     }
210     if (height <= 0) {
211       throw new IllegalArgumentException("height must be > 0");
212     }
213     if (Math.abs(stride) < width) {
214       throw new IllegalArgumentException("abs(stride) must be >= width");
215     }
216     checkNotNull(config);
217     int lastScanline = offset + (height - 1) * stride;
218     int length = colors.length;
219     if (offset < 0
220         || (offset + width > length)
221         || lastScanline < 0
222         || (lastScanline + width > length)) {
223       throw new ArrayIndexOutOfBoundsException();
224     }
225 
226     BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
227     bufferedImage.setRGB(0, 0, width, height, colors, offset, stride);
228     Bitmap bitmap = createBitmap(bufferedImage, width, height, config);
229     ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
230     shadowBitmap.setMutable(false);
231     shadowBitmap.createdFromColors = colors;
232     if (displayMetrics != null) {
233       bitmap.setDensity(displayMetrics.densityDpi);
234     }
235     if (RuntimeEnvironment.getApiLevel() >= O) {
236       shadowBitmap.colorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
237     }
238     return bitmap;
239   }
240 
createBitmap( BufferedImage bufferedImage, int width, int height, Bitmap.Config config)241   private static Bitmap createBitmap(
242       BufferedImage bufferedImage, int width, int height, Bitmap.Config config) {
243     Bitmap newBitmap = Bitmap.createBitmap(width, height, config);
244     ShadowLegacyBitmap shadowBitmap = Shadow.extract(newBitmap);
245     shadowBitmap.bufferedImage = bufferedImage;
246     return newBitmap;
247   }
248 
249   @Implementation
createScaledBitmap( Bitmap src, int dstWidth, int dstHeight, boolean filter)250   protected static Bitmap createScaledBitmap(
251       Bitmap src, int dstWidth, int dstHeight, boolean filter) {
252     if (dstWidth == src.getWidth() && dstHeight == src.getHeight() && !filter) {
253       return src; // Return the original.
254     }
255     if (dstWidth <= 0 || dstHeight <= 0) {
256       throw new IllegalArgumentException("width and height must be > 0");
257     }
258     Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
259     ShadowLegacyBitmap shadowBitmap = Shadow.extract(scaledBitmap);
260 
261     ShadowLegacyBitmap shadowSrcBitmap = Shadow.extract(src);
262     shadowBitmap.appendDescription(shadowSrcBitmap.getDescription());
263     shadowBitmap.appendDescription(" scaled to " + dstWidth + " x " + dstHeight);
264     if (filter) {
265       shadowBitmap.appendDescription(" with filter " + filter);
266     }
267 
268     shadowBitmap.createdFromBitmap = src;
269     shadowBitmap.scaledFromBitmap = src;
270     shadowBitmap.createdFromFilter = filter;
271     shadowBitmap.width = dstWidth;
272     shadowBitmap.height = dstHeight;
273     shadowBitmap.config = src.getConfig();
274     shadowBitmap.mutable = true;
275     if (!ImageUtil.scaledBitmap(src, scaledBitmap, filter)) {
276       shadowBitmap.bufferedImage =
277           new BufferedImage(dstWidth, dstHeight, BufferedImage.TYPE_INT_ARGB);
278       shadowBitmap.setPixelsInternal(
279           new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()],
280           0,
281           0,
282           0,
283           0,
284           shadowBitmap.getWidth(),
285           shadowBitmap.getHeight());
286     }
287     if (RuntimeEnvironment.getApiLevel() >= O) {
288       shadowBitmap.colorSpace = shadowSrcBitmap.colorSpace;
289     }
290     return scaledBitmap;
291   }
292 
293   @Implementation
nativeCreateFromParcel(Parcel p)294   protected static Bitmap nativeCreateFromParcel(Parcel p) {
295     int parceledWidth = p.readInt();
296     int parceledHeight = p.readInt();
297     Bitmap.Config parceledConfig = (Bitmap.Config) p.readSerializable();
298 
299     int[] parceledColors = new int[parceledHeight * parceledWidth];
300     p.readIntArray(parceledColors);
301 
302     return createBitmap(
303         parceledColors, 0, parceledWidth, parceledWidth, parceledHeight, parceledConfig);
304   }
305 
getBytesPerPixel(Bitmap.Config config)306   static int getBytesPerPixel(Bitmap.Config config) {
307     if (config == null) {
308       throw new NullPointerException("Bitmap config was null.");
309     }
310     switch (config) {
311       case RGBA_F16:
312         return 8;
313       case ARGB_8888:
314       case HARDWARE:
315         return 4;
316       case RGB_565:
317       case ARGB_4444:
318         return 2;
319       case ALPHA_8:
320         return 1;
321       default:
322         throw new IllegalArgumentException("Unknown bitmap config: " + config);
323     }
324   }
325 
326   /**
327    * Reference to original Bitmap from which this Bitmap was created. {@code null} if this Bitmap
328    * was not copied from another instance.
329    *
330    * @return Original Bitmap from which this Bitmap was created.
331    */
332   @Override
getCreatedFromBitmap()333   public Bitmap getCreatedFromBitmap() {
334     return createdFromBitmap;
335   }
336 
337   /**
338    * Resource ID from which this Bitmap was created. {@code 0} if this Bitmap was not created from a
339    * resource.
340    *
341    * @return Resource ID from which this Bitmap was created.
342    */
343   @Override
getCreatedFromResId()344   public int getCreatedFromResId() {
345     return createdFromResId;
346   }
347 
348   /**
349    * Path from which this Bitmap was created. {@code null} if this Bitmap was not create from a
350    * path.
351    *
352    * @return Path from which this Bitmap was created.
353    */
354   @Override
getCreatedFromPath()355   public String getCreatedFromPath() {
356     return createdFromPath;
357   }
358 
359   /**
360    * {@link InputStream} from which this Bitmap was created. {@code null} if this Bitmap was not
361    * created from a stream.
362    *
363    * @return InputStream from which this Bitmap was created.
364    */
365   @Override
getCreatedFromStream()366   public InputStream getCreatedFromStream() {
367     return createdFromStream;
368   }
369 
370   /**
371    * Bytes from which this Bitmap was created. {@code null} if this Bitmap was not created from
372    * bytes.
373    *
374    * @return Bytes from which this Bitmap was created.
375    */
376   @Override
getCreatedFromBytes()377   public byte[] getCreatedFromBytes() {
378     return createdFromBytes;
379   }
380 
381   /**
382    * Horizontal offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
383    *
384    * @return Horizontal offset within {@link #getCreatedFromBitmap()}.
385    */
386   @Override
getCreatedFromX()387   public int getCreatedFromX() {
388     return createdFromX;
389   }
390 
391   /**
392    * Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
393    *
394    * @return Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
395    */
396   @Override
getCreatedFromY()397   public int getCreatedFromY() {
398     return createdFromY;
399   }
400 
401   /**
402    * Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
403    * content, or -1.
404    *
405    * @return Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this
406    *     Bitmap's content, or -1.
407    */
408   @Override
getCreatedFromWidth()409   public int getCreatedFromWidth() {
410     return createdFromWidth;
411   }
412 
413   /**
414    * Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
415    * content, or -1.
416    *
417    * @return Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this
418    *     Bitmap's content, or -1.
419    */
420   @Override
getCreatedFromHeight()421   public int getCreatedFromHeight() {
422     return createdFromHeight;
423   }
424 
425   /**
426    * Color array from which this Bitmap was created. {@code null} if this Bitmap was not created
427    * from a color array.
428    *
429    * @return Color array from which this Bitmap was created.
430    */
431   @Override
getCreatedFromColors()432   public int[] getCreatedFromColors() {
433     return createdFromColors;
434   }
435 
436   /**
437    * Matrix from which this Bitmap's content was transformed, or {@code null}.
438    *
439    * @return Matrix from which this Bitmap's content was transformed, or {@code null}.
440    */
441   @Override
getCreatedFromMatrix()442   public Matrix getCreatedFromMatrix() {
443     return createdFromMatrix;
444   }
445 
446   /**
447    * {@code true} if this Bitmap was created with filtering.
448    *
449    * @return {@code true} if this Bitmap was created with filtering.
450    */
451   @Override
getCreatedFromFilter()452   public boolean getCreatedFromFilter() {
453     return createdFromFilter;
454   }
455 
456   @Implementation(minSdk = S)
asShared()457   protected Bitmap asShared() {
458     setMutable(false);
459     return realBitmap;
460   }
461 
462   @Implementation
compress(Bitmap.CompressFormat format, int quality, OutputStream stream)463   protected boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) {
464     appendDescription(" compressed as " + format + " with quality " + quality);
465     return ImageUtil.writeToStream(realBitmap, format, quality, stream);
466   }
467 
468   @Implementation
setPixels( int[] pixels, int offset, int stride, int x, int y, int width, int height)469   protected void setPixels(
470       int[] pixels, int offset, int stride, int x, int y, int width, int height) {
471     checkBitmapMutable();
472     setPixelsInternal(pixels, offset, stride, x, y, width, height);
473   }
474 
setPixelsInternal( int[] pixels, int offset, int stride, int x, int y, int width, int height)475   void setPixelsInternal(
476       int[] pixels, int offset, int stride, int x, int y, int width, int height) {
477     if (bufferedImage == null) {
478       bufferedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
479     }
480     bufferedImage.setRGB(x, y, width, height, pixels, offset, stride);
481   }
482 
483   @Implementation
getPixel(int x, int y)484   protected int getPixel(int x, int y) {
485     internalCheckPixelAccess(x, y);
486     if (bufferedImage != null) {
487       // Note that getPixel() returns a non-premultiplied ARGB value; if
488       // config is RGB_565, our return value will likely be more precise than
489       // on a physical device, since it needs to map each color component from
490       // 5 or 6 bits to 8 bits.
491       return bufferedImage.getRGB(x, y);
492     } else {
493       return 0;
494     }
495   }
496 
497   @Implementation
setPixel(int x, int y, int color)498   protected void setPixel(int x, int y, int color) {
499     checkBitmapMutable();
500     internalCheckPixelAccess(x, y);
501     if (bufferedImage == null) {
502       bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
503     }
504     bufferedImage.setRGB(x, y, color);
505   }
506 
507   /**
508    * Note that this method will return a RuntimeException unless: - {@code pixels} has the same
509    * length as the number of pixels of the bitmap. - {@code x = 0} - {@code y = 0} - {@code width}
510    * and {@code height} height match the current bitmap's dimensions.
511    */
512   @Implementation
getPixels( int[] pixels, int offset, int stride, int x, int y, int width, int height)513   protected void getPixels(
514       int[] pixels, int offset, int stride, int x, int y, int width, int height) {
515     bufferedImage.getRGB(x, y, width, height, pixels, offset, stride);
516   }
517 
518   @Implementation
getRowBytes()519   protected int getRowBytes() {
520     return getBytesPerPixel(config) * getWidth();
521   }
522 
523   @Implementation
getByteCount()524   protected int getByteCount() {
525     return getRowBytes() * getHeight();
526   }
527 
528   @Implementation
recycle()529   protected void recycle() {
530     recycled = true;
531   }
532 
533   @Implementation
isRecycled()534   protected boolean isRecycled() {
535     return recycled;
536   }
537 
538   @Implementation
copy(Bitmap.Config config, boolean isMutable)539   protected Bitmap copy(Bitmap.Config config, boolean isMutable) {
540     Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
541     ShadowLegacyBitmap shadowBitmap = Shadow.extract(newBitmap);
542     shadowBitmap.createdFromBitmap = realBitmap;
543     shadowBitmap.config = config;
544     shadowBitmap.mutable = isMutable;
545     shadowBitmap.height = getHeight();
546     shadowBitmap.width = getWidth();
547     if (bufferedImage != null) {
548       ColorModel cm = bufferedImage.getColorModel();
549       WritableRaster raster =
550           bufferedImage.copyData(bufferedImage.getRaster().createCompatibleWritableRaster());
551       shadowBitmap.bufferedImage = new BufferedImage(cm, raster, false, null);
552     }
553     return newBitmap;
554   }
555 
556   @Implementation
getAllocationByteCount()557   protected int getAllocationByteCount() {
558     return getRowBytes() * getHeight();
559   }
560 
561   @Implementation
getConfig()562   protected Bitmap.Config getConfig() {
563     return config;
564   }
565 
566   @Implementation
setConfig(Bitmap.Config config)567   protected void setConfig(Bitmap.Config config) {
568     this.config = config;
569   }
570 
571   @Implementation
isMutable()572   protected boolean isMutable() {
573     return mutable;
574   }
575 
576   @Override
setMutable(boolean mutable)577   public void setMutable(boolean mutable) {
578     this.mutable = mutable;
579   }
580 
581   @Override
appendDescription(String s)582   public void appendDescription(String s) {
583     description += s;
584   }
585 
586   @Override
getDescription()587   public String getDescription() {
588     return description;
589   }
590 
591   @Override
setDescription(String s)592   public void setDescription(String s) {
593     description = s;
594   }
595 
596   @Implementation
hasAlpha()597   protected boolean hasAlpha() {
598     return hasAlpha && config != Bitmap.Config.RGB_565;
599   }
600 
601   @Implementation
setHasAlpha(boolean hasAlpha)602   protected void setHasAlpha(boolean hasAlpha) {
603     this.hasAlpha = hasAlpha;
604   }
605 
606   @Implementation
extractAlpha()607   protected Bitmap extractAlpha() {
608     WritableRaster raster = bufferedImage.getAlphaRaster();
609     BufferedImage alphaImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
610     alphaImage.getAlphaRaster().setRect(raster);
611     return createBitmap(alphaImage, getWidth(), getHeight(), Bitmap.Config.ALPHA_8);
612   }
613 
614   /**
615    * This shadow implementation ignores the given paint and offsetXY and simply calls {@link
616    * #extractAlpha()}.
617    */
618   @Implementation
extractAlpha(Paint paint, int[] offsetXY)619   protected Bitmap extractAlpha(Paint paint, int[] offsetXY) {
620     return extractAlpha();
621   }
622 
623   @Implementation
hasMipMap()624   protected boolean hasMipMap() {
625     return hasMipMap;
626   }
627 
628   @Implementation
setHasMipMap(boolean hasMipMap)629   protected void setHasMipMap(boolean hasMipMap) {
630     this.hasMipMap = hasMipMap;
631   }
632 
633   @Implementation
getWidth()634   protected int getWidth() {
635     return width;
636   }
637 
638   @Implementation
setWidth(int width)639   protected void setWidth(int width) {
640     this.width = width;
641   }
642 
643   @Implementation
getHeight()644   protected int getHeight() {
645     return height;
646   }
647 
648   @Implementation
setHeight(int height)649   protected void setHeight(int height) {
650     this.height = height;
651   }
652 
653   @Implementation
getGenerationId()654   protected int getGenerationId() {
655     return 0;
656   }
657 
658   @Implementation(minSdk = M)
createAshmemBitmap()659   protected Bitmap createAshmemBitmap() {
660     return realBitmap;
661   }
662 
663   @Implementation
eraseColor(int color)664   protected void eraseColor(int color) {
665     if (bufferedImage != null) {
666       int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
667       Arrays.fill(pixels, color);
668     }
669     setDescription(String.format("Bitmap (%d, %d)", width, height));
670     if (color != 0) {
671       appendDescription(String.format(" erased with 0x%08x", color));
672     }
673   }
674 
675   @Implementation
writeToParcel(Parcel p, int flags)676   protected void writeToParcel(Parcel p, int flags) {
677     p.writeInt(width);
678     p.writeInt(height);
679     p.writeSerializable(config);
680     int[] pixels = new int[width * height];
681     getPixels(pixels, 0, width, 0, 0, width, height);
682     p.writeIntArray(pixels);
683 
684     if (RuntimeEnvironment.getApiLevel() >= U.SDK_INT) {
685       Object gainmap = reflector(BitmapReflector.class, realBitmap).getGainmap();
686       if (gainmap != null) {
687         p.writeBoolean(true);
688         p.writeTypedObject((Parcelable) gainmap, flags);
689       } else {
690         p.writeBoolean(false);
691       }
692     }
693   }
694 
695   @Implementation
copyPixelsFromBuffer(Buffer dst)696   protected void copyPixelsFromBuffer(Buffer dst) {
697     if (isRecycled()) {
698       throw new IllegalStateException("Can't call copyPixelsFromBuffer() on a recycled bitmap");
699     }
700 
701     // See the related comment in #copyPixelsToBuffer(Buffer).
702     if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
703       throw new RuntimeException(
704           "Not implemented: only Bitmaps with "
705               + INTERNAL_BYTES_PER_PIXEL
706               + " bytes per pixel are supported");
707     }
708     if (!(dst instanceof ByteBuffer) && !(dst instanceof IntBuffer)) {
709       throw new RuntimeException("Not implemented: unsupported Buffer subclass");
710     }
711 
712     ByteBuffer byteBuffer = null;
713     IntBuffer intBuffer;
714     if (dst instanceof IntBuffer) {
715       intBuffer = (IntBuffer) dst;
716     } else {
717       byteBuffer = (ByteBuffer) dst;
718       intBuffer = byteBuffer.asIntBuffer();
719     }
720 
721     if (intBuffer.remaining() < (width * height)) {
722       throw new RuntimeException("Buffer not large enough for pixels");
723     }
724 
725     int[] colors = new int[width * height];
726     intBuffer.get(colors);
727     if (byteBuffer != null) {
728       byteBuffer.position(byteBuffer.position() + intBuffer.position() * INTERNAL_BYTES_PER_PIXEL);
729     }
730     int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
731     System.arraycopy(colors, 0, pixels, 0, pixels.length);
732   }
733 
734   @Implementation
copyPixelsToBuffer(Buffer dst)735   protected void copyPixelsToBuffer(Buffer dst) {
736     // Ensure that the Bitmap uses 4 bytes per pixel, since we always use 4 bytes per pixels
737     // internally. Clients of this API probably expect that the buffer size must be >=
738     // getByteCount(), but if we don't enforce this restriction then for RGB_4444 and other
739     // configs that value would be smaller then the buffer size we actually need.
740     if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
741       throw new RuntimeException(
742           "Not implemented: only Bitmaps with "
743               + INTERNAL_BYTES_PER_PIXEL
744               + " bytes per pixel are supported");
745     }
746 
747     if (!(dst instanceof ByteBuffer) && !(dst instanceof IntBuffer)) {
748       throw new RuntimeException("Not implemented: unsupported Buffer subclass");
749     }
750     int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
751     if (dst instanceof ByteBuffer) {
752       IntBuffer intBuffer = ((ByteBuffer) dst).asIntBuffer();
753       intBuffer.put(pixels);
754       dst.position(intBuffer.position() * 4);
755     } else if (dst instanceof IntBuffer) {
756       ((IntBuffer) dst).put(pixels);
757     }
758   }
759 
760   @Implementation
reconfigure(int width, int height, Bitmap.Config config)761   protected void reconfigure(int width, int height, Bitmap.Config config) {
762     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this.config == Bitmap.Config.HARDWARE) {
763       throw new IllegalStateException("native-backed bitmaps may not be reconfigured");
764     }
765 
766     // This should throw if the resulting allocation size is greater than the initial allocation
767     // size of our Bitmap, but we don't keep track of that information reliably, so we're forced to
768     // assume that our original dimensions and config are large enough to fit the new dimensions and
769     // config
770     this.width = width;
771     this.height = height;
772     this.config = config;
773     bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
774   }
775 
776   @Implementation
isPremultiplied()777   protected boolean isPremultiplied() {
778     return requestPremultiplied && hasAlpha();
779   }
780 
781   @Implementation
setPremultiplied(boolean isPremultiplied)782   protected void setPremultiplied(boolean isPremultiplied) {
783     this.requestPremultiplied = isPremultiplied;
784   }
785 
786   @Implementation(minSdk = O)
getColorSpace()787   protected ColorSpace getColorSpace() {
788     return colorSpace;
789   }
790 
791   @Implementation(minSdk = Q)
setColorSpace(ColorSpace colorSpace)792   protected void setColorSpace(ColorSpace colorSpace) {
793     this.colorSpace = checkNotNull(colorSpace);
794   }
795 
796   @Implementation
sameAs(Bitmap other)797   protected boolean sameAs(Bitmap other) {
798     if (other == null) {
799       return false;
800     }
801     ShadowLegacyBitmap shadowOtherBitmap = Shadow.extract(other);
802     if (this.width != shadowOtherBitmap.width || this.height != shadowOtherBitmap.height) {
803       return false;
804     }
805     if (this.config != shadowOtherBitmap.config) {
806       return false;
807     }
808 
809     if (bufferedImage == null && shadowOtherBitmap.bufferedImage != null) {
810       return false;
811     } else if (bufferedImage != null && shadowOtherBitmap.bufferedImage == null) {
812       return false;
813     } else if (bufferedImage != null && shadowOtherBitmap.bufferedImage != null) {
814       int[] pixels = ((DataBufferInt) bufferedImage.getData().getDataBuffer()).getData();
815       int[] otherPixels =
816           ((DataBufferInt) shadowOtherBitmap.bufferedImage.getData().getDataBuffer()).getData();
817       if (!Arrays.equals(pixels, otherPixels)) {
818         return false;
819       }
820     }
821     // When Bitmap.createScaledBitmap is called, the colors array is cleared, so we need a basic
822     // way to detect if two scaled bitmaps are the same.
823     if (scaledFromBitmap != null && shadowOtherBitmap.scaledFromBitmap != null) {
824       return scaledFromBitmap.sameAs(shadowOtherBitmap.scaledFromBitmap);
825     }
826     return true;
827   }
828 
setCreatedFromResId(int resId, String description)829   void setCreatedFromResId(int resId, String description) {
830     this.createdFromResId = resId;
831     appendDescription(" for resource:" + description);
832   }
833 
checkBitmapMutable()834   private void checkBitmapMutable() {
835     if (isRecycled()) {
836       throw new IllegalStateException("Can't call setPixel() on a recycled bitmap");
837     } else if (!isMutable()) {
838       throw new IllegalStateException("Bitmap is immutable");
839     }
840   }
841 
internalCheckPixelAccess(int x, int y)842   private void internalCheckPixelAccess(int x, int y) {
843     if (x < 0) {
844       throw new IllegalArgumentException("x must be >= 0");
845     }
846     if (y < 0) {
847       throw new IllegalArgumentException("y must be >= 0");
848     }
849     if (x >= getWidth()) {
850       throw new IllegalArgumentException("x must be < bitmap.width()");
851     }
852     if (y >= getHeight()) {
853       throw new IllegalArgumentException("y must be < bitmap.height()");
854     }
855   }
856 
drawRect(Rect r, Paint paint)857   void drawRect(Rect r, Paint paint) {
858     if (bufferedImage == null) {
859       return;
860     }
861     int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
862 
863     Rect toDraw =
864         new Rect(
865             max(0, r.left), max(0, r.top), min(getWidth(), r.right), min(getHeight(), r.bottom));
866     if (toDraw.left == 0 && toDraw.top == 0 && toDraw.right == getWidth()) {
867       Arrays.fill(pixels, 0, getWidth() * toDraw.bottom, paint.getColor());
868       return;
869     }
870     for (int y = toDraw.top; y < toDraw.bottom; y++) {
871       Arrays.fill(
872           pixels, y * getWidth() + toDraw.left, y * getWidth() + toDraw.right, paint.getColor());
873     }
874   }
875 
drawRect(RectF r, Paint paint)876   void drawRect(RectF r, Paint paint) {
877     if (bufferedImage == null) {
878       return;
879     }
880 
881     Graphics2D graphics2D = bufferedImage.createGraphics();
882     Rectangle2D r2d = new Rectangle2D.Float(r.left, r.top, r.right - r.left, r.bottom - r.top);
883     graphics2D.setColor(new Color(paint.getColor()));
884     graphics2D.draw(r2d);
885     graphics2D.dispose();
886   }
887 
drawBitmap(Bitmap source, int left, int top)888   void drawBitmap(Bitmap source, int left, int top) {
889     ShadowLegacyBitmap shadowSource = Shadow.extract(source);
890     if (bufferedImage == null || shadowSource.bufferedImage == null) {
891       // pixel data not available, so there's nothing we can do
892       return;
893     }
894 
895     int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
896     int[] sourcePixels =
897         ((DataBufferInt) shadowSource.bufferedImage.getRaster().getDataBuffer()).getData();
898 
899     // fast path
900     if (left == 0 && top == 0 && getWidth() == source.getWidth()) {
901       int size = min(getWidth() * getHeight(), source.getWidth() * source.getHeight());
902       System.arraycopy(sourcePixels, 0, pixels, 0, size);
903       return;
904     }
905     // slower (row-by-row) path
906     int startSourceY = max(0, -top);
907     int startSourceX = max(0, -left);
908     int startY = max(0, top);
909     int startX = max(0, left);
910     int endY = min(getHeight(), top + source.getHeight());
911     int endX = min(getWidth(), left + source.getWidth());
912     int lenY = endY - startY;
913     int lenX = endX - startX;
914     for (int y = 0; y < lenY; y++) {
915       System.arraycopy(
916           sourcePixels,
917           (startSourceY + y) * source.getWidth() + startSourceX,
918           pixels,
919           (startY + y) * getWidth() + startX,
920           lenX);
921     }
922   }
923 
getBufferedImage()924   BufferedImage getBufferedImage() {
925     return bufferedImage;
926   }
927 
setBufferedImage(BufferedImage bufferedImage)928   void setBufferedImage(BufferedImage bufferedImage) {
929     this.bufferedImage = bufferedImage;
930   }
931 }
932