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 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