Home | History | Annotate | Download | only in toolbox
      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