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 com.android.volley.DefaultRetryPolicy; 20 import com.android.volley.NetworkResponse; 21 import com.android.volley.ParseError; 22 import com.android.volley.Request; 23 import com.android.volley.Response; 24 import com.android.volley.VolleyLog; 25 26 import android.graphics.Bitmap; 27 import android.graphics.Bitmap.Config; 28 import android.graphics.BitmapFactory; 29 30 /** 31 * A canned request for getting an image at a given URL and calling 32 * back with a decoded Bitmap. 33 */ 34 public class ImageRequest extends Request<Bitmap> { 35 /** Socket timeout in milliseconds for image requests */ 36 private static final int IMAGE_TIMEOUT_MS = 1000; 37 38 /** Default number of retries for image requests */ 39 private static final int IMAGE_MAX_RETRIES = 2; 40 41 /** Default backoff multiplier for image requests */ 42 private static final float IMAGE_BACKOFF_MULT = 2f; 43 44 private final Response.Listener<Bitmap> mListener; 45 private final Config mDecodeConfig; 46 private final int mMaxWidth; 47 private final int mMaxHeight; 48 49 /** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */ 50 private static final Object sDecodeLock = new Object(); 51 52 /** 53 * Creates a new image request, decoding to a maximum specified width and 54 * height. If both width and height are zero, the image will be decoded to 55 * its natural size. If one of the two is nonzero, that dimension will be 56 * clamped and the other one will be set to preserve the image's aspect 57 * ratio. If both width and height are nonzero, the image will be decoded to 58 * be fit in the rectangle of dimensions width x height while keeping its 59 * aspect ratio. 60 * 61 * @param url URL of the image 62 * @param listener Listener to receive the decoded bitmap 63 * @param maxWidth Maximum width to decode this bitmap to, or zero for none 64 * @param maxHeight Maximum height to decode this bitmap to, or zero for 65 * none 66 * @param decodeConfig Format to decode the bitmap to 67 * @param errorListener Error listener, or null to ignore errors 68 */ 69 public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, 70 Config decodeConfig, Response.ErrorListener errorListener) { 71 super(Method.GET, url, errorListener); 72 setRetryPolicy( 73 new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT)); 74 mListener = listener; 75 mDecodeConfig = decodeConfig; 76 mMaxWidth = maxWidth; 77 mMaxHeight = maxHeight; 78 } 79 80 @Override 81 public Priority getPriority() { 82 return Priority.LOW; 83 } 84 85 /** 86 * Scales one side of a rectangle to fit aspect ratio. 87 * 88 * @param maxPrimary Maximum size of the primary dimension (i.e. width for 89 * max width), or zero to maintain aspect ratio with secondary 90 * dimension 91 * @param maxSecondary Maximum size of the secondary dimension, or zero to 92 * maintain aspect ratio with primary dimension 93 * @param actualPrimary Actual size of the primary dimension 94 * @param actualSecondary Actual size of the secondary dimension 95 */ 96 private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, 97 int actualSecondary) { 98 // If no dominant value at all, just return the actual. 99 if (maxPrimary == 0 && maxSecondary == 0) { 100 return actualPrimary; 101 } 102 103 // If primary is unspecified, scale primary to match secondary's scaling ratio. 104 if (maxPrimary == 0) { 105 double ratio = (double) maxSecondary / (double) actualSecondary; 106 return (int) (actualPrimary * ratio); 107 } 108 109 if (maxSecondary == 0) { 110 return maxPrimary; 111 } 112 113 double ratio = (double) actualSecondary / (double) actualPrimary; 114 int resized = maxPrimary; 115 if (resized * ratio > maxSecondary) { 116 resized = (int) (maxSecondary / ratio); 117 } 118 return resized; 119 } 120 121 @Override 122 protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) { 123 // Serialize all decode on a global lock to reduce concurrent heap usage. 124 synchronized (sDecodeLock) { 125 try { 126 return doParse(response); 127 } catch (OutOfMemoryError e) { 128 VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl()); 129 return Response.error(new ParseError(e)); 130 } 131 } 132 } 133 134 /** 135 * The real guts of parseNetworkResponse. Broken out for readability. 136 */ 137 private Response<Bitmap> doParse(NetworkResponse response) { 138 byte[] data = response.data; 139 BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); 140 Bitmap bitmap = null; 141 if (mMaxWidth == 0 && mMaxHeight == 0) { 142 decodeOptions.inPreferredConfig = mDecodeConfig; 143 bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); 144 } else { 145 // If we have to resize this image, first get the natural bounds. 146 decodeOptions.inJustDecodeBounds = true; 147 BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); 148 int actualWidth = decodeOptions.outWidth; 149 int actualHeight = decodeOptions.outHeight; 150 151 // Then compute the dimensions we would ideally like to decode to. 152 int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, 153 actualWidth, actualHeight); 154 int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth, 155 actualHeight, actualWidth); 156 157 // Decode to the nearest power of two scaling factor. 158 decodeOptions.inJustDecodeBounds = false; 159 // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it? 160 // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED; 161 decodeOptions.inSampleSize = 162 findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); 163 Bitmap tempBitmap = 164 BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); 165 166 // If necessary, scale down to the maximal acceptable size. 167 if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || 168 tempBitmap.getHeight() > desiredHeight)) { 169 bitmap = Bitmap.createScaledBitmap(tempBitmap, 170 desiredWidth, desiredHeight, true); 171 tempBitmap.recycle(); 172 } else { 173 bitmap = tempBitmap; 174 } 175 } 176 177 if (bitmap == null) { 178 return Response.error(new ParseError()); 179 } else { 180 return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); 181 } 182 } 183 184 @Override 185 protected void deliverResponse(Bitmap response) { 186 mListener.onResponse(response); 187 } 188 189 /** 190 * Returns the largest power-of-two divisor for use in downscaling a bitmap 191 * that will not result in the scaling past the desired dimensions. 192 * 193 * @param actualWidth Actual width of the bitmap 194 * @param actualHeight Actual height of the bitmap 195 * @param desiredWidth Desired width of the bitmap 196 * @param desiredHeight Desired height of the bitmap 197 */ 198 // Visible for testing. 199 static int findBestSampleSize( 200 int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { 201 double wr = (double) actualWidth / desiredWidth; 202 double hr = (double) actualHeight / desiredHeight; 203 double ratio = Math.min(wr, hr); 204 float n = 1.0f; 205 while ((n * 2) <= ratio) { 206 n *= 2; 207 } 208 209 return (int) n; 210 } 211 } 212