Merge "Allow callers to set value of notifyAuthFailure"
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..219c63c
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="volley" default="help">
+
+    <!-- The local.properties file is created and updated by the 'android' tool.
+         It contains the path to the SDK. It should *NOT* be checked into
+         Version Control Systems. -->
+    <property file="local.properties" />
+
+    <!-- The ant.properties file can be created by you. It is only edited by the
+         'android' tool to add properties to it.
+         This is the place to change some Ant specific build properties.
+         Here are some properties you may want to change/update:
+
+         source.dir
+             The name of the source directory. Default is 'src'.
+         out.dir
+             The name of the output directory. Default is 'bin'.
+
+         For other overridable properties, look at the beginning of the rules
+         files in the SDK, at tools/ant/build.xml
+
+         Properties related to the SDK location or the project target should
+         be updated using the 'android' tool with the 'update' action.
+
+         This file is an integral part of the build system for your
+         application and should be checked into Version Control Systems.
+
+         -->
+    <property file="ant.properties" />
+
+    <!-- if sdk.dir was not set from one of the property file, then
+         get it from the ANDROID_HOME env var.
+         This must be done before we load project.properties since
+         the proguard config can use sdk.dir -->
+    <property environment="env" />
+    <condition property="sdk.dir" value="${env.ANDROID_HOME}">
+        <isset property="env.ANDROID_HOME" />
+    </condition>
+
+    <!-- The project.properties file is created and updated by the 'android'
+         tool, as well as ADT.
+
+         This contains project specific properties such as project target, and library
+         dependencies. Lower level build properties are stored in ant.properties
+         (or in .classpath for Eclipse projects).
+
+         This file is an integral part of the build system for your
+         application and should be checked into Version Control Systems. -->
+    <loadproperties srcFile="project.properties" />
+
+    <!-- quick check on sdk.dir -->
+    <fail
+            message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
+            unless="sdk.dir"
+    />
+
+    <!--
+        Import per project custom build rules if present at the root of the project.
+        This is the place to put custom intermediary targets such as:
+            -pre-build
+            -pre-compile
+            -post-compile (This is typically used for code obfuscation.
+                           Compiled code location: ${out.classes.absolute.dir}
+                           If this is not done in place, override ${out.dex.input.absolute.dir})
+            -post-package
+            -post-build
+            -pre-clean
+    -->
+    <import file="custom_rules.xml" optional="true" />
+
+    <!-- Import the actual build file.
+
+         To customize existing targets, there are two options:
+         - Customize only one target:
+             - copy/paste the target into this file, *before* the
+               <import> task.
+             - customize it to your needs.
+         - Customize the whole content of build.xml
+             - copy/paste the content of the rules files (minus the top node)
+               into this file, replacing the <import> task.
+             - customize to your needs.
+
+         ***********************
+         ****** IMPORTANT ******
+         ***********************
+         In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
+         in order to avoid having your file be overridden by tools such as "android update project"
+    -->
+    <!-- version-tag: 1 -->
+    <import file="${sdk.dir}/tools/ant/build.xml" />
+
+</project>
diff --git a/custom_rules.xml b/custom_rules.xml
new file mode 100644
index 0000000..1b94e5d
--- /dev/null
+++ b/custom_rules.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="volley-rules" default="help">
+
+  <!-- A rule to generate the JAR for inclusion in an Android
+       application. Output file will be bin/volley.jar -->
+  <target name="jar" depends="-compile">
+    <jar destfile="bin/volley.jar"
+         basedir="bin/classes" />
+  </target>
+</project>
diff --git a/proguard-project.txt b/proguard-project.txt
new file mode 100644
index 0000000..f2fe155
--- /dev/null
+++ b/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
diff --git a/project.properties b/project.properties
index 730e911..28341e2 100644
--- a/project.properties
+++ b/project.properties
@@ -9,3 +9,7 @@
 
 # Project target.
 target=android-14
+
+# Make sure to pass a valid value to renderscript
+# https://code.google.com/p/android/issues/detail?id=40487
+renderscript.opt.level=O0
diff --git a/src/com/android/volley/toolbox/DiskBasedCache.java b/src/com/android/volley/toolbox/DiskBasedCache.java
index 75bf4d1..9559a02 100644
--- a/src/com/android/volley/toolbox/DiskBasedCache.java
+++ b/src/com/android/volley/toolbox/DiskBasedCache.java
@@ -456,8 +456,8 @@
                     ? Collections.<String, String>emptyMap()
                     : new HashMap<String, String>(size);
             for (int i = 0; i < size; i++) {
-                String key = ois.readUTF();
-                String value = ois.readUTF();
+                String key = ois.readUTF().intern();
+                String value = ois.readUTF().intern();
                 result.put(key, value);
             }
             return result;
diff --git a/src/com/android/volley/toolbox/HurlStack.java b/src/com/android/volley/toolbox/HurlStack.java
index bbfb12a..97f94f0 100644
--- a/src/com/android/volley/toolbox/HurlStack.java
+++ b/src/com/android/volley/toolbox/HurlStack.java
@@ -146,13 +146,20 @@
     }
 
     /**
+     * Create an {@link HttpURLConnection} for the specified {@code url}.
+     */
+    protected HttpURLConnection createConnection(URL url) throws IOException {
+        return (HttpURLConnection) url.openConnection();
+    }
+
+    /**
      * Opens an {@link HttpURLConnection} with parameters.
      * @param url
      * @return an open connection
      * @throws IOException
      */
     private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
-        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        HttpURLConnection connection = createConnection(url);
 
         int timeoutMs = request.getTimeoutMs();
         connection.setConnectTimeout(timeoutMs);
diff --git a/src/com/android/volley/toolbox/ImageLoader.java b/src/com/android/volley/toolbox/ImageLoader.java
new file mode 100755
index 0000000..5735edd
--- /dev/null
+++ b/src/com/android/volley/toolbox/ImageLoader.java
@@ -0,0 +1,461 @@
+/**
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.os.Handler;
+import android.os.Looper;
+import android.widget.ImageView;
+
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.Response.ErrorListener;
+import com.android.volley.Response.Listener;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageRequest;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+
+/**
+ * Helper that handles loading and caching images from remote URLs.
+ *
+ * The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)}
+ * and to pass in the default image listener provided by
+ * {@link ImageLoader#getImageListener(ImageView, int, int)}. Note that all function calls to
+ * this class must be made from the main thead, and all responses will be delivered to the main
+ * thread as well.
+ */
+public class ImageLoader {
+    /** RequestQueue for dispatching ImageRequests onto. */
+    private final RequestQueue mRequestQueue;
+
+    /** Amount of time to wait after first response arrives before delivering all responses. */
+    private int mBatchResponseDelayMs = 100;
+
+    /** The cache implementation to be used as an L1 cache before calling into volley. */
+    private final ImageCache mCache;
+
+    /**
+     * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so
+     * that we can coalesce multiple requests to the same URL into a single network request.
+     */
+    private final HashMap<String, BatchedImageRequest> mInFlightRequests =
+            new HashMap<String, BatchedImageRequest>();
+
+    /** HashMap of the currently pending responses (waiting to be delivered). */
+    private final HashMap<String, BatchedImageRequest> mBatchedResponses =
+            new HashMap<String, BatchedImageRequest>();
+
+    /** Handler to the main thread. */
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+    /** Runnable for in-flight response delivery. */
+    private Runnable mRunnable;
+
+    /**
+     * Simple cache adapter interface. If provided to the ImageLoader, it
+     * will be used as an L1 cache before dispatch to Volley. Implementations
+     * must not block. Implementation with an LruCache is recommended.
+     */
+    public interface ImageCache {
+        public Bitmap getBitmap(String url);
+        public void putBitmap(String url, Bitmap bitmap);
+    }
+
+    /**
+     * Constructs a new ImageLoader.
+     * @param queue The RequestQueue to use for making image requests.
+     * @param imageCache The cache to use as an L1 cache.
+     */
+    public ImageLoader(RequestQueue queue, ImageCache imageCache) {
+        mRequestQueue = queue;
+        mCache = imageCache;
+    }
+
+    /**
+     * The default implementation of ImageListener which handles basic functionality
+     * of showing a default image until the network response is received, at which point
+     * it will switch to either the actual image or the error image.
+     * @param imageView The imageView that the listener is associated with.
+     * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist.
+     * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist.
+     */
+    public static ImageListener getImageListener(final ImageView view,
+            final int defaultImageResId, final int errorImageResId) {
+        return new ImageListener() {
+            @Override
+            public void onErrorResponse(VolleyError error) {
+                if (errorImageResId != 0) {
+                    view.setImageResource(errorImageResId);
+                }
+            }
+
+            @Override
+            public void onResponse(ImageContainer response, boolean isImmediate) {
+                if (response.getBitmap() != null) {
+                    view.setImageBitmap(response.getBitmap());
+                } else if (defaultImageResId != 0) {
+                    view.setImageResource(defaultImageResId);
+                }
+            }
+        };
+    }
+
+    /**
+     * Interface for the response handlers on image requests.
+     *
+     * The call flow is this:
+     * 1. Upon being  attached to a request, onResponse(response, true) will
+     * be invoked to reflect any cached data that was already available. If the
+     * data was available, response.getBitmap() will be non-null.
+     *
+     * 2. After a network response returns, only one of the following cases will happen:
+     *   - onResponse(response, false) will be called if the image was loaded.
+     *   or
+     *   - onErrorResponse will be called if there was an error loading the image.
+     */
+    public interface ImageListener extends ErrorListener {
+        /**
+         * Listens for non-error changes to the loading of the image request.
+         *
+         * @param response Holds all information pertaining to the request, as well
+         * as the bitmap (if it is loaded).
+         * @param isImmediate True if this was called during ImageLoader.get() variants.
+         * This can be used to differentiate between a cached image loading and a network
+         * image loading in order to, for example, run an animation to fade in network loaded
+         * images.
+         */
+        public void onResponse(ImageContainer response, boolean isImmediate);
+    }
+
+    /**
+     * Checks if the item is available in the cache.
+     * @param requestUrl The url of the remote image
+     * @param maxWidth The maximum width of the returned image.
+     * @param maxHeight The maximum height of the returned image.
+     * @return True if the item exists in cache, false otherwise.
+     */
+    public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
+        throwIfNotOnMainThread();
+
+        String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
+        return mCache.getBitmap(cacheKey) != null;
+    }
+
+    /**
+     * Returns an ImageContainer for the requested URL.
+     *
+     * The ImageContainer will contain either the specified default bitmap or the loaded bitmap.
+     * If the default was returned, the {@link ImageLoader} will be invoked when the
+     * request is fulfilled.
+     *
+     * @param requestUrl The URL of the image to be loaded.
+     * @param defaultImage Optional default image to return until the actual image is loaded.
+     */
+    public ImageContainer get(String requestUrl, final ImageListener listener) {
+        return get(requestUrl, listener, 0, 0);
+    }
+
+    /**
+     * Issues a bitmap request with the given URL if that image is not available
+     * in the cache, and returns a bitmap container that contains all of the data
+     * relating to the request (as well as the default image if the requested
+     * image is not available).
+     * @param requestUrl The url of the remote image
+     * @param imageListener The listener to call when the remote image is loaded
+     * @param maxWidth The maximum width of the returned image.
+     * @param maxHeight The maximum height of the returned image.
+     * @return A container object that contains all of the properties of the request, as well as
+     *     the currently available image (default if remote is not loaded).
+     */
+    public ImageContainer get(String requestUrl, ImageListener imageListener,
+            int maxWidth, int maxHeight) {
+        // only fulfill requests that were initiated from the main thread.
+        throwIfNotOnMainThread();
+
+        final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
+
+        // Try to look up the request in the cache of remote images.
+        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
+        if (cachedBitmap != null) {
+            // Return the cached bitmap.
+            ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
+            imageListener.onResponse(container, true);
+            return container;
+        }
+
+        // The bitmap did not exist in the cache, fetch it!
+        ImageContainer imageContainer =
+                new ImageContainer(null, requestUrl, cacheKey, imageListener);
+
+        // Update the caller to let them know that they should use the default bitmap.
+        imageListener.onResponse(imageContainer, true);
+
+        // Check to see if a request is already in-flight.
+        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
+        if (request != null) {
+            // If it is, add this request to the list of listeners.
+            request.addContainer(imageContainer);
+            return imageContainer;
+        }
+
+        // The request is not already in flight. Send the new request to the network and
+        // track it.
+        Request<?> newRequest =
+            new ImageRequest(requestUrl, new Listener<Bitmap>() {
+                @Override
+                public void onResponse(Bitmap response) {
+                    onGetImageSuccess(cacheKey, response);
+                }
+            }, maxWidth, maxHeight,
+            Config.RGB_565, new ErrorListener() {
+                @Override
+                public void onErrorResponse(VolleyError error) {
+                    onGetImageError(cacheKey, error);
+                }
+            });
+
+        mRequestQueue.add(newRequest);
+        mInFlightRequests.put(cacheKey,
+                new BatchedImageRequest(newRequest, imageContainer));
+        return imageContainer;
+    }
+
+    /**
+     * Sets the amount of time to wait after the first response arrives before delivering all
+     * responses. Batching can be disabled entirely by passing in 0.
+     * @param newBatchedResponseDelayMs The time in milliseconds to wait.
+     */
+    public void setBatchedResponseDelay(int newBatchedResponseDelayMs) {
+        mBatchResponseDelayMs = newBatchedResponseDelayMs;
+    }
+
+    /**
+     * Handler for when an image was successfully loaded.
+     * @param cacheKey The cache key that is associated with the image request.
+     * @param response The bitmap that was returned from the network.
+     */
+    private void onGetImageSuccess(String cacheKey, Bitmap response) {
+        // cache the image that was fetched.
+        mCache.putBitmap(cacheKey, response);
+
+        // remove the request from the list of in-flight requests.
+        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
+
+        if (request != null) {
+            // Update the response bitmap.
+            request.mResponseBitmap = response;
+
+            // Send the batched response
+            batchResponse(cacheKey, request, null);
+        }
+    }
+
+    /**
+     * Handler for when an image failed to load.
+     * @param cacheKey The cache key that is associated with the image request.
+     */
+    private void onGetImageError(String cacheKey, VolleyError error) {
+        // Notify the requesters that something failed via a null result.
+        // Remove this request from the list of in-flight requests.
+        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
+
+        if (request != null) {
+            // Send the batched response
+            batchResponse(cacheKey, request, error);
+        }
+    }
+
+    /**
+     * Container object for all of the data surrounding an image request.
+     */
+    public class ImageContainer {
+        /**
+         * The most relevant bitmap for the container. If the image was in cache, the
+         * Holder to use for the final bitmap (the one that pairs to the requested URL).
+         */
+        private Bitmap mBitmap;
+
+        private final ImageListener mListener;
+
+        /** The cache key that was associated with the request */
+        private final String mCacheKey;
+
+        /** The request URL that was specified */
+        private final String mRequestUrl;
+
+        /**
+         * Constructs a BitmapContainer object.
+         * @param bitmap The final bitmap (if it exists).
+         * @param requestUrl The requested URL for this container.
+         * @param cacheKey The cache key that identifies the requested URL for this container.
+         */
+        public ImageContainer(Bitmap bitmap, String requestUrl,
+                String cacheKey, ImageListener listener) {
+            mBitmap = bitmap;
+            mRequestUrl = requestUrl;
+            mCacheKey = cacheKey;
+            mListener = listener;
+        }
+
+        /**
+         * Releases interest in the in-flight request (and cancels it if no one else is listening).
+         */
+        public void cancelRequest() {
+            if (mListener == null) {
+                return;
+            }
+
+            BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
+            if (request != null) {
+                boolean canceled = request.removeContainerAndCancelIfNecessary(this);
+                if (canceled) {
+                    mInFlightRequests.remove(mCacheKey);
+                }
+            } else {
+                // check to see if it is already batched for delivery.
+                request = mBatchedResponses.get(mCacheKey);
+                if (request != null) {
+                    request.removeContainerAndCancelIfNecessary(this);
+                    if (request.mContainers.size() == 0) {
+                        mBatchedResponses.remove(mCacheKey);
+                    }
+                }
+            }
+        }
+
+        /**
+         * Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
+         */
+        public Bitmap getBitmap() {
+            return mBitmap;
+        }
+
+        /**
+         * Returns the requested URL for this container.
+         */
+        public String getRequestUrl() {
+            return mRequestUrl;
+        }
+    }
+
+    /**
+     * Wrapper class used to map a Request to the set of active ImageContainer objects that are
+     * interested in its results.
+     */
+    private class BatchedImageRequest {
+        /** The request being tracked */
+        private final Request<?> mRequest;
+
+        /** The result of the request being tracked by this item */
+        private Bitmap mResponseBitmap;
+
+        /** List of all of the active ImageContainers that are interested in the request */
+        private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
+
+        /**
+         * Constructs a new BatchedImageRequest object
+         * @param request The request being tracked
+         * @param container The ImageContainer of the person who initiated the request.
+         */
+        public BatchedImageRequest(Request<?> request, ImageContainer container) {
+            mRequest = request;
+            mContainers.add(container);
+        }
+
+        /**
+         * Adds another ImageContainer to the list of those interested in the results of
+         * the request.
+         */
+        public void addContainer(ImageContainer container) {
+            mContainers.add(container);
+        }
+
+        /**
+         * Detatches the bitmap container from the request and cancels the request if no one is
+         * left listening.
+         * @param container The container to remove from the list
+         * @return True if the request was canceled, false otherwise.
+         */
+        public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
+            mContainers.remove(container);
+            if (mContainers.size() == 0) {
+                mRequest.cancel();
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Starts the runnable for batched delivery of responses if it is not already started.
+     * @param cacheKey The cacheKey of the response being delivered.
+     * @param request The BatchedImageRequest to be delivered.
+     * @param error The volley error associated with the request (if applicable).
+     */
+    private void batchResponse(String cacheKey, BatchedImageRequest request,
+            final VolleyError error) {
+        mBatchedResponses.put(cacheKey, request);
+        // If we don't already have a batch delivery runnable in flight, make a new one.
+        // Note that this will be used to deliver responses to all callers in mBatchedResponses.
+        if (mRunnable == null) {
+            mRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    for (BatchedImageRequest bir : mBatchedResponses.values()) {
+                        for (ImageContainer container : bir.mContainers) {
+                            // If one of the callers in the batched request canceled the request
+                            // after the response was received but before it was delivered,
+                            // skip them.
+                            if (container.mListener == null) {
+                                continue;
+                            }
+                            if (error == null) {
+                                container.mBitmap = bir.mResponseBitmap;
+                                container.mListener.onResponse(container, false);
+                            } else {
+                                container.mListener.onErrorResponse(error);
+                            }
+                        }
+                    }
+                    mBatchedResponses.clear();
+                    mRunnable = null;
+                }
+
+            };
+            // Post the runnable.
+            mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
+        }
+    }
+
+    private void throwIfNotOnMainThread() {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
+        }
+    }
+    /**
+     * Creates a cache key for use with the L1 cache.
+     * @param url The URL of the request.
+     * @param maxWidth The max-width of the output.
+     * @param maxHeight The max-height of the output.
+     */
+    private static String getCacheKey(String url, int maxWidth, int maxHeight) {
+        return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
+                .append("#H").append(maxHeight).append(url).toString();
+    }
+}
diff --git a/src/com/android/volley/toolbox/NetworkImageView.java b/src/com/android/volley/toolbox/NetworkImageView.java
new file mode 100644
index 0000000..0d1d6be
--- /dev/null
+++ b/src/com/android/volley/toolbox/NetworkImageView.java
@@ -0,0 +1,197 @@
+/**
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader.ImageContainer;
+import com.android.volley.toolbox.ImageLoader.ImageListener;
+
+/**
+ * Handles fetching an image from a URL as well as the life-cycle of the
+ * associated request.
+ */
+public class NetworkImageView extends ImageView {
+    /** The URL of the network image to load */
+    private String mUrl;
+
+    /**
+     * Resource ID of the image to be used as a placeholder until the network image is loaded.
+     */
+    private int mDefaultImageId;
+
+    /**
+     * Resource ID of the image to be used if the network response fails.
+     */
+    private int mErrorImageId;
+
+    /** Local copy of the ImageLoader. */
+    private ImageLoader mImageLoader;
+
+    /** Current ImageContainer. (either in-flight or finished) */
+    private ImageContainer mImageContainer;
+
+    public NetworkImageView(Context context) {
+        this(context, null);
+    }
+
+    public NetworkImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public NetworkImageView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    /**
+     * Sets URL of the image that should be loaded into this view. Note that calling this will
+     * immediately either set the cached image (if available) or the default image specified by
+     * {@link NetworkImageView#setDefaultImageResId(int)} on the view.
+     *
+     * NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} and
+     * {@link NetworkImageView#setErrorImageResId(int)} should be called prior to calling
+     * this function.
+     *
+     * @param url The URL that should be loaded into this ImageView.
+     * @param imageLoader ImageLoader that will be used to make the request.
+     */
+    public void setImageUrl(String url, ImageLoader imageLoader) {
+        mUrl = url;
+        mImageLoader = imageLoader;
+        // The URL has potentially changed. See if we need to load it.
+        loadImageIfNecessary(false);
+    }
+
+    /**
+     * Sets the default image resource ID to be used for this view until the attempt to load it
+     * completes.
+     */
+    public void setDefaultImageResId(int defaultImage) {
+        mDefaultImageId = defaultImage;
+    }
+
+    /**
+     * Sets the error image resource ID to be used for this view in the event that the image
+     * requested fails to load.
+     */
+    public void setErrorImageResId(int errorImage) {
+        mErrorImageId = errorImage;
+    }
+
+    /**
+     * Loads the image for the view if it isn't already loaded.
+     * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
+     */
+    private void loadImageIfNecessary(final boolean isInLayoutPass) {
+        int width = getWidth();
+        int height = getHeight();
+
+        // if the view's bounds aren't known yet, hold off on loading the image.
+        if (width == 0 && height == 0) {
+            return;
+        }
+
+        // if the URL to be loaded in this view is empty, cancel any old requests and clear the
+        // currently loaded image.
+        if (TextUtils.isEmpty(mUrl)) {
+            if (mImageContainer != null) {
+                mImageContainer.cancelRequest();
+                mImageContainer = null;
+            }
+            setImageBitmap(null);
+            return;
+        }
+
+        // if there was an old request in this view, check if it needs to be canceled.
+        if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
+            if (mImageContainer.getRequestUrl().equals(mUrl)) {
+                // if the request is from the same URL, return.
+                return;
+            } else {
+                // if there is a pre-existing request, cancel it if it's fetching a different URL.
+                mImageContainer.cancelRequest();
+                setImageBitmap(null);
+            }
+        }
+
+        // The pre-existing content of this view didn't match the current URL. Load the new image
+        // from the network.
+        ImageContainer newContainer = mImageLoader.get(mUrl,
+                new ImageListener() {
+                    @Override
+                    public void onErrorResponse(VolleyError error) {
+                        if (mErrorImageId != 0) {
+                            setImageResource(mErrorImageId);
+                        }
+                    }
+
+                    @Override
+                    public void onResponse(final ImageContainer response, boolean isImmediate) {
+                        // If this was an immediate response that was delivered inside of a layout
+                        // pass do not set the image immediately as it will trigger a requestLayout
+                        // inside of a layout. Instead, defer setting the image by posting back to
+                        // the main thread.
+                        if (isImmediate && isInLayoutPass) {
+                            post(new Runnable() {
+                                @Override
+                                public void run() {
+                                    onResponse(response, false);
+                                }
+                            });
+                            return;
+                        }
+
+                        if (response.getBitmap() != null) {
+                            setImageBitmap(response.getBitmap());
+                        } else if (mDefaultImageId != 0) {
+                            setImageResource(mDefaultImageId);
+                        }
+                    }
+                });
+
+        // update the ImageContainer to be the new bitmap container.
+        mImageContainer = newContainer;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        loadImageIfNecessary(true);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        if (mImageContainer != null) {
+            // If the view was bound to an image request, cancel it and clear
+            // out the image from the view.
+            mImageContainer.cancelRequest();
+            setImageBitmap(null);
+            // also clear out the container so we can reload the image if necessary.
+            mImageContainer = null;
+        }
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        invalidate();
+    }
+}
diff --git a/src/com/android/volley/toolbox/Volley.java b/src/com/android/volley/toolbox/Volley.java
index bba1153..1304045 100644
--- a/src/com/android/volley/toolbox/Volley.java
+++ b/src/com/android/volley/toolbox/Volley.java
@@ -36,9 +36,10 @@
      * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
      *
      * @param context A {@link Context} to use for creating the cache dir.
+     * @param stack An {@link HttpStack} to use for the network, or null for default.
      * @return A started {@link RequestQueue} instance.
      */
-    public static RequestQueue newRequestQueue(Context context) {
+    public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
         File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
 
         String userAgent = "volley/0";
@@ -50,13 +51,14 @@
         } catch (NameNotFoundException e) {
         }
 
-        HttpStack stack;
-        if (Build.VERSION.SDK_INT >= 9) {
-            stack = new HurlStack();
-        } else {
-            // Prior to Gingerbread, HttpUrlConnection was unreliable.
-            // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
-            stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
+        if (stack == null) {
+            if (Build.VERSION.SDK_INT >= 9) {
+                stack = new HurlStack();
+            } else {
+                // Prior to Gingerbread, HttpUrlConnection was unreliable.
+                // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
+                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
+            }
         }
 
         Network network = new BasicNetwork(stack);
@@ -66,4 +68,14 @@
 
         return queue;
     }
+
+    /**
+     * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
+     *
+     * @param context A {@link Context} to use for creating the cache dir.
+     * @return A started {@link RequestQueue} instance.
+     */
+    public static RequestQueue newRequestQueue(Context context) {
+        return newRequestQueue(context, null);
+    }
 }