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