1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.volley.toolbox; 18 19 import android.graphics.Bitmap; 20 import android.graphics.Bitmap.Config; 21 import android.graphics.BitmapFactory; 22 import android.widget.ImageView.ScaleType; 23 24 import com.android.volley.DefaultRetryPolicy; 25 import com.android.volley.NetworkResponse; 26 import com.android.volley.ParseError; 27 import com.android.volley.Request; 28 import com.android.volley.Response; 29 import com.android.volley.VolleyLog; 30 31 /** 32 * A canned request for getting an image at a given URL and calling 33 * back with a decoded Bitmap. 34 */ 35 public class ImageRequest extends Request<Bitmap> { 36 /** Socket timeout in milliseconds for image requests */ 37 public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000; 38 39 /** Default number of retries for image requests */ 40 public static final int DEFAULT_IMAGE_MAX_RETRIES = 2; 41 42 /** Default backoff multiplier for image requests */ 43 public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f; 44 45 /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */ 46 private final Object mLock = new Object(); 47 48 // @GuardedBy("mLock") 49 private Response.Listener<Bitmap> mListener; 50 private final Config mDecodeConfig; 51 private final int mMaxWidth; 52 private final int mMaxHeight; 53 private final ScaleType mScaleType; 54 55 /** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */ 56 private static final Object sDecodeLock = new Object(); 57 58 /** 59 * Creates a new image request, decoding to a maximum specified width and 60 * height. If both width and height are zero, the image will be decoded to 61 * its natural size. If one of the two is nonzero, that dimension will be 62 * clamped and the other one will be set to preserve the image's aspect 63 * ratio. If both width and height are nonzero, the image will be decoded to 64 * be fit in the rectangle of dimensions width x height while keeping its 65 * aspect ratio. 66 * 67 * @param url URL of the image 68 * @param listener Listener to receive the decoded bitmap 69 * @param maxWidth Maximum width to decode this bitmap to, or zero for none 70 * @param maxHeight Maximum height to decode this bitmap to, or zero for 71 * none 72 * @param scaleType The ImageViews ScaleType used to calculate the needed image size. 73 * @param decodeConfig Format to decode the bitmap to 74 * @param errorListener Error listener, or null to ignore errors 75 */ 76 public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, 77 ScaleType scaleType, Config decodeConfig, Response.ErrorListener errorListener) { 78 super(Method.GET, url, errorListener); 79 setRetryPolicy(new DefaultRetryPolicy(DEFAULT_IMAGE_TIMEOUT_MS, DEFAULT_IMAGE_MAX_RETRIES, 80 DEFAULT_IMAGE_BACKOFF_MULT)); 81 mListener = listener; 82 mDecodeConfig = decodeConfig; 83 mMaxWidth = maxWidth; 84 mMaxHeight = maxHeight; 85 mScaleType = scaleType; 86 } 87 88 /** 89 * For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to 90 * the normal constructor with {@code ScaleType.CENTER_INSIDE}. 91 */ 92 @Deprecated 93 public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, 94 Config decodeConfig, Response.ErrorListener errorListener) { 95 this(url, listener, maxWidth, maxHeight, 96 ScaleType.CENTER_INSIDE, decodeConfig, errorListener); 97 } 98 @Override 99 public Priority getPriority() { 100 return Priority.LOW; 101 } 102 103 /** 104 * Scales one side of a rectangle to fit aspect ratio. 105 * 106 * @param maxPrimary Maximum size of the primary dimension (i.e. width for 107 * max width), or zero to maintain aspect ratio with secondary 108 * dimension 109 * @param maxSecondary Maximum size of the secondary dimension, or zero to 110 * maintain aspect ratio with primary dimension 111 * @param actualPrimary Actual size of the primary dimension 112 * @param actualSecondary Actual size of the secondary dimension 113 * @param scaleType The ScaleType used to calculate the needed image size. 114 */ 115 private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, 116 int actualSecondary, ScaleType scaleType) { 117 118 // If no dominant value at all, just return the actual. 119 if ((maxPrimary == 0) && (maxSecondary == 0)) { 120 return actualPrimary; 121 } 122 123 // If ScaleType.FIT_XY fill the whole rectangle, ignore ratio. 124 if (scaleType == ScaleType.FIT_XY) { 125 if (maxPrimary == 0) { 126 return actualPrimary; 127 } 128 return maxPrimary; 129 } 130 131 // If primary is unspecified, scale primary to match secondary's scaling ratio. 132 if (maxPrimary == 0) { 133 double ratio = (double) maxSecondary / (double) actualSecondary; 134 return (int) (actualPrimary * ratio); 135 } 136 137 if (maxSecondary == 0) { 138 return maxPrimary; 139 } 140 141 double ratio = (double) actualSecondary / (double) actualPrimary; 142 int resized = maxPrimary; 143 144 // If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio. 145 if (scaleType == ScaleType.CENTER_CROP) { 146 if ((resized * ratio) < maxSecondary) { 147 resized = (int) (maxSecondary / ratio); 148 } 149 return resized; 150 } 151 152 if ((resized * ratio) > maxSecondary) { 153 resized = (int) (maxSecondary / ratio); 154 } 155 return resized; 156 } 157 158 @Override 159 protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) { 160 // Serialize all decode on a global lock to reduce concurrent heap usage. 161 synchronized (sDecodeLock) { 162 try { 163 return doParse(response); 164 } catch (OutOfMemoryError e) { 165 VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl()); 166 return Response.error(new ParseError(e)); 167 } 168 } 169 } 170 171 /** 172 * The real guts of parseNetworkResponse. Broken out for readability. 173 */ 174 private Response<Bitmap> doParse(NetworkResponse response) { 175 byte[] data = response.data; 176 BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); 177 Bitmap bitmap = null; 178 if (mMaxWidth == 0 && mMaxHeight == 0) { 179 decodeOptions.inPreferredConfig = mDecodeConfig; 180 bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); 181 } else { 182 // If we have to resize this image, first get the natural bounds. 183 decodeOptions.inJustDecodeBounds = true; 184 BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); 185 int actualWidth = decodeOptions.outWidth; 186 int actualHeight = decodeOptions.outHeight; 187 188 // Then compute the dimensions we would ideally like to decode to. 189 int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, 190 actualWidth, actualHeight, mScaleType); 191 int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth, 192 actualHeight, actualWidth, mScaleType); 193 194 // Decode to the nearest power of two scaling factor. 195 decodeOptions.inJustDecodeBounds = false; 196 // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it? 197 // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED; 198 decodeOptions.inSampleSize = 199 findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); 200 Bitmap tempBitmap = 201 BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); 202 203 // If necessary, scale down to the maximal acceptable size. 204 if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || 205 tempBitmap.getHeight() > desiredHeight)) { 206 bitmap = Bitmap.createScaledBitmap(tempBitmap, 207 desiredWidth, desiredHeight, true); 208 tempBitmap.recycle(); 209 } else { 210 bitmap = tempBitmap; 211 } 212 } 213 214 if (bitmap == null) { 215 return Response.error(new ParseError(response)); 216 } else { 217 return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); 218 } 219 } 220 221 @Override 222 public void cancel() { 223 super.cancel(); 224 synchronized (mLock) { 225 mListener = null; 226 } 227 } 228 229 @Override 230 protected void deliverResponse(Bitmap response) { 231 Response.Listener<Bitmap> listener; 232 synchronized (mLock) { 233 listener = mListener; 234 } 235 if (listener != null) { 236 listener.onResponse(response); 237 } 238 } 239 240 /** 241 * Returns the largest power-of-two divisor for use in downscaling a bitmap 242 * that will not result in the scaling past the desired dimensions. 243 * 244 * @param actualWidth Actual width of the bitmap 245 * @param actualHeight Actual height of the bitmap 246 * @param desiredWidth Desired width of the bitmap 247 * @param desiredHeight Desired height of the bitmap 248 */ 249 // Visible for testing. 250 static int findBestSampleSize( 251 int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { 252 double wr = (double) actualWidth / desiredWidth; 253 double hr = (double) actualHeight / desiredHeight; 254 double ratio = Math.min(wr, hr); 255 float n = 1.0f; 256 while ((n * 2) <= ratio) { 257 n *= 2; 258 } 259 260 return (int) n; 261 } 262 } 263