1 package org.robolectric.shadows;
2 
3 import static java.nio.charset.StandardCharsets.UTF_8;
4 import static org.robolectric.shadow.api.Shadow.directlyOn;
5 import static org.robolectric.shadows.ImageUtil.getImageSizeFromStream;
6 
7 import android.content.res.AssetManager.AssetInputStream;
8 import android.content.res.Resources;
9 import android.graphics.Bitmap;
10 import android.graphics.BitmapFactory;
11 import android.graphics.Point;
12 import android.graphics.Rect;
13 import android.net.Uri;
14 import android.util.TypedValue;
15 import java.io.ByteArrayInputStream;
16 import java.io.FileDescriptor;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.util.ArrayList;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import org.robolectric.RuntimeEnvironment;
24 import org.robolectric.annotation.Implementation;
25 import org.robolectric.annotation.Implements;
26 import org.robolectric.annotation.Resetter;
27 import org.robolectric.shadow.api.Shadow;
28 import org.robolectric.util.Join;
29 import org.robolectric.util.NamedStream;
30 import org.robolectric.util.ReflectionHelpers;
31 import org.robolectric.util.ReflectionHelpers.ClassParameter;
32 
33 @SuppressWarnings({"UnusedDeclaration"})
34 @Implements(BitmapFactory.class)
35 public class ShadowBitmapFactory {
36   private static Map<String, Point> widthAndHeightMap = new HashMap<>();
37 
38   @Implementation
decodeResourceStream( Resources res, TypedValue value, InputStream is, Rect pad, BitmapFactory.Options opts)39   protected static Bitmap decodeResourceStream(
40       Resources res, TypedValue value, InputStream is, Rect pad, BitmapFactory.Options opts) {
41     Bitmap bitmap = directlyOn(BitmapFactory.class, "decodeResourceStream",
42         ClassParameter.from(Resources.class, res),
43         ClassParameter.from(TypedValue.class, value),
44         ClassParameter.from(InputStream.class, is),
45         ClassParameter.from(Rect.class, pad),
46         ClassParameter.from(BitmapFactory.Options.class, opts));
47 
48     if (value != null && value.string != null && value.string.toString().contains(".9.")) {
49       // todo: better support for nine-patches
50       ReflectionHelpers.callInstanceMethod(bitmap, "setNinePatchChunk", ClassParameter.from(byte[].class, new byte[0]));
51     }
52     return bitmap;
53   }
54 
55   @Implementation
decodeResource(Resources res, int id, BitmapFactory.Options options)56   protected static Bitmap decodeResource(Resources res, int id, BitmapFactory.Options options) {
57     if (id == 0) {
58       return null;
59     }
60 
61     final TypedValue value = new TypedValue();
62     InputStream is = res.openRawResource(id, value);
63 
64     Point imageSizeFromStream = getImageSizeFromStream(is);
65 
66     Bitmap bitmap = create("resource:" + res.getResourceName(id), options, imageSizeFromStream);
67     ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
68     shadowBitmap.createdFromResId = id;
69     return bitmap;
70   }
71 
72   @Implementation
decodeFile(String pathName)73   protected static Bitmap decodeFile(String pathName) {
74     return decodeFile(pathName, null);
75   }
76 
77   @Implementation
decodeFile(String pathName, BitmapFactory.Options options)78   protected static Bitmap decodeFile(String pathName, BitmapFactory.Options options) {
79     Bitmap bitmap = create("file:" + pathName, options);
80     ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
81     shadowBitmap.createdFromPath = pathName;
82     return bitmap;
83   }
84 
85   @SuppressWarnings("ObjectToString")
86   @Implementation
decodeFileDescriptor( FileDescriptor fd, Rect outPadding, BitmapFactory.Options opts)87   protected static Bitmap decodeFileDescriptor(
88       FileDescriptor fd, Rect outPadding, BitmapFactory.Options opts) {
89     Bitmap bitmap = create("fd:" + fd, opts);
90     ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
91     shadowBitmap.createdFromFileDescriptor = fd;
92     return bitmap;
93   }
94 
95   @Implementation
decodeStream(InputStream is)96   protected static Bitmap decodeStream(InputStream is) {
97     return decodeStream(is, null, null);
98   }
99 
100   @Implementation
decodeStream( InputStream is, Rect outPadding, BitmapFactory.Options opts)101   protected static Bitmap decodeStream(
102       InputStream is, Rect outPadding, BitmapFactory.Options opts) {
103     byte[] ninePatchChunk = null;
104 
105     if (is instanceof AssetInputStream) {
106       ShadowAssetInputStream sais = Shadow.extract(is);
107       if (sais.isNinePatch()) {
108         ninePatchChunk = new byte[0];
109       }
110       if (sais.getDelegate() != null) {
111         is = sais.getDelegate();
112       }
113     }
114 
115     try {
116       if (is != null) {
117         is.reset();
118       }
119     } catch (IOException e) {
120       // ignore
121     }
122 
123     String name = (is instanceof NamedStream)
124         ? is.toString().replace("stream for ", "")
125         : null;
126     Point imageSize = (is instanceof NamedStream) ? null : getImageSizeFromStream(is);
127     Bitmap bitmap = create(name, opts, imageSize);
128     bitmap.setNinePatchChunk(ninePatchChunk);
129     ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
130     shadowBitmap.createdFromStream = is;
131     return bitmap;
132   }
133 
134   @Implementation
decodeByteArray(byte[] data, int offset, int length)135   protected static Bitmap decodeByteArray(byte[] data, int offset, int length) {
136     Bitmap bitmap = decodeByteArray(data, offset, length, new BitmapFactory.Options());
137     ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
138     shadowBitmap.createdFromBytes = data;
139     return bitmap;
140   }
141 
142   @Implementation
decodeByteArray( byte[] data, int offset, int length, BitmapFactory.Options opts)143   protected static Bitmap decodeByteArray(
144       byte[] data, int offset, int length, BitmapFactory.Options opts) {
145     String desc = new String(data, UTF_8);
146 
147     if (offset != 0 || length != data.length) {
148       desc += " bytes " + offset + ".." + length;
149     }
150 
151     Point imageSize = getImageSizeFromStream(new ByteArrayInputStream(data, offset, length));
152     return create(desc, opts, imageSize);
153   }
154 
create(String name)155   static Bitmap create(String name) {
156     return create(name, null);
157   }
158 
create(String name, BitmapFactory.Options options)159   public static Bitmap create(String name, BitmapFactory.Options options) {
160     return create(name, options, null);
161   }
162 
create(final String name, final BitmapFactory.Options options, final Point widthAndHeight)163   public static Bitmap create(final String name, final BitmapFactory.Options options, final Point widthAndHeight) {
164     Bitmap bitmap = Shadow.newInstanceOf(Bitmap.class);
165     ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
166     shadowBitmap.appendDescription(name == null ? "Bitmap" : "Bitmap for " + name);
167 
168     Bitmap.Config config;
169     if (options != null && options.inPreferredConfig != null) {
170       config = options.inPreferredConfig;
171     } else {
172       config = Bitmap.Config.ARGB_8888;
173     }
174     shadowBitmap.setConfig(config);
175 
176     String optionsString = stringify(options);
177     if (!optionsString.isEmpty()) {
178       shadowBitmap.appendDescription(" with options ");
179       shadowBitmap.appendDescription(optionsString);
180     }
181 
182     Point p = new Point(selectWidthAndHeight(name, widthAndHeight));
183     if (options != null && options.inSampleSize > 1) {
184       p.x = p.x / options.inSampleSize;
185       p.y = p.y / options.inSampleSize;
186 
187       p.x = p.x == 0 ? 1 : p.x;
188       p.y = p.y == 0 ? 1 : p.y;
189     }
190 
191     shadowBitmap.setWidth(p.x);
192     shadowBitmap.setHeight(p.y);
193     shadowBitmap.setPixels(new int[p.x * p.y], 0, 0, 0, 0, p.x, p.y);
194     if (options != null) {
195       options.outWidth = p.x;
196       options.outHeight = p.y;
197     }
198     return bitmap;
199   }
200 
provideWidthAndHeightHints(Uri uri, int width, int height)201   public static void provideWidthAndHeightHints(Uri uri, int width, int height) {
202     widthAndHeightMap.put(uri.toString(), new Point(width, height));
203   }
204 
provideWidthAndHeightHints(int resourceId, int width, int height)205   public static void provideWidthAndHeightHints(int resourceId, int width, int height) {
206     widthAndHeightMap.put("resource:" + RuntimeEnvironment.application.getResources().getResourceName(resourceId), new Point(width, height));
207   }
208 
provideWidthAndHeightHints(String file, int width, int height)209   public static void provideWidthAndHeightHints(String file, int width, int height) {
210     widthAndHeightMap.put("file:" + file, new Point(width, height));
211   }
212 
213   @SuppressWarnings("ObjectToString")
provideWidthAndHeightHints(FileDescriptor fd, int width, int height)214   public static void provideWidthAndHeightHints(FileDescriptor fd, int width, int height) {
215     widthAndHeightMap.put("fd:" + fd, new Point(width, height));
216   }
217 
stringify(BitmapFactory.Options options)218   private static String stringify(BitmapFactory.Options options) {
219     if (options == null) return "";
220     List<String> opts = new ArrayList<>();
221 
222     if (options.inJustDecodeBounds) opts.add("inJustDecodeBounds");
223     if (options.inSampleSize > 1) opts.add("inSampleSize=" + options.inSampleSize);
224 
225     return Join.join(", ", opts);
226   }
227 
228   @Resetter
reset()229   public static void reset() {
230     widthAndHeightMap.clear();
231   }
232 
selectWidthAndHeight(final String name, final Point widthAndHeight)233   private static Point selectWidthAndHeight(final String name, final Point widthAndHeight) {
234     final Point widthAndHeightFromMap = widthAndHeightMap.get(name);
235 
236     if (widthAndHeightFromMap != null) {
237       return widthAndHeightFromMap;
238     }
239 
240     if (widthAndHeight != null) {
241       return widthAndHeight;
242     }
243 
244     return new Point(100, 100);
245   }
246 }
247