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 ReflectionHelpers.callInstanceMethod(bitmap, "setNinePatchChunk", 129 ClassParameter.from(byte[].class, ninePatchChunk)); 130 ShadowBitmap shadowBitmap = Shadow.extract(bitmap); 131 shadowBitmap.createdFromStream = is; 132 return bitmap; 133 } 134 135 @Implementation decodeByteArray(byte[] data, int offset, int length)136 protected static Bitmap decodeByteArray(byte[] data, int offset, int length) { 137 Bitmap bitmap = decodeByteArray(data, offset, length, new BitmapFactory.Options()); 138 ShadowBitmap shadowBitmap = Shadow.extract(bitmap); 139 shadowBitmap.createdFromBytes = data; 140 return bitmap; 141 } 142 143 @Implementation decodeByteArray( byte[] data, int offset, int length, BitmapFactory.Options opts)144 protected static Bitmap decodeByteArray( 145 byte[] data, int offset, int length, BitmapFactory.Options opts) { 146 String desc = new String(data, UTF_8); 147 148 if (offset != 0 || length != data.length) { 149 desc += " bytes " + offset + ".." + length; 150 } 151 152 Point imageSize = getImageSizeFromStream(new ByteArrayInputStream(data, offset, length)); 153 return create(desc, opts, imageSize); 154 } 155 create(String name)156 static Bitmap create(String name) { 157 return create(name, null); 158 } 159 create(String name, BitmapFactory.Options options)160 public static Bitmap create(String name, BitmapFactory.Options options) { 161 return create(name, options, null); 162 } 163 create(final String name, final BitmapFactory.Options options, final Point widthAndHeight)164 public static Bitmap create(final String name, final BitmapFactory.Options options, final Point widthAndHeight) { 165 Bitmap bitmap = Shadow.newInstanceOf(Bitmap.class); 166 ShadowBitmap shadowBitmap = Shadow.extract(bitmap); 167 shadowBitmap.appendDescription(name == null ? "Bitmap" : "Bitmap for " + name); 168 169 Bitmap.Config config; 170 if (options != null && options.inPreferredConfig != null) { 171 config = options.inPreferredConfig; 172 } else { 173 config = Bitmap.Config.ARGB_8888; 174 } 175 shadowBitmap.setConfig(config); 176 177 String optionsString = stringify(options); 178 if (!optionsString.isEmpty()) { 179 shadowBitmap.appendDescription(" with options "); 180 shadowBitmap.appendDescription(optionsString); 181 } 182 183 Point p = new Point(selectWidthAndHeight(name, widthAndHeight)); 184 if (options != null && options.inSampleSize > 1) { 185 p.x = p.x / options.inSampleSize; 186 p.y = p.y / options.inSampleSize; 187 188 p.x = p.x == 0 ? 1 : p.x; 189 p.y = p.y == 0 ? 1 : p.y; 190 } 191 192 shadowBitmap.setWidth(p.x); 193 shadowBitmap.setHeight(p.y); 194 shadowBitmap.setPixels(new int[p.x * p.y], 0, 0, 0, 0, p.x, p.y); 195 if (options != null) { 196 options.outWidth = p.x; 197 options.outHeight = p.y; 198 } 199 return bitmap; 200 } 201 provideWidthAndHeightHints(Uri uri, int width, int height)202 public static void provideWidthAndHeightHints(Uri uri, int width, int height) { 203 widthAndHeightMap.put(uri.toString(), new Point(width, height)); 204 } 205 provideWidthAndHeightHints(int resourceId, int width, int height)206 public static void provideWidthAndHeightHints(int resourceId, int width, int height) { 207 widthAndHeightMap.put("resource:" + RuntimeEnvironment.application.getResources().getResourceName(resourceId), new Point(width, height)); 208 } 209 provideWidthAndHeightHints(String file, int width, int height)210 public static void provideWidthAndHeightHints(String file, int width, int height) { 211 widthAndHeightMap.put("file:" + file, new Point(width, height)); 212 } 213 214 @SuppressWarnings("ObjectToString") provideWidthAndHeightHints(FileDescriptor fd, int width, int height)215 public static void provideWidthAndHeightHints(FileDescriptor fd, int width, int height) { 216 widthAndHeightMap.put("fd:" + fd, new Point(width, height)); 217 } 218 stringify(BitmapFactory.Options options)219 private static String stringify(BitmapFactory.Options options) { 220 if (options == null) return ""; 221 List<String> opts = new ArrayList<>(); 222 223 if (options.inJustDecodeBounds) opts.add("inJustDecodeBounds"); 224 if (options.inSampleSize > 1) opts.add("inSampleSize=" + options.inSampleSize); 225 226 return Join.join(", ", opts); 227 } 228 229 @Resetter reset()230 public static void reset() { 231 widthAndHeightMap.clear(); 232 } 233 selectWidthAndHeight(final String name, final Point widthAndHeight)234 private static Point selectWidthAndHeight(final String name, final Point widthAndHeight) { 235 final Point widthAndHeightFromMap = widthAndHeightMap.get(name); 236 237 if (widthAndHeightFromMap != null) { 238 return widthAndHeightFromMap; 239 } 240 241 if (widthAndHeight != null) { 242 return widthAndHeight; 243 } 244 245 return new Point(100, 100); 246 } 247 } 248