Home | History | Annotate | Download | only in shadows
      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