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 android.graphics.Bitmap;
20import android.graphics.Bitmap.Config;
21import android.graphics.BitmapFactory;
22import android.widget.ImageView.ScaleType;
23
24import com.android.volley.DefaultRetryPolicy;
25import com.android.volley.NetworkResponse;
26import com.android.volley.ParseError;
27import com.android.volley.Request;
28import com.android.volley.Response;
29import 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 */
35public 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    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(new DefaultRetryPolicy(DEFAULT_IMAGE_TIMEOUT_MS, DEFAULT_IMAGE_MAX_RETRIES,
76                DEFAULT_IMAGE_BACKOFF_MULT));
77        mListener = listener;
78        mDecodeConfig = decodeConfig;
79        mMaxWidth = maxWidth;
80        mMaxHeight = maxHeight;
81        mScaleType = scaleType;
82    }
83
84    /**
85     * For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to
86     * the normal constructor with {@code ScaleType.CENTER_INSIDE}.
87     */
88    @Deprecated
89    public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
90            Config decodeConfig, Response.ErrorListener errorListener) {
91        this(url, listener, maxWidth, maxHeight,
92                ScaleType.CENTER_INSIDE, decodeConfig, errorListener);
93    }
94    @Override
95    public Priority getPriority() {
96        return Priority.LOW;
97    }
98
99    /**
100     * Scales one side of a rectangle to fit aspect ratio.
101     *
102     * @param maxPrimary Maximum size of the primary dimension (i.e. width for
103     *        max width), or zero to maintain aspect ratio with secondary
104     *        dimension
105     * @param maxSecondary Maximum size of the secondary dimension, or zero to
106     *        maintain aspect ratio with primary dimension
107     * @param actualPrimary Actual size of the primary dimension
108     * @param actualSecondary Actual size of the secondary dimension
109     * @param scaleType The ScaleType used to calculate the needed image size.
110     */
111    private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
112            int actualSecondary, ScaleType scaleType) {
113
114        // If no dominant value at all, just return the actual.
115        if ((maxPrimary == 0) && (maxSecondary == 0)) {
116            return actualPrimary;
117        }
118
119        // If ScaleType.FIT_XY fill the whole rectangle, ignore ratio.
120        if (scaleType == ScaleType.FIT_XY) {
121            if (maxPrimary == 0) {
122                return actualPrimary;
123            }
124            return maxPrimary;
125        }
126
127        // If primary is unspecified, scale primary to match secondary's scaling ratio.
128        if (maxPrimary == 0) {
129            double ratio = (double) maxSecondary / (double) actualSecondary;
130            return (int) (actualPrimary * ratio);
131        }
132
133        if (maxSecondary == 0) {
134            return maxPrimary;
135        }
136
137        double ratio = (double) actualSecondary / (double) actualPrimary;
138        int resized = maxPrimary;
139
140        // If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio.
141        if (scaleType == ScaleType.CENTER_CROP) {
142            if ((resized * ratio) < maxSecondary) {
143                resized = (int) (maxSecondary / ratio);
144            }
145            return resized;
146        }
147
148        if ((resized * ratio) > maxSecondary) {
149            resized = (int) (maxSecondary / ratio);
150        }
151        return resized;
152    }
153
154    @Override
155    protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
156        // Serialize all decode on a global lock to reduce concurrent heap usage.
157        synchronized (sDecodeLock) {
158            try {
159                return doParse(response);
160            } catch (OutOfMemoryError e) {
161                VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
162                return Response.error(new ParseError(e));
163            }
164        }
165    }
166
167    /**
168     * The real guts of parseNetworkResponse. Broken out for readability.
169     */
170    private Response<Bitmap> doParse(NetworkResponse response) {
171        byte[] data = response.data;
172        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
173        Bitmap bitmap = null;
174        if (mMaxWidth == 0 && mMaxHeight == 0) {
175            decodeOptions.inPreferredConfig = mDecodeConfig;
176            bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
177        } else {
178            // If we have to resize this image, first get the natural bounds.
179            decodeOptions.inJustDecodeBounds = true;
180            BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
181            int actualWidth = decodeOptions.outWidth;
182            int actualHeight = decodeOptions.outHeight;
183
184            // Then compute the dimensions we would ideally like to decode to.
185            int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
186                    actualWidth, actualHeight, mScaleType);
187            int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
188                    actualHeight, actualWidth, mScaleType);
189
190            // Decode to the nearest power of two scaling factor.
191            decodeOptions.inJustDecodeBounds = false;
192            // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
193            // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
194            decodeOptions.inSampleSize =
195                findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
196            Bitmap tempBitmap =
197                BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
198
199            // If necessary, scale down to the maximal acceptable size.
200            if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
201                    tempBitmap.getHeight() > desiredHeight)) {
202                bitmap = Bitmap.createScaledBitmap(tempBitmap,
203                        desiredWidth, desiredHeight, true);
204                tempBitmap.recycle();
205            } else {
206                bitmap = tempBitmap;
207            }
208        }
209
210        if (bitmap == null) {
211            return Response.error(new ParseError(response));
212        } else {
213            return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
214        }
215    }
216
217    @Override
218    protected void deliverResponse(Bitmap response) {
219        mListener.onResponse(response);
220    }
221
222    /**
223     * Returns the largest power-of-two divisor for use in downscaling a bitmap
224     * that will not result in the scaling past the desired dimensions.
225     *
226     * @param actualWidth Actual width of the bitmap
227     * @param actualHeight Actual height of the bitmap
228     * @param desiredWidth Desired width of the bitmap
229     * @param desiredHeight Desired height of the bitmap
230     */
231    // Visible for testing.
232    static int findBestSampleSize(
233            int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
234        double wr = (double) actualWidth / desiredWidth;
235        double hr = (double) actualHeight / desiredHeight;
236        double ratio = Math.min(wr, hr);
237        float n = 1.0f;
238        while ((n * 2) <= ratio) {
239            n *= 2;
240        }
241
242        return (int) n;
243    }
244}
245