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