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