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;
29
30/**
31 * A canned request for getting an image at a given URL and calling
32 * back with a decoded Bitmap.
33 */
34public 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(response));
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