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