aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/android
diff options
context:
space:
mode:
authorFicus Kirkpatrick <ficus@android.com>2014-11-29 09:32:54 -0800
committerFicus Kirkpatrick <ficus@android.com>2014-12-05 15:46:44 -0800
commitb9b8dc3d98fb1a8c3f02c2c2fcc18cbd344c05cb (patch)
tree1f4a33c24f608d79781de54e5d1ff5f3072fa261 /src/main/java/com/android
parent008e0cc8e51ef9e91110d91d4d662d0d86b252a1 (diff)
downloadvolley-b9b8dc3d98fb1a8c3f02c2c2fcc18cbd344c05cb.tar.gz
Migrate from Gradle to Maven.
- Restructure source to src/{main,test} style - Add pom.xml and update Android.mk - Migrate all tests to JUnit4 and Robolectric - RequestQueueTest is currently @Ignored as fixing it will involve more extensive refactoring. - Main library still builds in Gradle; tests do not Change-Id: I1edc53bb1a54f64d3e806e4572901295ef63e2ca
Diffstat (limited to 'src/main/java/com/android')
-rw-r--r--src/main/java/com/android/volley/AuthFailureError.java61
-rw-r--r--src/main/java/com/android/volley/Cache.java97
-rw-r--r--src/main/java/com/android/volley/CacheDispatcher.java158
-rw-r--r--src/main/java/com/android/volley/DefaultRetryPolicy.java105
-rw-r--r--src/main/java/com/android/volley/ExecutorDelivery.java118
-rw-r--r--src/main/java/com/android/volley/Network.java30
-rw-r--r--src/main/java/com/android/volley/NetworkDispatcher.java152
-rw-r--r--src/main/java/com/android/volley/NetworkError.java38
-rw-r--r--src/main/java/com/android/volley/NetworkResponse.java73
-rw-r--r--src/main/java/com/android/volley/NoConnectionError.java31
-rw-r--r--src/main/java/com/android/volley/ParseError.java36
-rw-r--r--src/main/java/com/android/volley/Request.java603
-rw-r--r--src/main/java/com/android/volley/RequestQueue.java286
-rw-r--r--src/main/java/com/android/volley/Response.java85
-rw-r--r--src/main/java/com/android/volley/ResponseDelivery.java35
-rw-r--r--src/main/java/com/android/volley/RetryPolicy.java41
-rw-r--r--src/main/java/com/android/volley/ServerError.java34
-rw-r--r--src/main/java/com/android/volley/TimeoutError.java23
-rw-r--r--src/main/java/com/android/volley/VolleyError.java57
-rw-r--r--src/main/java/com/android/volley/VolleyLog.java176
-rw-r--r--src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java102
-rw-r--r--src/main/java/com/android/volley/toolbox/Authenticator.java36
-rw-r--r--src/main/java/com/android/volley/toolbox/BasicNetwork.java266
-rw-r--r--src/main/java/com/android/volley/toolbox/ByteArrayPool.java135
-rw-r--r--src/main/java/com/android/volley/toolbox/ClearCacheRequest.java70
-rw-r--r--src/main/java/com/android/volley/toolbox/DiskBasedCache.java562
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpClientStack.java194
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpHeaderParser.java137
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpStack.java45
-rw-r--r--src/main/java/com/android/volley/toolbox/HurlStack.java245
-rw-r--r--src/main/java/com/android/volley/toolbox/ImageLoader.java482
-rw-r--r--src/main/java/com/android/volley/toolbox/ImageRequest.java211
-rw-r--r--src/main/java/com/android/volley/toolbox/JsonArrayRequest.java58
-rw-r--r--src/main/java/com/android/volley/toolbox/JsonObjectRequest.java76
-rw-r--r--src/main/java/com/android/volley/toolbox/JsonRequest.java102
-rw-r--r--src/main/java/com/android/volley/toolbox/NetworkImageView.java219
-rw-r--r--src/main/java/com/android/volley/toolbox/NoCache.java49
-rw-r--r--src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java93
-rw-r--r--src/main/java/com/android/volley/toolbox/RequestFuture.java153
-rw-r--r--src/main/java/com/android/volley/toolbox/StringRequest.java73
-rw-r--r--src/main/java/com/android/volley/toolbox/Volley.java80
41 files changed, 5627 insertions, 0 deletions
diff --git a/src/main/java/com/android/volley/AuthFailureError.java b/src/main/java/com/android/volley/AuthFailureError.java
new file mode 100644
index 0000000..7bb2e15
--- /dev/null
+++ b/src/main/java/com/android/volley/AuthFailureError.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2011 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;
+
+import android.content.Intent;
+
+import com.android.volley.NetworkResponse;
+import com.android.volley.VolleyError;
+
+/**
+ * Error indicating that there was an authentication failure when performing a Request.
+ */
+@SuppressWarnings("serial")
+public class AuthFailureError extends VolleyError {
+ /** An intent that can be used to resolve this exception. (Brings up the password dialog.) */
+ private Intent mResolutionIntent;
+
+ public AuthFailureError() { }
+
+ public AuthFailureError(Intent intent) {
+ mResolutionIntent = intent;
+ }
+
+ public AuthFailureError(NetworkResponse response) {
+ super(response);
+ }
+
+ public AuthFailureError(String message) {
+ super(message);
+ }
+
+ public AuthFailureError(String message, Exception reason) {
+ super(message, reason);
+ }
+
+ public Intent getResolutionIntent() {
+ return mResolutionIntent;
+ }
+
+ @Override
+ public String getMessage() {
+ if (mResolutionIntent != null) {
+ return "User needs to (re)enter credentials.";
+ }
+ return super.getMessage();
+ }
+}
diff --git a/src/main/java/com/android/volley/Cache.java b/src/main/java/com/android/volley/Cache.java
new file mode 100644
index 0000000..eafd118
--- /dev/null
+++ b/src/main/java/com/android/volley/Cache.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2011 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;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * An interface for a cache keyed by a String with a byte array as data.
+ */
+public interface Cache {
+ /**
+ * Retrieves an entry from the cache.
+ * @param key Cache key
+ * @return An {@link Entry} or null in the event of a cache miss
+ */
+ public Entry get(String key);
+
+ /**
+ * Adds or replaces an entry to the cache.
+ * @param key Cache key
+ * @param entry Data to store and metadata for cache coherency, TTL, etc.
+ */
+ public void put(String key, Entry entry);
+
+ /**
+ * Performs any potentially long-running actions needed to initialize the cache;
+ * will be called from a worker thread.
+ */
+ public void initialize();
+
+ /**
+ * Invalidates an entry in the cache.
+ * @param key Cache key
+ * @param fullExpire True to fully expire the entry, false to soft expire
+ */
+ public void invalidate(String key, boolean fullExpire);
+
+ /**
+ * Removes an entry from the cache.
+ * @param key Cache key
+ */
+ public void remove(String key);
+
+ /**
+ * Empties the cache.
+ */
+ public void clear();
+
+ /**
+ * Data and metadata for an entry returned by the cache.
+ */
+ public static class Entry {
+ /** The data returned from cache. */
+ public byte[] data;
+
+ /** ETag for cache coherency. */
+ public String etag;
+
+ /** Date of this response as reported by the server. */
+ public long serverDate;
+
+ /** TTL for this record. */
+ public long ttl;
+
+ /** Soft TTL for this record. */
+ public long softTtl;
+
+ /** Immutable response headers as received from server; must be non-null. */
+ public Map<String, String> responseHeaders = Collections.emptyMap();
+
+ /** True if the entry is expired. */
+ public boolean isExpired() {
+ return this.ttl < System.currentTimeMillis();
+ }
+
+ /** True if a refresh is needed from the original data source. */
+ public boolean refreshNeeded() {
+ return this.softTtl < System.currentTimeMillis();
+ }
+ }
+
+}
diff --git a/src/main/java/com/android/volley/CacheDispatcher.java b/src/main/java/com/android/volley/CacheDispatcher.java
new file mode 100644
index 0000000..18d219b
--- /dev/null
+++ b/src/main/java/com/android/volley/CacheDispatcher.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2011 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;
+
+import android.os.Process;
+
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Provides a thread for performing cache triage on a queue of requests.
+ *
+ * Requests added to the specified cache queue are resolved from cache.
+ * Any deliverable response is posted back to the caller via a
+ * {@link ResponseDelivery}. Cache misses and responses that require
+ * refresh are enqueued on the specified network queue for processing
+ * by a {@link NetworkDispatcher}.
+ */
+public class CacheDispatcher extends Thread {
+
+ private static final boolean DEBUG = VolleyLog.DEBUG;
+
+ /** The queue of requests coming in for triage. */
+ private final BlockingQueue<Request<?>> mCacheQueue;
+
+ /** The queue of requests going out to the network. */
+ private final BlockingQueue<Request<?>> mNetworkQueue;
+
+ /** The cache to read from. */
+ private final Cache mCache;
+
+ /** For posting responses. */
+ private final ResponseDelivery mDelivery;
+
+ /** Used for telling us to die. */
+ private volatile boolean mQuit = false;
+
+ /**
+ * Creates a new cache triage dispatcher thread. You must call {@link #start()}
+ * in order to begin processing.
+ *
+ * @param cacheQueue Queue of incoming requests for triage
+ * @param networkQueue Queue to post requests that require network to
+ * @param cache Cache interface to use for resolution
+ * @param delivery Delivery interface to use for posting responses
+ */
+ public CacheDispatcher(
+ BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue,
+ Cache cache, ResponseDelivery delivery) {
+ mCacheQueue = cacheQueue;
+ mNetworkQueue = networkQueue;
+ mCache = cache;
+ mDelivery = delivery;
+ }
+
+ /**
+ * Forces this dispatcher to quit immediately. If any requests are still in
+ * the queue, they are not guaranteed to be processed.
+ */
+ public void quit() {
+ mQuit = true;
+ interrupt();
+ }
+
+ @Override
+ public void run() {
+ if (DEBUG) VolleyLog.v("start new dispatcher");
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ // Make a blocking call to initialize the cache.
+ mCache.initialize();
+
+ while (true) {
+ try {
+ // Get a request from the cache triage queue, blocking until
+ // at least one is available.
+ final Request<?> request = mCacheQueue.take();
+ request.addMarker("cache-queue-take");
+
+ // If the request has been canceled, don't bother dispatching it.
+ if (request.isCanceled()) {
+ request.finish("cache-discard-canceled");
+ continue;
+ }
+
+ // Attempt to retrieve this item from cache.
+ Cache.Entry entry = mCache.get(request.getCacheKey());
+ if (entry == null) {
+ request.addMarker("cache-miss");
+ // Cache miss; send off to the network dispatcher.
+ mNetworkQueue.put(request);
+ continue;
+ }
+
+ // If it is completely expired, just send it to the network.
+ if (entry.isExpired()) {
+ request.addMarker("cache-hit-expired");
+ request.setCacheEntry(entry);
+ mNetworkQueue.put(request);
+ continue;
+ }
+
+ // We have a cache hit; parse its data for delivery back to the request.
+ request.addMarker("cache-hit");
+ Response<?> response = request.parseNetworkResponse(
+ new NetworkResponse(entry.data, entry.responseHeaders));
+ request.addMarker("cache-hit-parsed");
+
+ if (!entry.refreshNeeded()) {
+ // Completely unexpired cache hit. Just deliver the response.
+ mDelivery.postResponse(request, response);
+ } else {
+ // Soft-expired cache hit. We can deliver the cached response,
+ // but we need to also send the request to the network for
+ // refreshing.
+ request.addMarker("cache-hit-refresh-needed");
+ request.setCacheEntry(entry);
+
+ // Mark the response as intermediate.
+ response.intermediate = true;
+
+ // Post the intermediate response back to the user and have
+ // the delivery then forward the request along to the network.
+ mDelivery.postResponse(request, response, new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mNetworkQueue.put(request);
+ } catch (InterruptedException e) {
+ // Not much we can do about this.
+ }
+ }
+ });
+ }
+
+ } catch (InterruptedException e) {
+ // We may have been interrupted because it was time to quit.
+ if (mQuit) {
+ return;
+ }
+ continue;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/DefaultRetryPolicy.java b/src/main/java/com/android/volley/DefaultRetryPolicy.java
new file mode 100644
index 0000000..d8abab0
--- /dev/null
+++ b/src/main/java/com/android/volley/DefaultRetryPolicy.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2011 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;
+
+/**
+ * Default retry policy for requests.
+ */
+public class DefaultRetryPolicy implements RetryPolicy {
+ /** The current timeout in milliseconds. */
+ private int mCurrentTimeoutMs;
+
+ /** The current retry count. */
+ private int mCurrentRetryCount;
+
+ /** The maximum number of attempts. */
+ private final int mMaxNumRetries;
+
+ /** The backoff multiplier for the policy. */
+ private final float mBackoffMultiplier;
+
+ /** The default socket timeout in milliseconds */
+ public static final int DEFAULT_TIMEOUT_MS = 2500;
+
+ /** The default number of retries */
+ public static final int DEFAULT_MAX_RETRIES = 1;
+
+ /** The default backoff multiplier */
+ public static final float DEFAULT_BACKOFF_MULT = 1f;
+
+ /**
+ * Constructs a new retry policy using the default timeouts.
+ */
+ public DefaultRetryPolicy() {
+ this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
+ }
+
+ /**
+ * Constructs a new retry policy.
+ * @param initialTimeoutMs The initial timeout for the policy.
+ * @param maxNumRetries The maximum number of retries.
+ * @param backoffMultiplier Backoff multiplier for the policy.
+ */
+ public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
+ mCurrentTimeoutMs = initialTimeoutMs;
+ mMaxNumRetries = maxNumRetries;
+ mBackoffMultiplier = backoffMultiplier;
+ }
+
+ /**
+ * Returns the current timeout.
+ */
+ @Override
+ public int getCurrentTimeout() {
+ return mCurrentTimeoutMs;
+ }
+
+ /**
+ * Returns the current retry count.
+ */
+ @Override
+ public int getCurrentRetryCount() {
+ return mCurrentRetryCount;
+ }
+
+ /**
+ * Returns the backoff multiplier for the policy.
+ */
+ public float getBackoffMultiplier() {
+ return mBackoffMultiplier;
+ }
+
+ /**
+ * Prepares for the next retry by applying a backoff to the timeout.
+ * @param error The error code of the last attempt.
+ */
+ @Override
+ public void retry(VolleyError error) throws VolleyError {
+ mCurrentRetryCount++;
+ mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
+ if (!hasAttemptRemaining()) {
+ throw error;
+ }
+ }
+
+ /**
+ * Returns true if this policy has attempts remaining, false otherwise.
+ */
+ protected boolean hasAttemptRemaining() {
+ return mCurrentRetryCount <= mMaxNumRetries;
+ }
+}
diff --git a/src/main/java/com/android/volley/ExecutorDelivery.java b/src/main/java/com/android/volley/ExecutorDelivery.java
new file mode 100644
index 0000000..1babfcd
--- /dev/null
+++ b/src/main/java/com/android/volley/ExecutorDelivery.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2011 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;
+
+import android.os.Handler;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Delivers responses and errors.
+ */
+public class ExecutorDelivery implements ResponseDelivery {
+ /** Used for posting responses, typically to the main thread. */
+ private final Executor mResponsePoster;
+
+ /**
+ * Creates a new response delivery interface.
+ * @param handler {@link Handler} to post responses on
+ */
+ public ExecutorDelivery(final Handler handler) {
+ // Make an Executor that just wraps the handler.
+ mResponsePoster = new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ handler.post(command);
+ }
+ };
+ }
+
+ /**
+ * Creates a new response delivery interface, mockable version
+ * for testing.
+ * @param executor For running delivery tasks
+ */
+ public ExecutorDelivery(Executor executor) {
+ mResponsePoster = executor;
+ }
+
+ @Override
+ public void postResponse(Request<?> request, Response<?> response) {
+ postResponse(request, response, null);
+ }
+
+ @Override
+ public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
+ request.markDelivered();
+ request.addMarker("post-response");
+ mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
+ }
+
+ @Override
+ public void postError(Request<?> request, VolleyError error) {
+ request.addMarker("post-error");
+ Response<?> response = Response.error(error);
+ mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));
+ }
+
+ /**
+ * A Runnable used for delivering network responses to a listener on the
+ * main thread.
+ */
+ @SuppressWarnings("rawtypes")
+ private class ResponseDeliveryRunnable implements Runnable {
+ private final Request mRequest;
+ private final Response mResponse;
+ private final Runnable mRunnable;
+
+ public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
+ mRequest = request;
+ mResponse = response;
+ mRunnable = runnable;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void run() {
+ // If this request has canceled, finish it and don't deliver.
+ if (mRequest.isCanceled()) {
+ mRequest.finish("canceled-at-delivery");
+ return;
+ }
+
+ // Deliver a normal response or error, depending.
+ if (mResponse.isSuccess()) {
+ mRequest.deliverResponse(mResponse.result);
+ } else {
+ mRequest.deliverError(mResponse.error);
+ }
+
+ // If this is an intermediate response, add a marker, otherwise we're done
+ // and the request can be finished.
+ if (mResponse.intermediate) {
+ mRequest.addMarker("intermediate-response");
+ } else {
+ mRequest.finish("done");
+ }
+
+ // If we have been provided a post-delivery runnable, run it.
+ if (mRunnable != null) {
+ mRunnable.run();
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/Network.java b/src/main/java/com/android/volley/Network.java
new file mode 100644
index 0000000..ab45830
--- /dev/null
+++ b/src/main/java/com/android/volley/Network.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2011 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;
+
+/**
+ * An interface for performing requests.
+ */
+public interface Network {
+ /**
+ * Performs the specified request.
+ * @param request Request to process
+ * @return A {@link NetworkResponse} with data and caching metadata; will never be null
+ * @throws VolleyError on errors
+ */
+ public NetworkResponse performRequest(Request<?> request) throws VolleyError;
+}
diff --git a/src/main/java/com/android/volley/NetworkDispatcher.java b/src/main/java/com/android/volley/NetworkDispatcher.java
new file mode 100644
index 0000000..beb7861
--- /dev/null
+++ b/src/main/java/com/android/volley/NetworkDispatcher.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2011 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;
+
+import android.annotation.TargetApi;
+import android.net.TrafficStats;
+import android.os.Build;
+import android.os.Process;
+import android.os.SystemClock;
+
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Provides a thread for performing network dispatch from a queue of requests.
+ *
+ * Requests added to the specified queue are processed from the network via a
+ * specified {@link Network} interface. Responses are committed to cache, if
+ * eligible, using a specified {@link Cache} interface. Valid responses and
+ * errors are posted back to the caller via a {@link ResponseDelivery}.
+ */
+public class NetworkDispatcher extends Thread {
+ /** The queue of requests to service. */
+ private final BlockingQueue<Request<?>> mQueue;
+ /** The network interface for processing requests. */
+ private final Network mNetwork;
+ /** The cache to write to. */
+ private final Cache mCache;
+ /** For posting responses and errors. */
+ private final ResponseDelivery mDelivery;
+ /** Used for telling us to die. */
+ private volatile boolean mQuit = false;
+
+ /**
+ * Creates a new network dispatcher thread. You must call {@link #start()}
+ * in order to begin processing.
+ *
+ * @param queue Queue of incoming requests for triage
+ * @param network Network interface to use for performing requests
+ * @param cache Cache interface to use for writing responses to cache
+ * @param delivery Delivery interface to use for posting responses
+ */
+ public NetworkDispatcher(BlockingQueue<Request<?>> queue,
+ Network network, Cache cache,
+ ResponseDelivery delivery) {
+ mQueue = queue;
+ mNetwork = network;
+ mCache = cache;
+ mDelivery = delivery;
+ }
+
+ /**
+ * Forces this dispatcher to quit immediately. If any requests are still in
+ * the queue, they are not guaranteed to be processed.
+ */
+ public void quit() {
+ mQuit = true;
+ interrupt();
+ }
+
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void addTrafficStatsTag(Request<?> request) {
+ // Tag the request (if API >= 14)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ TrafficStats.setThreadStatsTag(request.getTrafficStatsTag());
+ }
+ }
+
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ while (true) {
+ long startTimeMs = SystemClock.elapsedRealtime();
+ Request<?> request;
+ try {
+ // Take a request from the queue.
+ request = mQueue.take();
+ } catch (InterruptedException e) {
+ // We may have been interrupted because it was time to quit.
+ if (mQuit) {
+ return;
+ }
+ continue;
+ }
+
+ try {
+ request.addMarker("network-queue-take");
+
+ // If the request was cancelled already, do not perform the
+ // network request.
+ if (request.isCanceled()) {
+ request.finish("network-discard-cancelled");
+ continue;
+ }
+
+ addTrafficStatsTag(request);
+
+ // Perform the network request.
+ NetworkResponse networkResponse = mNetwork.performRequest(request);
+ request.addMarker("network-http-complete");
+
+ // If the server returned 304 AND we delivered a response already,
+ // we're done -- don't deliver a second identical response.
+ if (networkResponse.notModified && request.hasHadResponseDelivered()) {
+ request.finish("not-modified");
+ continue;
+ }
+
+ // Parse the response here on the worker thread.
+ Response<?> response = request.parseNetworkResponse(networkResponse);
+ request.addMarker("network-parse-complete");
+
+ // Write to cache if applicable.
+ // TODO: Only update cache metadata instead of entire record for 304s.
+ if (request.shouldCache() && response.cacheEntry != null) {
+ mCache.put(request.getCacheKey(), response.cacheEntry);
+ request.addMarker("network-cache-written");
+ }
+
+ // Post the response back.
+ request.markDelivered();
+ mDelivery.postResponse(request, response);
+ } catch (VolleyError volleyError) {
+ volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
+ parseAndDeliverNetworkError(request, volleyError);
+ } catch (Exception e) {
+ VolleyLog.e(e, "Unhandled exception %s", e.toString());
+ VolleyError volleyError = new VolleyError(e);
+ volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
+ mDelivery.postError(request, volleyError);
+ }
+ }
+ }
+
+ private void parseAndDeliverNetworkError(Request<?> request, VolleyError error) {
+ error = request.parseNetworkError(error);
+ mDelivery.postError(request, error);
+ }
+}
diff --git a/src/main/java/com/android/volley/NetworkError.java b/src/main/java/com/android/volley/NetworkError.java
new file mode 100644
index 0000000..42fbcc2
--- /dev/null
+++ b/src/main/java/com/android/volley/NetworkError.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011 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;
+
+import com.android.volley.NetworkResponse;
+import com.android.volley.VolleyError;
+
+/**
+ * Indicates that there was a network error when performing a Volley request.
+ */
+@SuppressWarnings("serial")
+public class NetworkError extends VolleyError {
+ public NetworkError() {
+ super();
+ }
+
+ public NetworkError(Throwable cause) {
+ super(cause);
+ }
+
+ public NetworkError(NetworkResponse networkResponse) {
+ super(networkResponse);
+ }
+}
diff --git a/src/main/java/com/android/volley/NetworkResponse.java b/src/main/java/com/android/volley/NetworkResponse.java
new file mode 100644
index 0000000..a787fa7
--- /dev/null
+++ b/src/main/java/com/android/volley/NetworkResponse.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2011 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;
+
+import org.apache.http.HttpStatus;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Data and headers returned from {@link Network#performRequest(Request)}.
+ */
+public class NetworkResponse {
+ /**
+ * Creates a new network response.
+ * @param statusCode the HTTP status code
+ * @param data Response body
+ * @param headers Headers returned with this response, or null for none
+ * @param notModified True if the server returned a 304 and the data was already in cache
+ * @param networkTimeMs Round-trip network time to receive network response
+ */
+ public NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,
+ boolean notModified, long networkTimeMs) {
+ this.statusCode = statusCode;
+ this.data = data;
+ this.headers = headers;
+ this.notModified = notModified;
+ this.networkTimeMs = networkTimeMs;
+ }
+
+ public NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,
+ boolean notModified) {
+ this(statusCode, data, headers, notModified, 0);
+ }
+
+ public NetworkResponse(byte[] data) {
+ this(HttpStatus.SC_OK, data, Collections.<String, String>emptyMap(), false, 0);
+ }
+
+ public NetworkResponse(byte[] data, Map<String, String> headers) {
+ this(HttpStatus.SC_OK, data, headers, false, 0);
+ }
+
+ /** The HTTP status code. */
+ public final int statusCode;
+
+ /** Raw data from this response. */
+ public final byte[] data;
+
+ /** Response headers. */
+ public final Map<String, String> headers;
+
+ /** True if the server returned a 304 (Not Modified). */
+ public final boolean notModified;
+
+ /** Network roundtrip time in milliseconds. */
+ public final long networkTimeMs;
+}
+
diff --git a/src/main/java/com/android/volley/NoConnectionError.java b/src/main/java/com/android/volley/NoConnectionError.java
new file mode 100644
index 0000000..fc23156
--- /dev/null
+++ b/src/main/java/com/android/volley/NoConnectionError.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 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;
+
+/**
+ * Error indicating that no connection could be established when performing a Volley request.
+ */
+@SuppressWarnings("serial")
+public class NoConnectionError extends NetworkError {
+ public NoConnectionError() {
+ super();
+ }
+
+ public NoConnectionError(Throwable reason) {
+ super(reason);
+ }
+}
diff --git a/src/main/java/com/android/volley/ParseError.java b/src/main/java/com/android/volley/ParseError.java
new file mode 100644
index 0000000..a55da47
--- /dev/null
+++ b/src/main/java/com/android/volley/ParseError.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2011 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;
+
+import com.android.volley.NetworkResponse;
+import com.android.volley.VolleyError;
+
+/**
+ * Indicates that the server's response could not be parsed.
+ */
+@SuppressWarnings("serial")
+public class ParseError extends VolleyError {
+ public ParseError() { }
+
+ public ParseError(NetworkResponse networkResponse) {
+ super(networkResponse);
+ }
+
+ public ParseError(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/android/volley/Request.java b/src/main/java/com/android/volley/Request.java
new file mode 100644
index 0000000..9832952
--- /dev/null
+++ b/src/main/java/com/android/volley/Request.java
@@ -0,0 +1,603 @@
+/*
+ * Copyright (C) 2011 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;
+
+import android.net.TrafficStats;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.text.TextUtils;
+
+import com.android.volley.VolleyLog.MarkerLog;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Base class for all network requests.
+ *
+ * @param <T> The type of parsed response this request expects.
+ */
+public abstract class Request<T> implements Comparable<Request<T>> {
+
+ /**
+ * Default encoding for POST or PUT parameters. See {@link #getParamsEncoding()}.
+ */
+ private static final String DEFAULT_PARAMS_ENCODING = "UTF-8";
+
+ /**
+ * Supported request methods.
+ */
+ public interface Method {
+ int DEPRECATED_GET_OR_POST = -1;
+ int GET = 0;
+ int POST = 1;
+ int PUT = 2;
+ int DELETE = 3;
+ int HEAD = 4;
+ int OPTIONS = 5;
+ int TRACE = 6;
+ int PATCH = 7;
+ }
+
+ /** An event log tracing the lifetime of this request; for debugging. */
+ private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null;
+
+ /**
+ * Request method of this request. Currently supports GET, POST, PUT, DELETE, HEAD, OPTIONS,
+ * TRACE, and PATCH.
+ */
+ private final int mMethod;
+
+ /** URL of this request. */
+ private final String mUrl;
+
+ /** Default tag for {@link TrafficStats}. */
+ private final int mDefaultTrafficStatsTag;
+
+ /** Listener interface for errors. */
+ private final Response.ErrorListener mErrorListener;
+
+ /** Sequence number of this request, used to enforce FIFO ordering. */
+ private Integer mSequence;
+
+ /** The request queue this request is associated with. */
+ private RequestQueue mRequestQueue;
+
+ /** Whether or not responses to this request should be cached. */
+ private boolean mShouldCache = true;
+
+ /** Whether or not this request has been canceled. */
+ private boolean mCanceled = false;
+
+ /** Whether or not a response has been delivered for this request yet. */
+ private boolean mResponseDelivered = false;
+
+ // A cheap variant of request tracing used to dump slow requests.
+ private long mRequestBirthTime = 0;
+
+ /** Threshold at which we should log the request (even when debug logging is not enabled). */
+ private static final long SLOW_REQUEST_THRESHOLD_MS = 3000;
+
+ /** The retry policy for this request. */
+ private RetryPolicy mRetryPolicy;
+
+ /**
+ * When a request can be retrieved from cache but must be refreshed from
+ * the network, the cache entry will be stored here so that in the event of
+ * a "Not Modified" response, we can be sure it hasn't been evicted from cache.
+ */
+ private Cache.Entry mCacheEntry = null;
+
+ /** An opaque token tagging this request; used for bulk cancellation. */
+ private Object mTag;
+
+ /**
+ * Creates a new request with the given URL and error listener. Note that
+ * the normal response listener is not provided here as delivery of responses
+ * is provided by subclasses, who have a better idea of how to deliver an
+ * already-parsed response.
+ *
+ * @deprecated Use {@link #Request(int, String, com.android.volley.Response.ErrorListener)}.
+ */
+ @Deprecated
+ public Request(String url, Response.ErrorListener listener) {
+ this(Method.DEPRECATED_GET_OR_POST, url, listener);
+ }
+
+ /**
+ * Creates a new request with the given method (one of the values from {@link Method}),
+ * URL, and error listener. Note that the normal response listener is not provided here as
+ * delivery of responses is provided by subclasses, who have a better idea of how to deliver
+ * an already-parsed response.
+ */
+ public Request(int method, String url, Response.ErrorListener listener) {
+ mMethod = method;
+ mUrl = url;
+ mErrorListener = listener;
+ setRetryPolicy(new DefaultRetryPolicy());
+
+ mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
+ }
+
+ /**
+ * Return the method for this request. Can be one of the values in {@link Method}.
+ */
+ public int getMethod() {
+ return mMethod;
+ }
+
+ /**
+ * Set a tag on this request. Can be used to cancel all requests with this
+ * tag by {@link RequestQueue#cancelAll(Object)}.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public Request<?> setTag(Object tag) {
+ mTag = tag;
+ return this;
+ }
+
+ /**
+ * Returns this request's tag.
+ * @see Request#setTag(Object)
+ */
+ public Object getTag() {
+ return mTag;
+ }
+
+ /**
+ * @return this request's {@link com.android.volley.Response.ErrorListener}.
+ */
+ public Response.ErrorListener getErrorListener() {
+ return mErrorListener;
+ }
+
+ /**
+ * @return A tag for use with {@link TrafficStats#setThreadStatsTag(int)}
+ */
+ public int getTrafficStatsTag() {
+ return mDefaultTrafficStatsTag;
+ }
+
+ /**
+ * @return The hashcode of the URL's host component, or 0 if there is none.
+ */
+ private static int findDefaultTrafficStatsTag(String url) {
+ if (!TextUtils.isEmpty(url)) {
+ Uri uri = Uri.parse(url);
+ if (uri != null) {
+ String host = uri.getHost();
+ if (host != null) {
+ return host.hashCode();
+ }
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Sets the retry policy for this request.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public Request<?> setRetryPolicy(RetryPolicy retryPolicy) {
+ mRetryPolicy = retryPolicy;
+ return this;
+ }
+
+ /**
+ * Adds an event to this request's event log; for debugging.
+ */
+ public void addMarker(String tag) {
+ if (MarkerLog.ENABLED) {
+ mEventLog.add(tag, Thread.currentThread().getId());
+ } else if (mRequestBirthTime == 0) {
+ mRequestBirthTime = SystemClock.elapsedRealtime();
+ }
+ }
+
+ /**
+ * Notifies the request queue that this request has finished (successfully or with error).
+ *
+ * <p>Also dumps all events from this request's event log; for debugging.</p>
+ */
+ void finish(final String tag) {
+ if (mRequestQueue != null) {
+ mRequestQueue.finish(this);
+ }
+ if (MarkerLog.ENABLED) {
+ final long threadId = Thread.currentThread().getId();
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ // If we finish marking off of the main thread, we need to
+ // actually do it on the main thread to ensure correct ordering.
+ Handler mainThread = new Handler(Looper.getMainLooper());
+ mainThread.post(new Runnable() {
+ @Override
+ public void run() {
+ mEventLog.add(tag, threadId);
+ mEventLog.finish(this.toString());
+ }
+ });
+ return;
+ }
+
+ mEventLog.add(tag, threadId);
+ mEventLog.finish(this.toString());
+ } else {
+ long requestTime = SystemClock.elapsedRealtime() - mRequestBirthTime;
+ if (requestTime >= SLOW_REQUEST_THRESHOLD_MS) {
+ VolleyLog.d("%d ms: %s", requestTime, this.toString());
+ }
+ }
+ }
+
+ /**
+ * Associates this request with the given queue. The request queue will be notified when this
+ * request has finished.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public Request<?> setRequestQueue(RequestQueue requestQueue) {
+ mRequestQueue = requestQueue;
+ return this;
+ }
+
+ /**
+ * Sets the sequence number of this request. Used by {@link RequestQueue}.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public final Request<?> setSequence(int sequence) {
+ mSequence = sequence;
+ return this;
+ }
+
+ /**
+ * Returns the sequence number of this request.
+ */
+ public final int getSequence() {
+ if (mSequence == null) {
+ throw new IllegalStateException("getSequence called before setSequence");
+ }
+ return mSequence;
+ }
+
+ /**
+ * Returns the URL of this request.
+ */
+ public String getUrl() {
+ return mUrl;
+ }
+
+ /**
+ * Returns the cache key for this request. By default, this is the URL.
+ */
+ public String getCacheKey() {
+ return getUrl();
+ }
+
+ /**
+ * Annotates this request with an entry retrieved for it from cache.
+ * Used for cache coherency support.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public Request<?> setCacheEntry(Cache.Entry entry) {
+ mCacheEntry = entry;
+ return this;
+ }
+
+ /**
+ * Returns the annotated cache entry, or null if there isn't one.
+ */
+ public Cache.Entry getCacheEntry() {
+ return mCacheEntry;
+ }
+
+ /**
+ * Mark this request as canceled. No callback will be delivered.
+ */
+ public void cancel() {
+ mCanceled = true;
+ }
+
+ /**
+ * Returns true if this request has been canceled.
+ */
+ public boolean isCanceled() {
+ return mCanceled;
+ }
+
+ /**
+ * Returns a list of extra HTTP headers to go along with this request. Can
+ * throw {@link AuthFailureError} as authentication may be required to
+ * provide these values.
+ * @throws AuthFailureError In the event of auth failure
+ */
+ public Map<String, String> getHeaders() throws AuthFailureError {
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Returns a Map of POST parameters to be used for this request, or null if
+ * a simple GET should be used. Can throw {@link AuthFailureError} as
+ * authentication may be required to provide these values.
+ *
+ * <p>Note that only one of getPostParams() and getPostBody() can return a non-null
+ * value.</p>
+ * @throws AuthFailureError In the event of auth failure
+ *
+ * @deprecated Use {@link #getParams()} instead.
+ */
+ @Deprecated
+ protected Map<String, String> getPostParams() throws AuthFailureError {
+ return getParams();
+ }
+
+ /**
+ * Returns which encoding should be used when converting POST parameters returned by
+ * {@link #getPostParams()} into a raw POST body.
+ *
+ * <p>This controls both encodings:
+ * <ol>
+ * <li>The string encoding used when converting parameter names and values into bytes prior
+ * to URL encoding them.</li>
+ * <li>The string encoding used when converting the URL encoded parameters into a raw
+ * byte array.</li>
+ * </ol>
+ *
+ * @deprecated Use {@link #getParamsEncoding()} instead.
+ */
+ @Deprecated
+ protected String getPostParamsEncoding() {
+ return getParamsEncoding();
+ }
+
+ /**
+ * @deprecated Use {@link #getBodyContentType()} instead.
+ */
+ @Deprecated
+ public String getPostBodyContentType() {
+ return getBodyContentType();
+ }
+
+ /**
+ * Returns the raw POST body to be sent.
+ *
+ * @throws AuthFailureError In the event of auth failure
+ *
+ * @deprecated Use {@link #getBody()} instead.
+ */
+ @Deprecated
+ public byte[] getPostBody() throws AuthFailureError {
+ // Note: For compatibility with legacy clients of volley, this implementation must remain
+ // here instead of simply calling the getBody() function because this function must
+ // call getPostParams() and getPostParamsEncoding() since legacy clients would have
+ // overridden these two member functions for POST requests.
+ Map<String, String> postParams = getPostParams();
+ if (postParams != null && postParams.size() > 0) {
+ return encodeParameters(postParams, getPostParamsEncoding());
+ }
+ return null;
+ }
+
+ /**
+ * Returns a Map of parameters to be used for a POST or PUT request. Can throw
+ * {@link AuthFailureError} as authentication may be required to provide these values.
+ *
+ * <p>Note that you can directly override {@link #getBody()} for custom data.</p>
+ *
+ * @throws AuthFailureError in the event of auth failure
+ */
+ protected Map<String, String> getParams() throws AuthFailureError {
+ return null;
+ }
+
+ /**
+ * Returns which encoding should be used when converting POST or PUT parameters returned by
+ * {@link #getParams()} into a raw POST or PUT body.
+ *
+ * <p>This controls both encodings:
+ * <ol>
+ * <li>The string encoding used when converting parameter names and values into bytes prior
+ * to URL encoding them.</li>
+ * <li>The string encoding used when converting the URL encoded parameters into a raw
+ * byte array.</li>
+ * </ol>
+ */
+ protected String getParamsEncoding() {
+ return DEFAULT_PARAMS_ENCODING;
+ }
+
+ /**
+ * Returns the content type of the POST or PUT body.
+ */
+ public String getBodyContentType() {
+ return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
+ }
+
+ /**
+ * Returns the raw POST or PUT body to be sent.
+ *
+ * <p>By default, the body consists of the request parameters in
+ * application/x-www-form-urlencoded format. When overriding this method, consider overriding
+ * {@link #getBodyContentType()} as well to match the new body format.
+ *
+ * @throws AuthFailureError in the event of auth failure
+ */
+ public byte[] getBody() throws AuthFailureError {
+ Map<String, String> params = getParams();
+ if (params != null && params.size() > 0) {
+ return encodeParameters(params, getParamsEncoding());
+ }
+ return null;
+ }
+
+ /**
+ * Converts <code>params</code> into an application/x-www-form-urlencoded encoded string.
+ */
+ private byte[] encodeParameters(Map<String, String> params, String paramsEncoding) {
+ StringBuilder encodedParams = new StringBuilder();
+ try {
+ for (Map.Entry<String, String> entry : params.entrySet()) {
+ encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding));
+ encodedParams.append('=');
+ encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding));
+ encodedParams.append('&');
+ }
+ return encodedParams.toString().getBytes(paramsEncoding);
+ } catch (UnsupportedEncodingException uee) {
+ throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee);
+ }
+ }
+
+ /**
+ * Set whether or not responses to this request should be cached.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public final Request<?> setShouldCache(boolean shouldCache) {
+ mShouldCache = shouldCache;
+ return this;
+ }
+
+ /**
+ * Returns true if responses to this request should be cached.
+ */
+ public final boolean shouldCache() {
+ return mShouldCache;
+ }
+
+ /**
+ * Priority values. Requests will be processed from higher priorities to
+ * lower priorities, in FIFO order.
+ */
+ public enum Priority {
+ LOW,
+ NORMAL,
+ HIGH,
+ IMMEDIATE
+ }
+
+ /**
+ * Returns the {@link Priority} of this request; {@link Priority#NORMAL} by default.
+ */
+ public Priority getPriority() {
+ return Priority.NORMAL;
+ }
+
+ /**
+ * Returns the socket timeout in milliseconds per retry attempt. (This value can be changed
+ * per retry attempt if a backoff is specified via backoffTimeout()). If there are no retry
+ * attempts remaining, this will cause delivery of a {@link TimeoutError} error.
+ */
+ public final int getTimeoutMs() {
+ return mRetryPolicy.getCurrentTimeout();
+ }
+
+ /**
+ * Returns the retry policy that should be used for this request.
+ */
+ public RetryPolicy getRetryPolicy() {
+ return mRetryPolicy;
+ }
+
+ /**
+ * Mark this request as having a response delivered on it. This can be used
+ * later in the request's lifetime for suppressing identical responses.
+ */
+ public void markDelivered() {
+ mResponseDelivered = true;
+ }
+
+ /**
+ * Returns true if this request has had a response delivered for it.
+ */
+ public boolean hasHadResponseDelivered() {
+ return mResponseDelivered;
+ }
+
+ /**
+ * Subclasses must implement this to parse the raw network response
+ * and return an appropriate response type. This method will be
+ * called from a worker thread. The response will not be delivered
+ * if you return null.
+ * @param response Response from the network
+ * @return The parsed response, or null in the case of an error
+ */
+ abstract protected Response<T> parseNetworkResponse(NetworkResponse response);
+
+ /**
+ * Subclasses can override this method to parse 'networkError' and return a more specific error.
+ *
+ * <p>The default implementation just returns the passed 'networkError'.</p>
+ *
+ * @param volleyError the error retrieved from the network
+ * @return an NetworkError augmented with additional information
+ */
+ protected VolleyError parseNetworkError(VolleyError volleyError) {
+ return volleyError;
+ }
+
+ /**
+ * Subclasses must implement this to perform delivery of the parsed
+ * response to their listeners. The given response is guaranteed to
+ * be non-null; responses that fail to parse are not delivered.
+ * @param response The parsed response returned by
+ * {@link #parseNetworkResponse(NetworkResponse)}
+ */
+ abstract protected void deliverResponse(T response);
+
+ /**
+ * Delivers error message to the ErrorListener that the Request was
+ * initialized with.
+ *
+ * @param error Error details
+ */
+ public void deliverError(VolleyError error) {
+ if (mErrorListener != null) {
+ mErrorListener.onErrorResponse(error);
+ }
+ }
+
+ /**
+ * Our comparator sorts from high to low priority, and secondarily by
+ * sequence number to provide FIFO ordering.
+ */
+ @Override
+ public int compareTo(Request<T> other) {
+ Priority left = this.getPriority();
+ Priority right = other.getPriority();
+
+ // High-priority requests are "lesser" so they are sorted to the front.
+ // Equal priorities are sorted by sequence number to provide FIFO ordering.
+ return left == right ?
+ this.mSequence - other.mSequence :
+ right.ordinal() - left.ordinal();
+ }
+
+ @Override
+ public String toString() {
+ String trafficStatsTag = "0x" + Integer.toHexString(getTrafficStatsTag());
+ return (mCanceled ? "[X] " : "[ ] ") + getUrl() + " " + trafficStatsTag + " "
+ + getPriority() + " " + mSequence;
+ }
+}
diff --git a/src/main/java/com/android/volley/RequestQueue.java b/src/main/java/com/android/volley/RequestQueue.java
new file mode 100644
index 0000000..5c0e7af
--- /dev/null
+++ b/src/main/java/com/android/volley/RequestQueue.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2011 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;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A request dispatch queue with a thread pool of dispatchers.
+ *
+ * Calling {@link #add(Request)} will enqueue the given Request for dispatch,
+ * resolving from either cache or network on a worker thread, and then delivering
+ * a parsed response on the main thread.
+ */
+public class RequestQueue {
+
+ /** Used for generating monotonically-increasing sequence numbers for requests. */
+ private AtomicInteger mSequenceGenerator = new AtomicInteger();
+
+ /**
+ * Staging area for requests that already have a duplicate request in flight.
+ *
+ * <ul>
+ * <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache
+ * key.</li>
+ * <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request
+ * is <em>not</em> contained in that list. Is null if no requests are staged.</li>
+ * </ul>
+ */
+ private final Map<String, Queue<Request<?>>> mWaitingRequests =
+ new HashMap<String, Queue<Request<?>>>();
+
+ /**
+ * The set of all requests currently being processed by this RequestQueue. A Request
+ * will be in this set if it is waiting in any queue or currently being processed by
+ * any dispatcher.
+ */
+ private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();
+
+ /** The cache triage queue. */
+ private final PriorityBlockingQueue<Request<?>> mCacheQueue =
+ new PriorityBlockingQueue<Request<?>>();
+
+ /** The queue of requests that are actually going out to the network. */
+ private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
+ new PriorityBlockingQueue<Request<?>>();
+
+ /** Number of network request dispatcher threads to start. */
+ private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
+
+ /** Cache interface for retrieving and storing responses. */
+ private final Cache mCache;
+
+ /** Network interface for performing requests. */
+ private final Network mNetwork;
+
+ /** Response delivery mechanism. */
+ private final ResponseDelivery mDelivery;
+
+ /** The network dispatchers. */
+ private NetworkDispatcher[] mDispatchers;
+
+ /** The cache dispatcher. */
+ private CacheDispatcher mCacheDispatcher;
+
+ /**
+ * Creates the worker pool. Processing will not begin until {@link #start()} is called.
+ *
+ * @param cache A Cache to use for persisting responses to disk
+ * @param network A Network interface for performing HTTP requests
+ * @param threadPoolSize Number of network dispatcher threads to create
+ * @param delivery A ResponseDelivery interface for posting responses and errors
+ */
+ public RequestQueue(Cache cache, Network network, int threadPoolSize,
+ ResponseDelivery delivery) {
+ mCache = cache;
+ mNetwork = network;
+ mDispatchers = new NetworkDispatcher[threadPoolSize];
+ mDelivery = delivery;
+ }
+
+ /**
+ * Creates the worker pool. Processing will not begin until {@link #start()} is called.
+ *
+ * @param cache A Cache to use for persisting responses to disk
+ * @param network A Network interface for performing HTTP requests
+ * @param threadPoolSize Number of network dispatcher threads to create
+ */
+ public RequestQueue(Cache cache, Network network, int threadPoolSize) {
+ this(cache, network, threadPoolSize,
+ new ExecutorDelivery(new Handler(Looper.getMainLooper())));
+ }
+
+ /**
+ * Creates the worker pool. Processing will not begin until {@link #start()} is called.
+ *
+ * @param cache A Cache to use for persisting responses to disk
+ * @param network A Network interface for performing HTTP requests
+ */
+ public RequestQueue(Cache cache, Network network) {
+ this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
+ }
+
+ /**
+ * Starts the dispatchers in this queue.
+ */
+ public void start() {
+ stop(); // Make sure any currently running dispatchers are stopped.
+ // Create the cache dispatcher and start it.
+ mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
+ mCacheDispatcher.start();
+
+ // Create network dispatchers (and corresponding threads) up to the pool size.
+ for (int i = 0; i < mDispatchers.length; i++) {
+ NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
+ mCache, mDelivery);
+ mDispatchers[i] = networkDispatcher;
+ networkDispatcher.start();
+ }
+ }
+
+ /**
+ * Stops the cache and network dispatchers.
+ */
+ public void stop() {
+ if (mCacheDispatcher != null) {
+ mCacheDispatcher.quit();
+ }
+ for (int i = 0; i < mDispatchers.length; i++) {
+ if (mDispatchers[i] != null) {
+ mDispatchers[i].quit();
+ }
+ }
+ }
+
+ /**
+ * Gets a sequence number.
+ */
+ public int getSequenceNumber() {
+ return mSequenceGenerator.incrementAndGet();
+ }
+
+ /**
+ * Gets the {@link Cache} instance being used.
+ */
+ public Cache getCache() {
+ return mCache;
+ }
+
+ /**
+ * A simple predicate or filter interface for Requests, for use by
+ * {@link RequestQueue#cancelAll(RequestFilter)}.
+ */
+ public interface RequestFilter {
+ public boolean apply(Request<?> request);
+ }
+
+ /**
+ * Cancels all requests in this queue for which the given filter applies.
+ * @param filter The filtering function to use
+ */
+ public void cancelAll(RequestFilter filter) {
+ synchronized (mCurrentRequests) {
+ for (Request<?> request : mCurrentRequests) {
+ if (filter.apply(request)) {
+ request.cancel();
+ }
+ }
+ }
+ }
+
+ /**
+ * Cancels all requests in this queue with the given tag. Tag must be non-null
+ * and equality is by identity.
+ */
+ public void cancelAll(final Object tag) {
+ if (tag == null) {
+ throw new IllegalArgumentException("Cannot cancelAll with a null tag");
+ }
+ cancelAll(new RequestFilter() {
+ @Override
+ public boolean apply(Request<?> request) {
+ return request.getTag() == tag;
+ }
+ });
+ }
+
+ /**
+ * Adds a Request to the dispatch queue.
+ * @param request The request to service
+ * @return The passed-in request
+ */
+ public <T> Request<T> add(Request<T> request) {
+ // Tag the request as belonging to this queue and add it to the set of current requests.
+ request.setRequestQueue(this);
+ synchronized (mCurrentRequests) {
+ mCurrentRequests.add(request);
+ }
+
+ // Process requests in the order they are added.
+ request.setSequence(getSequenceNumber());
+ request.addMarker("add-to-queue");
+
+ // If the request is uncacheable, skip the cache queue and go straight to the network.
+ if (!request.shouldCache()) {
+ mNetworkQueue.add(request);
+ return request;
+ }
+
+ // Insert request into stage if there's already a request with the same cache key in flight.
+ synchronized (mWaitingRequests) {
+ String cacheKey = request.getCacheKey();
+ if (mWaitingRequests.containsKey(cacheKey)) {
+ // There is already a request in flight. Queue up.
+ Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
+ if (stagedRequests == null) {
+ stagedRequests = new LinkedList<Request<?>>();
+ }
+ stagedRequests.add(request);
+ mWaitingRequests.put(cacheKey, stagedRequests);
+ if (VolleyLog.DEBUG) {
+ VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
+ }
+ } else {
+ // Insert 'null' queue for this cacheKey, indicating there is now a request in
+ // flight.
+ mWaitingRequests.put(cacheKey, null);
+ mCacheQueue.add(request);
+ }
+ return request;
+ }
+ }
+
+ /**
+ * Called from {@link Request#finish(String)}, indicating that processing of the given request
+ * has finished.
+ *
+ * <p>Releases waiting requests for <code>request.getCacheKey()</code> if
+ * <code>request.shouldCache()</code>.</p>
+ */
+ void finish(Request<?> request) {
+ // Remove from the set of requests currently being processed.
+ synchronized (mCurrentRequests) {
+ mCurrentRequests.remove(request);
+ }
+
+ if (request.shouldCache()) {
+ synchronized (mWaitingRequests) {
+ String cacheKey = request.getCacheKey();
+ Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
+ if (waitingRequests != null) {
+ if (VolleyLog.DEBUG) {
+ VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
+ waitingRequests.size(), cacheKey);
+ }
+ // Process all queued up requests. They won't be considered as in flight, but
+ // that's not a problem as the cache has been primed by 'request'.
+ mCacheQueue.addAll(waitingRequests);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/Response.java b/src/main/java/com/android/volley/Response.java
new file mode 100644
index 0000000..1165595
--- /dev/null
+++ b/src/main/java/com/android/volley/Response.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2011 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;
+
+/**
+ * Encapsulates a parsed response for delivery.
+ *
+ * @param <T> Parsed type of this response
+ */
+public class Response<T> {
+
+ /** Callback interface for delivering parsed responses. */
+ public interface Listener<T> {
+ /** Called when a response is received. */
+ public void onResponse(T response);
+ }
+
+ /** Callback interface for delivering error responses. */
+ public interface ErrorListener {
+ /**
+ * Callback method that an error has been occurred with the
+ * provided error code and optional user-readable message.
+ */
+ public void onErrorResponse(VolleyError error);
+ }
+
+ /** Returns a successful response containing the parsed result. */
+ public static <T> Response<T> success(T result, Cache.Entry cacheEntry) {
+ return new Response<T>(result, cacheEntry);
+ }
+
+ /**
+ * Returns a failed response containing the given error code and an optional
+ * localized message displayed to the user.
+ */
+ public static <T> Response<T> error(VolleyError error) {
+ return new Response<T>(error);
+ }
+
+ /** Parsed response, or null in the case of error. */
+ public final T result;
+
+ /** Cache metadata for this response, or null in the case of error. */
+ public final Cache.Entry cacheEntry;
+
+ /** Detailed error information if <code>errorCode != OK</code>. */
+ public final VolleyError error;
+
+ /** True if this response was a soft-expired one and a second one MAY be coming. */
+ public boolean intermediate = false;
+
+ /**
+ * Returns whether this response is considered successful.
+ */
+ public boolean isSuccess() {
+ return error == null;
+ }
+
+
+ private Response(T result, Cache.Entry cacheEntry) {
+ this.result = result;
+ this.cacheEntry = cacheEntry;
+ this.error = null;
+ }
+
+ private Response(VolleyError error) {
+ this.result = null;
+ this.cacheEntry = null;
+ this.error = error;
+ }
+}
diff --git a/src/main/java/com/android/volley/ResponseDelivery.java b/src/main/java/com/android/volley/ResponseDelivery.java
new file mode 100644
index 0000000..87706af
--- /dev/null
+++ b/src/main/java/com/android/volley/ResponseDelivery.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2011 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;
+
+public interface ResponseDelivery {
+ /**
+ * Parses a response from the network or cache and delivers it.
+ */
+ public void postResponse(Request<?> request, Response<?> response);
+
+ /**
+ * Parses a response from the network or cache and delivers it. The provided
+ * Runnable will be executed after delivery.
+ */
+ public void postResponse(Request<?> request, Response<?> response, Runnable runnable);
+
+ /**
+ * Posts an error for the given request.
+ */
+ public void postError(Request<?> request, VolleyError error);
+}
diff --git a/src/main/java/com/android/volley/RetryPolicy.java b/src/main/java/com/android/volley/RetryPolicy.java
new file mode 100644
index 0000000..0dd198b
--- /dev/null
+++ b/src/main/java/com/android/volley/RetryPolicy.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2011 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;
+
+/**
+ * Retry policy for a request.
+ */
+public interface RetryPolicy {
+
+ /**
+ * Returns the current timeout (used for logging).
+ */
+ public int getCurrentTimeout();
+
+ /**
+ * Returns the current retry count (used for logging).
+ */
+ public int getCurrentRetryCount();
+
+ /**
+ * Prepares for the next retry by applying a backoff to the timeout.
+ * @param error The error code of the last attempt.
+ * @throws VolleyError In the event that the retry could not be performed (for example if we
+ * ran out of attempts), the passed in error is thrown.
+ */
+ public void retry(VolleyError error) throws VolleyError;
+}
diff --git a/src/main/java/com/android/volley/ServerError.java b/src/main/java/com/android/volley/ServerError.java
new file mode 100644
index 0000000..70a9fd5
--- /dev/null
+++ b/src/main/java/com/android/volley/ServerError.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2011 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;
+
+import com.android.volley.NetworkResponse;
+import com.android.volley.VolleyError;
+
+/**
+ * Indicates that the server responded with an error response.
+ */
+@SuppressWarnings("serial")
+public class ServerError extends VolleyError {
+ public ServerError(NetworkResponse networkResponse) {
+ super(networkResponse);
+ }
+
+ public ServerError() {
+ super();
+ }
+}
diff --git a/src/main/java/com/android/volley/TimeoutError.java b/src/main/java/com/android/volley/TimeoutError.java
new file mode 100644
index 0000000..0b5d6ac
--- /dev/null
+++ b/src/main/java/com/android/volley/TimeoutError.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 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;
+
+/**
+ * Indicates that the connection or the socket timed out.
+ */
+@SuppressWarnings("serial")
+public class TimeoutError extends VolleyError { }
diff --git a/src/main/java/com/android/volley/VolleyError.java b/src/main/java/com/android/volley/VolleyError.java
new file mode 100644
index 0000000..1471d40
--- /dev/null
+++ b/src/main/java/com/android/volley/VolleyError.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2011 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;
+
+/**
+ * Exception style class encapsulating Volley errors
+ */
+@SuppressWarnings("serial")
+public class VolleyError extends Exception {
+ public final NetworkResponse networkResponse;
+ private long networkTimeMs;
+
+ public VolleyError() {
+ networkResponse = null;
+ }
+
+ public VolleyError(NetworkResponse response) {
+ networkResponse = response;
+ }
+
+ public VolleyError(String exceptionMessage) {
+ super(exceptionMessage);
+ networkResponse = null;
+ }
+
+ public VolleyError(String exceptionMessage, Throwable reason) {
+ super(exceptionMessage, reason);
+ networkResponse = null;
+ }
+
+ public VolleyError(Throwable cause) {
+ super(cause);
+ networkResponse = null;
+ }
+
+ /* package */ void setNetworkTimeMs(long networkTimeMs) {
+ this.networkTimeMs = networkTimeMs;
+ }
+
+ public long getNetworkTimeMs() {
+ return networkTimeMs;
+ }
+}
diff --git a/src/main/java/com/android/volley/VolleyLog.java b/src/main/java/com/android/volley/VolleyLog.java
new file mode 100644
index 0000000..6684690
--- /dev/null
+++ b/src/main/java/com/android/volley/VolleyLog.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2011 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;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/** Logging helper class. */
+public class VolleyLog {
+ public static String TAG = "Volley";
+
+ public static boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
+
+ /**
+ * Customize the log tag for your application, so that other apps
+ * using Volley don't mix their logs with yours.
+ * <br />
+ * Enable the log property for your tag before starting your app:
+ * <br />
+ * {@code adb shell setprop log.tag.&lt;tag&gt;}
+ */
+ public static void setTag(String tag) {
+ d("Changing log tag to %s", tag);
+ TAG = tag;
+
+ // Reinitialize the DEBUG "constant"
+ DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
+ }
+
+ public static void v(String format, Object... args) {
+ if (DEBUG) {
+ Log.v(TAG, buildMessage(format, args));
+ }
+ }
+
+ public static void d(String format, Object... args) {
+ Log.d(TAG, buildMessage(format, args));
+ }
+
+ public static void e(String format, Object... args) {
+ Log.e(TAG, buildMessage(format, args));
+ }
+
+ public static void e(Throwable tr, String format, Object... args) {
+ Log.e(TAG, buildMessage(format, args), tr);
+ }
+
+ public static void wtf(String format, Object... args) {
+ Log.wtf(TAG, buildMessage(format, args));
+ }
+
+ public static void wtf(Throwable tr, String format, Object... args) {
+ Log.wtf(TAG, buildMessage(format, args), tr);
+ }
+
+ /**
+ * Formats the caller's provided message and prepends useful info like
+ * calling thread ID and method name.
+ */
+ private static String buildMessage(String format, Object... args) {
+ String msg = (args == null) ? format : String.format(Locale.US, format, args);
+ StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace();
+
+ String caller = "<unknown>";
+ // Walk up the stack looking for the first caller outside of VolleyLog.
+ // It will be at least two frames up, so start there.
+ for (int i = 2; i < trace.length; i++) {
+ Class<?> clazz = trace[i].getClass();
+ if (!clazz.equals(VolleyLog.class)) {
+ String callingClass = trace[i].getClassName();
+ callingClass = callingClass.substring(callingClass.lastIndexOf('.') + 1);
+ callingClass = callingClass.substring(callingClass.lastIndexOf('$') + 1);
+
+ caller = callingClass + "." + trace[i].getMethodName();
+ break;
+ }
+ }
+ return String.format(Locale.US, "[%d] %s: %s",
+ Thread.currentThread().getId(), caller, msg);
+ }
+
+ /**
+ * A simple event log with records containing a name, thread ID, and timestamp.
+ */
+ static class MarkerLog {
+ public static final boolean ENABLED = VolleyLog.DEBUG;
+
+ /** Minimum duration from first marker to last in an marker log to warrant logging. */
+ private static final long MIN_DURATION_FOR_LOGGING_MS = 0;
+
+ private static class Marker {
+ public final String name;
+ public final long thread;
+ public final long time;
+
+ public Marker(String name, long thread, long time) {
+ this.name = name;
+ this.thread = thread;
+ this.time = time;
+ }
+ }
+
+ private final List<Marker> mMarkers = new ArrayList<Marker>();
+ private boolean mFinished = false;
+
+ /** Adds a marker to this log with the specified name. */
+ public synchronized void add(String name, long threadId) {
+ if (mFinished) {
+ throw new IllegalStateException("Marker added to finished log");
+ }
+
+ mMarkers.add(new Marker(name, threadId, SystemClock.elapsedRealtime()));
+ }
+
+ /**
+ * Closes the log, dumping it to logcat if the time difference between
+ * the first and last markers is greater than {@link #MIN_DURATION_FOR_LOGGING_MS}.
+ * @param header Header string to print above the marker log.
+ */
+ public synchronized void finish(String header) {
+ mFinished = true;
+
+ long duration = getTotalDuration();
+ if (duration <= MIN_DURATION_FOR_LOGGING_MS) {
+ return;
+ }
+
+ long prevTime = mMarkers.get(0).time;
+ d("(%-4d ms) %s", duration, header);
+ for (Marker marker : mMarkers) {
+ long thisTime = marker.time;
+ d("(+%-4d) [%2d] %s", (thisTime - prevTime), marker.thread, marker.name);
+ prevTime = thisTime;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ // Catch requests that have been collected (and hence end-of-lifed)
+ // but had no debugging output printed for them.
+ if (!mFinished) {
+ finish("Request on the loose");
+ e("Marker log finalized without finish() - uncaught exit point for request");
+ }
+ }
+
+ /** Returns the time difference between the first and last events in this log. */
+ private long getTotalDuration() {
+ if (mMarkers.size() == 0) {
+ return 0;
+ }
+
+ long first = mMarkers.get(0).time;
+ long last = mMarkers.get(mMarkers.size() - 1).time;
+ return last - first;
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java b/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java
new file mode 100644
index 0000000..371fd83
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.AuthFailureError;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * An Authenticator that uses {@link AccountManager} to get auth
+ * tokens of a specified type for a specified account.
+ */
+public class AndroidAuthenticator implements Authenticator {
+ private final Context mContext;
+ private final Account mAccount;
+ private final String mAuthTokenType;
+ private final boolean mNotifyAuthFailure;
+
+ /**
+ * Creates a new authenticator.
+ * @param context Context for accessing AccountManager
+ * @param account Account to authenticate as
+ * @param authTokenType Auth token type passed to AccountManager
+ */
+ public AndroidAuthenticator(Context context, Account account, String authTokenType) {
+ this(context, account, authTokenType, false);
+ }
+
+ /**
+ * Creates a new authenticator.
+ * @param context Context for accessing AccountManager
+ * @param account Account to authenticate as
+ * @param authTokenType Auth token type passed to AccountManager
+ * @param notifyAuthFailure Whether to raise a notification upon auth failure
+ */
+ public AndroidAuthenticator(Context context, Account account, String authTokenType,
+ boolean notifyAuthFailure) {
+ mContext = context;
+ mAccount = account;
+ mAuthTokenType = authTokenType;
+ mNotifyAuthFailure = notifyAuthFailure;
+ }
+
+ /**
+ * Returns the Account being used by this authenticator.
+ */
+ public Account getAccount() {
+ return mAccount;
+ }
+
+ // TODO: Figure out what to do about notifyAuthFailure
+ @SuppressWarnings("deprecation")
+ @Override
+ public String getAuthToken() throws AuthFailureError {
+ final AccountManager accountManager = AccountManager.get(mContext);
+ AccountManagerFuture<Bundle> future = accountManager.getAuthToken(mAccount,
+ mAuthTokenType, mNotifyAuthFailure, null, null);
+ Bundle result;
+ try {
+ result = future.getResult();
+ } catch (Exception e) {
+ throw new AuthFailureError("Error while retrieving auth token", e);
+ }
+ String authToken = null;
+ if (future.isDone() && !future.isCancelled()) {
+ if (result.containsKey(AccountManager.KEY_INTENT)) {
+ Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
+ throw new AuthFailureError(intent);
+ }
+ authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
+ }
+ if (authToken == null) {
+ throw new AuthFailureError("Got null auth token for type: " + mAuthTokenType);
+ }
+
+ return authToken;
+ }
+
+ @Override
+ public void invalidateAuthToken(String authToken) {
+ AccountManager.get(mContext).invalidateAuthToken(mAccount.type, authToken);
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/Authenticator.java b/src/main/java/com/android/volley/toolbox/Authenticator.java
new file mode 100644
index 0000000..d9f5e3c
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/Authenticator.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.AuthFailureError;
+
+/**
+ * An interface for interacting with auth tokens.
+ */
+public interface Authenticator {
+ /**
+ * Synchronously retrieves an auth token.
+ *
+ * @throws AuthFailureError If authentication did not succeed
+ */
+ public String getAuthToken() throws AuthFailureError;
+
+ /**
+ * Invalidates the provided auth token.
+ */
+ public void invalidateAuthToken(String authToken);
+}
diff --git a/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
new file mode 100644
index 0000000..bc1cfdb
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2011 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.os.SystemClock;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Cache;
+import com.android.volley.Cache.Entry;
+import com.android.volley.Network;
+import com.android.volley.NetworkError;
+import com.android.volley.NetworkResponse;
+import com.android.volley.NoConnectionError;
+import com.android.volley.Request;
+import com.android.volley.RetryPolicy;
+import com.android.volley.ServerError;
+import com.android.volley.TimeoutError;
+import com.android.volley.VolleyError;
+import com.android.volley.VolleyLog;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.StatusLine;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.impl.cookie.DateUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * A network performing Volley requests over an {@link HttpStack}.
+ */
+public class BasicNetwork implements Network {
+ protected static final boolean DEBUG = VolleyLog.DEBUG;
+
+ private static int SLOW_REQUEST_THRESHOLD_MS = 3000;
+
+ private static int DEFAULT_POOL_SIZE = 4096;
+
+ protected final HttpStack mHttpStack;
+
+ protected final ByteArrayPool mPool;
+
+ /**
+ * @param httpStack HTTP stack to be used
+ */
+ public BasicNetwork(HttpStack httpStack) {
+ // If a pool isn't passed in, then build a small default pool that will give us a lot of
+ // benefit and not use too much memory.
+ this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
+ }
+
+ /**
+ * @param httpStack HTTP stack to be used
+ * @param pool a buffer pool that improves GC performance in copy operations
+ */
+ public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
+ mHttpStack = httpStack;
+ mPool = pool;
+ }
+
+ @Override
+ public NetworkResponse performRequest(Request<?> request) throws VolleyError {
+ long requestStart = SystemClock.elapsedRealtime();
+ while (true) {
+ HttpResponse httpResponse = null;
+ byte[] responseContents = null;
+ Map<String, String> responseHeaders = Collections.emptyMap();
+ try {
+ // Gather headers.
+ Map<String, String> headers = new HashMap<String, String>();
+ addCacheHeaders(headers, request.getCacheEntry());
+ httpResponse = mHttpStack.performRequest(request, headers);
+ StatusLine statusLine = httpResponse.getStatusLine();
+ int statusCode = statusLine.getStatusCode();
+
+ responseHeaders = convertHeaders(httpResponse.getAllHeaders());
+ // Handle cache validation.
+ if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
+
+ Entry entry = request.getCacheEntry();
+ if (entry == null) {
+ return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null,
+ responseHeaders, true,
+ SystemClock.elapsedRealtime() - requestStart);
+ }
+
+ // A HTTP 304 response does not have all header fields. We
+ // have to use the header fields from the cache entry plus
+ // the new ones from the response.
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+ entry.responseHeaders.putAll(responseHeaders);
+ return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data,
+ entry.responseHeaders, true,
+ SystemClock.elapsedRealtime() - requestStart);
+ }
+
+ // Some responses such as 204s do not have content. We must check.
+ if (httpResponse.getEntity() != null) {
+ responseContents = entityToBytes(httpResponse.getEntity());
+ } else {
+ // Add 0 byte response as a way of honestly representing a
+ // no-content request.
+ responseContents = new byte[0];
+ }
+
+ // if the request is slow, log it.
+ long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
+ logSlowRequests(requestLifetime, request, responseContents, statusLine);
+
+ if (statusCode < 200 || statusCode > 299) {
+ throw new IOException();
+ }
+ return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
+ SystemClock.elapsedRealtime() - requestStart);
+ } catch (SocketTimeoutException e) {
+ attemptRetryOnException("socket", request, new TimeoutError());
+ } catch (ConnectTimeoutException e) {
+ attemptRetryOnException("connection", request, new TimeoutError());
+ } catch (MalformedURLException e) {
+ throw new RuntimeException("Bad URL " + request.getUrl(), e);
+ } catch (IOException e) {
+ int statusCode = 0;
+ NetworkResponse networkResponse = null;
+ if (httpResponse != null) {
+ statusCode = httpResponse.getStatusLine().getStatusCode();
+ } else {
+ throw new NoConnectionError(e);
+ }
+ VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
+ if (responseContents != null) {
+ networkResponse = new NetworkResponse(statusCode, responseContents,
+ responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
+ if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
+ statusCode == HttpStatus.SC_FORBIDDEN) {
+ attemptRetryOnException("auth",
+ request, new AuthFailureError(networkResponse));
+ } else {
+ // TODO: Only throw ServerError for 5xx status codes.
+ throw new ServerError(networkResponse);
+ }
+ } else {
+ throw new NetworkError(networkResponse);
+ }
+ }
+ }
+ }
+
+ /**
+ * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete.
+ */
+ private void logSlowRequests(long requestLifetime, Request<?> request,
+ byte[] responseContents, StatusLine statusLine) {
+ if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
+ VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " +
+ "[rc=%d], [retryCount=%s]", request, requestLifetime,
+ responseContents != null ? responseContents.length : "null",
+ statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount());
+ }
+ }
+
+ /**
+ * Attempts to prepare the request for a retry. If there are no more attempts remaining in the
+ * request's retry policy, a timeout exception is thrown.
+ * @param request The request to use.
+ */
+ private static void attemptRetryOnException(String logPrefix, Request<?> request,
+ VolleyError exception) throws VolleyError {
+ RetryPolicy retryPolicy = request.getRetryPolicy();
+ int oldTimeout = request.getTimeoutMs();
+
+ try {
+ retryPolicy.retry(exception);
+ } catch (VolleyError e) {
+ request.addMarker(
+ String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
+ throw e;
+ }
+ request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
+ }
+
+ private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
+ // If there's no cache entry, we're done.
+ if (entry == null) {
+ return;
+ }
+
+ if (entry.etag != null) {
+ headers.put("If-None-Match", entry.etag);
+ }
+
+ if (entry.serverDate > 0) {
+ Date refTime = new Date(entry.serverDate);
+ headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
+ }
+ }
+
+ protected void logError(String what, String url, long start) {
+ long now = SystemClock.elapsedRealtime();
+ VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url);
+ }
+
+ /** Reads the contents of HttpEntity into a byte[]. */
+ private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {
+ PoolingByteArrayOutputStream bytes =
+ new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength());
+ byte[] buffer = null;
+ try {
+ InputStream in = entity.getContent();
+ if (in == null) {
+ throw new ServerError();
+ }
+ buffer = mPool.getBuf(1024);
+ int count;
+ while ((count = in.read(buffer)) != -1) {
+ bytes.write(buffer, 0, count);
+ }
+ return bytes.toByteArray();
+ } finally {
+ try {
+ // Close the InputStream and release the resources by "consuming the content".
+ entity.consumeContent();
+ } catch (IOException e) {
+ // This can happen if there was an exception above that left the entity in
+ // an invalid state.
+ VolleyLog.v("Error occured when calling consumingContent");
+ }
+ mPool.returnBuf(buffer);
+ bytes.close();
+ }
+ }
+
+ /**
+ * Converts Headers[] to Map<String, String>.
+ */
+ protected static Map<String, String> convertHeaders(Header[] headers) {
+ Map<String, String> result = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
+ for (int i = 0; i < headers.length; i++) {
+ result.put(headers[i].getName(), headers[i].getValue());
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/ByteArrayPool.java b/src/main/java/com/android/volley/toolbox/ByteArrayPool.java
new file mode 100644
index 0000000..af95076
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/ByteArrayPool.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2012 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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * ByteArrayPool is a source and repository of <code>byte[]</code> objects. Its purpose is to
+ * supply those buffers to consumers who need to use them for a short period of time and then
+ * dispose of them. Simply creating and disposing such buffers in the conventional manner can
+ * considerable heap churn and garbage collection delays on Android, which lacks good management of
+ * short-lived heap objects. It may be advantageous to trade off some memory in the form of a
+ * permanently allocated pool of buffers in order to gain heap performance improvements; that is
+ * what this class does.
+ * <p>
+ * A good candidate user for this class is something like an I/O system that uses large temporary
+ * <code>byte[]</code> buffers to copy data around. In these use cases, often the consumer wants
+ * the buffer to be a certain minimum size to ensure good performance (e.g. when copying data chunks
+ * off of a stream), but doesn't mind if the buffer is larger than the minimum. Taking this into
+ * account and also to maximize the odds of being able to reuse a recycled buffer, this class is
+ * free to return buffers larger than the requested size. The caller needs to be able to gracefully
+ * deal with getting buffers any size over the minimum.
+ * <p>
+ * If there is not a suitably-sized buffer in its recycling pool when a buffer is requested, this
+ * class will allocate a new buffer and return it.
+ * <p>
+ * This class has no special ownership of buffers it creates; the caller is free to take a buffer
+ * it receives from this pool, use it permanently, and never return it to the pool; additionally,
+ * it is not harmful to return to this pool a buffer that was allocated elsewhere, provided there
+ * are no other lingering references to it.
+ * <p>
+ * This class ensures that the total size of the buffers in its recycling pool never exceeds a
+ * certain byte limit. When a buffer is returned that would cause the pool to exceed the limit,
+ * least-recently-used buffers are disposed.
+ */
+public class ByteArrayPool {
+ /** The buffer pool, arranged both by last use and by buffer size */
+ private List<byte[]> mBuffersByLastUse = new LinkedList<byte[]>();
+ private List<byte[]> mBuffersBySize = new ArrayList<byte[]>(64);
+
+ /** The total size of the buffers in the pool */
+ private int mCurrentSize = 0;
+
+ /**
+ * The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay
+ * under this limit.
+ */
+ private final int mSizeLimit;
+
+ /** Compares buffers by size */
+ protected static final Comparator<byte[]> BUF_COMPARATOR = new Comparator<byte[]>() {
+ @Override
+ public int compare(byte[] lhs, byte[] rhs) {
+ return lhs.length - rhs.length;
+ }
+ };
+
+ /**
+ * @param sizeLimit the maximum size of the pool, in bytes
+ */
+ public ByteArrayPool(int sizeLimit) {
+ mSizeLimit = sizeLimit;
+ }
+
+ /**
+ * Returns a buffer from the pool if one is available in the requested size, or allocates a new
+ * one if a pooled one is not available.
+ *
+ * @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be
+ * larger.
+ * @return a byte[] buffer is always returned.
+ */
+ public synchronized byte[] getBuf(int len) {
+ for (int i = 0; i < mBuffersBySize.size(); i++) {
+ byte[] buf = mBuffersBySize.get(i);
+ if (buf.length >= len) {
+ mCurrentSize -= buf.length;
+ mBuffersBySize.remove(i);
+ mBuffersByLastUse.remove(buf);
+ return buf;
+ }
+ }
+ return new byte[len];
+ }
+
+ /**
+ * Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted
+ * size.
+ *
+ * @param buf the buffer to return to the pool.
+ */
+ public synchronized void returnBuf(byte[] buf) {
+ if (buf == null || buf.length > mSizeLimit) {
+ return;
+ }
+ mBuffersByLastUse.add(buf);
+ int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);
+ if (pos < 0) {
+ pos = -pos - 1;
+ }
+ mBuffersBySize.add(pos, buf);
+ mCurrentSize += buf.length;
+ trim();
+ }
+
+ /**
+ * Removes buffers from the pool until it is under its size limit.
+ */
+ private synchronized void trim() {
+ while (mCurrentSize > mSizeLimit) {
+ byte[] buf = mBuffersByLastUse.remove(0);
+ mBuffersBySize.remove(buf);
+ mCurrentSize -= buf.length;
+ }
+ }
+
+}
diff --git a/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java b/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java
new file mode 100644
index 0000000..a3478bf
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.Cache;
+import com.android.volley.NetworkResponse;
+import com.android.volley.Request;
+import com.android.volley.Response;
+
+import android.os.Handler;
+import android.os.Looper;
+
+/**
+ * A synthetic request used for clearing the cache.
+ */
+public class ClearCacheRequest extends Request<Object> {
+ private final Cache mCache;
+ private final Runnable mCallback;
+
+ /**
+ * Creates a synthetic request for clearing the cache.
+ * @param cache Cache to clear
+ * @param callback Callback to make on the main thread once the cache is clear,
+ * or null for none
+ */
+ public ClearCacheRequest(Cache cache, Runnable callback) {
+ super(Method.GET, null, null);
+ mCache = cache;
+ mCallback = callback;
+ }
+
+ @Override
+ public boolean isCanceled() {
+ // This is a little bit of a hack, but hey, why not.
+ mCache.clear();
+ if (mCallback != null) {
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.postAtFrontOfQueue(mCallback);
+ }
+ return true;
+ }
+
+ @Override
+ public Priority getPriority() {
+ return Priority.IMMEDIATE;
+ }
+
+ @Override
+ protected Response<Object> parseNetworkResponse(NetworkResponse response) {
+ return null;
+ }
+
+ @Override
+ protected void deliverResponse(Object response) {
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
new file mode 100644
index 0000000..b283788
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
@@ -0,0 +1,562 @@
+/*
+ * Copyright (C) 2011 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.os.SystemClock;
+
+import com.android.volley.Cache;
+import com.android.volley.VolleyLog;
+
+import java.io.BufferedInputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Cache implementation that caches files directly onto the hard disk in the specified
+ * directory. The default disk usage size is 5MB, but is configurable.
+ */
+public class DiskBasedCache implements Cache {
+
+ /** Map of the Key, CacheHeader pairs */
+ private final Map<String, CacheHeader> mEntries =
+ new LinkedHashMap<String, CacheHeader>(16, .75f, true);
+
+ /** Total amount of space currently used by the cache in bytes. */
+ private long mTotalSize = 0;
+
+ /** The root directory to use for the cache. */
+ private final File mRootDirectory;
+
+ /** The maximum size of the cache in bytes. */
+ private final int mMaxCacheSizeInBytes;
+
+ /** Default maximum disk usage in bytes. */
+ private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
+
+ /** High water mark percentage for the cache */
+ private static final float HYSTERESIS_FACTOR = 0.9f;
+
+ /** Magic number for current version of cache file format. */
+ private static final int CACHE_MAGIC = 0x20140623;
+
+ /**
+ * Constructs an instance of the DiskBasedCache at the specified directory.
+ * @param rootDirectory The root directory of the cache.
+ * @param maxCacheSizeInBytes The maximum size of the cache in bytes.
+ */
+ public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
+ mRootDirectory = rootDirectory;
+ mMaxCacheSizeInBytes = maxCacheSizeInBytes;
+ }
+
+ /**
+ * Constructs an instance of the DiskBasedCache at the specified directory using
+ * the default maximum cache size of 5MB.
+ * @param rootDirectory The root directory of the cache.
+ */
+ public DiskBasedCache(File rootDirectory) {
+ this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
+ }
+
+ /**
+ * Clears the cache. Deletes all cached files from disk.
+ */
+ @Override
+ public synchronized void clear() {
+ File[] files = mRootDirectory.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ file.delete();
+ }
+ }
+ mEntries.clear();
+ mTotalSize = 0;
+ VolleyLog.d("Cache cleared.");
+ }
+
+ /**
+ * Returns the cache entry with the specified key if it exists, null otherwise.
+ */
+ @Override
+ public synchronized Entry get(String key) {
+ CacheHeader entry = mEntries.get(key);
+ // if the entry does not exist, return.
+ if (entry == null) {
+ return null;
+ }
+
+ File file = getFileForKey(key);
+ CountingInputStream cis = null;
+ try {
+ cis = new CountingInputStream(new FileInputStream(file));
+ CacheHeader.readHeader(cis); // eat header
+ byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
+ return entry.toCacheEntry(data);
+ } catch (IOException e) {
+ VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
+ remove(key);
+ return null;
+ } finally {
+ if (cis != null) {
+ try {
+ cis.close();
+ } catch (IOException ioe) {
+ return null;
+ }
+ }
+ }
+ }
+
+ /**
+ * Initializes the DiskBasedCache by scanning for all files currently in the
+ * specified root directory. Creates the root directory if necessary.
+ */
+ @Override
+ public synchronized void initialize() {
+ if (!mRootDirectory.exists()) {
+ if (!mRootDirectory.mkdirs()) {
+ VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
+ }
+ return;
+ }
+
+ File[] files = mRootDirectory.listFiles();
+ if (files == null) {
+ return;
+ }
+ for (File file : files) {
+ BufferedInputStream fis = null;
+ try {
+ fis = new BufferedInputStream(new FileInputStream(file));
+ CacheHeader entry = CacheHeader.readHeader(fis);
+ entry.size = file.length();
+ putEntry(entry.key, entry);
+ } catch (IOException e) {
+ if (file != null) {
+ file.delete();
+ }
+ } finally {
+ try {
+ if (fis != null) {
+ fis.close();
+ }
+ } catch (IOException ignored) { }
+ }
+ }
+ }
+
+ /**
+ * Invalidates an entry in the cache.
+ * @param key Cache key
+ * @param fullExpire True to fully expire the entry, false to soft expire
+ */
+ @Override
+ public synchronized void invalidate(String key, boolean fullExpire) {
+ Entry entry = get(key);
+ if (entry != null) {
+ entry.softTtl = 0;
+ if (fullExpire) {
+ entry.ttl = 0;
+ }
+ put(key, entry);
+ }
+
+ }
+
+ /**
+ * Puts the entry with the specified key into the cache.
+ */
+ @Override
+ public synchronized void put(String key, Entry entry) {
+ pruneIfNeeded(entry.data.length);
+ File file = getFileForKey(key);
+ try {
+ FileOutputStream fos = new FileOutputStream(file);
+ CacheHeader e = new CacheHeader(key, entry);
+ boolean success = e.writeHeader(fos);
+ if (!success) {
+ fos.close();
+ VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
+ throw new IOException();
+ }
+ fos.write(entry.data);
+ fos.close();
+ putEntry(key, e);
+ return;
+ } catch (IOException e) {
+ }
+ boolean deleted = file.delete();
+ if (!deleted) {
+ VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
+ }
+ }
+
+ /**
+ * Removes the specified key from the cache if it exists.
+ */
+ @Override
+ public synchronized void remove(String key) {
+ boolean deleted = getFileForKey(key).delete();
+ removeEntry(key);
+ if (!deleted) {
+ VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
+ key, getFilenameForKey(key));
+ }
+ }
+
+ /**
+ * Creates a pseudo-unique filename for the specified cache key.
+ * @param key The key to generate a file name for.
+ * @return A pseudo-unique filename.
+ */
+ private String getFilenameForKey(String key) {
+ int firstHalfLength = key.length() / 2;
+ String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
+ localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
+ return localFilename;
+ }
+
+ /**
+ * Returns a file object for the given cache key.
+ */
+ public File getFileForKey(String key) {
+ return new File(mRootDirectory, getFilenameForKey(key));
+ }
+
+ /**
+ * Prunes the cache to fit the amount of bytes specified.
+ * @param neededSpace The amount of bytes we are trying to fit into the cache.
+ */
+ private void pruneIfNeeded(int neededSpace) {
+ if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
+ return;
+ }
+ if (VolleyLog.DEBUG) {
+ VolleyLog.v("Pruning old cache entries.");
+ }
+
+ long before = mTotalSize;
+ int prunedFiles = 0;
+ long startTime = SystemClock.elapsedRealtime();
+
+ Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry<String, CacheHeader> entry = iterator.next();
+ CacheHeader e = entry.getValue();
+ boolean deleted = getFileForKey(e.key).delete();
+ if (deleted) {
+ mTotalSize -= e.size;
+ } else {
+ VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
+ e.key, getFilenameForKey(e.key));
+ }
+ iterator.remove();
+ prunedFiles++;
+
+ if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
+ break;
+ }
+ }
+
+ if (VolleyLog.DEBUG) {
+ VolleyLog.v("pruned %d files, %d bytes, %d ms",
+ prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
+ }
+ }
+
+ /**
+ * Puts the entry with the specified key into the cache.
+ * @param key The key to identify the entry by.
+ * @param entry The entry to cache.
+ */
+ private void putEntry(String key, CacheHeader entry) {
+ if (!mEntries.containsKey(key)) {
+ mTotalSize += entry.size;
+ } else {
+ CacheHeader oldEntry = mEntries.get(key);
+ mTotalSize += (entry.size - oldEntry.size);
+ }
+ mEntries.put(key, entry);
+ }
+
+ /**
+ * Removes the entry identified by 'key' from the cache.
+ */
+ private void removeEntry(String key) {
+ CacheHeader entry = mEntries.get(key);
+ if (entry != null) {
+ mTotalSize -= entry.size;
+ mEntries.remove(key);
+ }
+ }
+
+ /**
+ * Reads the contents of an InputStream into a byte[].
+ * */
+ private static byte[] streamToBytes(InputStream in, int length) throws IOException {
+ byte[] bytes = new byte[length];
+ int count;
+ int pos = 0;
+ while (pos < length && ((count = in.read(bytes, pos, length - pos)) != -1)) {
+ pos += count;
+ }
+ if (pos != length) {
+ throw new IOException("Expected " + length + " bytes, read " + pos + " bytes");
+ }
+ return bytes;
+ }
+
+ /**
+ * Handles holding onto the cache headers for an entry.
+ */
+ // Visible for testing.
+ static class CacheHeader {
+ /** The size of the data identified by this CacheHeader. (This is not
+ * serialized to disk. */
+ public long size;
+
+ /** The key that identifies the cache entry. */
+ public String key;
+
+ /** ETag for cache coherence. */
+ public String etag;
+
+ /** Date of this response as reported by the server. */
+ public long serverDate;
+
+ /** TTL for this record. */
+ public long ttl;
+
+ /** Soft TTL for this record. */
+ public long softTtl;
+
+ /** Headers from the response resulting in this cache entry. */
+ public Map<String, String> responseHeaders;
+
+ private CacheHeader() { }
+
+ /**
+ * Instantiates a new CacheHeader object
+ * @param key The key that identifies the cache entry
+ * @param entry The cache entry.
+ */
+ public CacheHeader(String key, Entry entry) {
+ this.key = key;
+ this.size = entry.data.length;
+ this.etag = entry.etag;
+ this.serverDate = entry.serverDate;
+ this.ttl = entry.ttl;
+ this.softTtl = entry.softTtl;
+ this.responseHeaders = entry.responseHeaders;
+ }
+
+ /**
+ * Reads the header off of an InputStream and returns a CacheHeader object.
+ * @param is The InputStream to read from.
+ * @throws IOException
+ */
+ public static CacheHeader readHeader(InputStream is) throws IOException {
+ CacheHeader entry = new CacheHeader();
+ int magic = readInt(is);
+ if (magic != CACHE_MAGIC) {
+ // don't bother deleting, it'll get pruned eventually
+ throw new IOException();
+ }
+ entry.key = readString(is);
+ entry.etag = readString(is);
+ if (entry.etag.equals("")) {
+ entry.etag = null;
+ }
+ entry.serverDate = readLong(is);
+ entry.ttl = readLong(is);
+ entry.softTtl = readLong(is);
+ entry.responseHeaders = readStringStringMap(is);
+ return entry;
+ }
+
+ /**
+ * Creates a cache entry for the specified data.
+ */
+ public Entry toCacheEntry(byte[] data) {
+ Entry e = new Entry();
+ e.data = data;
+ e.etag = etag;
+ e.serverDate = serverDate;
+ e.ttl = ttl;
+ e.softTtl = softTtl;
+ e.responseHeaders = responseHeaders;
+ return e;
+ }
+
+
+ /**
+ * Writes the contents of this CacheHeader to the specified OutputStream.
+ */
+ public boolean writeHeader(OutputStream os) {
+ try {
+ writeInt(os, CACHE_MAGIC);
+ writeString(os, key);
+ writeString(os, etag == null ? "" : etag);
+ writeLong(os, serverDate);
+ writeLong(os, ttl);
+ writeLong(os, softTtl);
+ writeStringStringMap(responseHeaders, os);
+ os.flush();
+ return true;
+ } catch (IOException e) {
+ VolleyLog.d("%s", e.toString());
+ return false;
+ }
+ }
+
+ }
+
+ private static class CountingInputStream extends FilterInputStream {
+ private int bytesRead = 0;
+
+ private CountingInputStream(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public int read() throws IOException {
+ int result = super.read();
+ if (result != -1) {
+ bytesRead++;
+ }
+ return result;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int count) throws IOException {
+ int result = super.read(buffer, offset, count);
+ if (result != -1) {
+ bytesRead += result;
+ }
+ return result;
+ }
+ }
+
+ /*
+ * Homebrewed simple serialization system used for reading and writing cache
+ * headers on disk. Once upon a time, this used the standard Java
+ * Object{Input,Output}Stream, but the default implementation relies heavily
+ * on reflection (even for standard types) and generates a ton of garbage.
+ */
+
+ /**
+ * Simple wrapper around {@link InputStream#read()} that throws EOFException
+ * instead of returning -1.
+ */
+ private static int read(InputStream is) throws IOException {
+ int b = is.read();
+ if (b == -1) {
+ throw new EOFException();
+ }
+ return b;
+ }
+
+ static void writeInt(OutputStream os, int n) throws IOException {
+ os.write((n >> 0) & 0xff);
+ os.write((n >> 8) & 0xff);
+ os.write((n >> 16) & 0xff);
+ os.write((n >> 24) & 0xff);
+ }
+
+ static int readInt(InputStream is) throws IOException {
+ int n = 0;
+ n |= (read(is) << 0);
+ n |= (read(is) << 8);
+ n |= (read(is) << 16);
+ n |= (read(is) << 24);
+ return n;
+ }
+
+ static void writeLong(OutputStream os, long n) throws IOException {
+ os.write((byte)(n >>> 0));
+ os.write((byte)(n >>> 8));
+ os.write((byte)(n >>> 16));
+ os.write((byte)(n >>> 24));
+ os.write((byte)(n >>> 32));
+ os.write((byte)(n >>> 40));
+ os.write((byte)(n >>> 48));
+ os.write((byte)(n >>> 56));
+ }
+
+ static long readLong(InputStream is) throws IOException {
+ long n = 0;
+ n |= ((read(is) & 0xFFL) << 0);
+ n |= ((read(is) & 0xFFL) << 8);
+ n |= ((read(is) & 0xFFL) << 16);
+ n |= ((read(is) & 0xFFL) << 24);
+ n |= ((read(is) & 0xFFL) << 32);
+ n |= ((read(is) & 0xFFL) << 40);
+ n |= ((read(is) & 0xFFL) << 48);
+ n |= ((read(is) & 0xFFL) << 56);
+ return n;
+ }
+
+ static void writeString(OutputStream os, String s) throws IOException {
+ byte[] b = s.getBytes("UTF-8");
+ writeLong(os, b.length);
+ os.write(b, 0, b.length);
+ }
+
+ static String readString(InputStream is) throws IOException {
+ int n = (int) readLong(is);
+ byte[] b = streamToBytes(is, n);
+ return new String(b, "UTF-8");
+ }
+
+ static void writeStringStringMap(Map<String, String> map, OutputStream os) throws IOException {
+ if (map != null) {
+ writeInt(os, map.size());
+ for (Map.Entry<String, String> entry : map.entrySet()) {
+ writeString(os, entry.getKey());
+ writeString(os, entry.getValue());
+ }
+ } else {
+ writeInt(os, 0);
+ }
+ }
+
+ static Map<String, String> readStringStringMap(InputStream is) throws IOException {
+ int size = readInt(is);
+ Map<String, String> result = (size == 0)
+ ? Collections.<String, String>emptyMap()
+ : new HashMap<String, String>(size);
+ for (int i = 0; i < size; i++) {
+ String key = readString(is).intern();
+ String value = readString(is).intern();
+ result.put(key, value);
+ }
+ return result;
+ }
+
+
+}
diff --git a/src/main/java/com/android/volley/toolbox/HttpClientStack.java b/src/main/java/com/android/volley/toolbox/HttpClientStack.java
new file mode 100644
index 0000000..377110e
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/HttpClientStack.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.Request.Method;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpTrace;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An HttpStack that performs request over an {@link HttpClient}.
+ */
+public class HttpClientStack implements HttpStack {
+ protected final HttpClient mClient;
+
+ private final static String HEADER_CONTENT_TYPE = "Content-Type";
+
+ public HttpClientStack(HttpClient client) {
+ mClient = client;
+ }
+
+ private static void addHeaders(HttpUriRequest httpRequest, Map<String, String> headers) {
+ for (String key : headers.keySet()) {
+ httpRequest.setHeader(key, headers.get(key));
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static List<NameValuePair> getPostParameterPairs(Map<String, String> postParams) {
+ List<NameValuePair> result = new ArrayList<NameValuePair>(postParams.size());
+ for (String key : postParams.keySet()) {
+ result.add(new BasicNameValuePair(key, postParams.get(key)));
+ }
+ return result;
+ }
+
+ @Override
+ public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders);
+ addHeaders(httpRequest, additionalHeaders);
+ addHeaders(httpRequest, request.getHeaders());
+ onPrepareRequest(httpRequest);
+ HttpParams httpParams = httpRequest.getParams();
+ int timeoutMs = request.getTimeoutMs();
+ // TODO: Reevaluate this connection timeout based on more wide-scale
+ // data collection and possibly different for wifi vs. 3G.
+ HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
+ HttpConnectionParams.setSoTimeout(httpParams, timeoutMs);
+ return mClient.execute(httpRequest);
+ }
+
+ /**
+ * Creates the appropriate subclass of HttpUriRequest for passed in request.
+ */
+ @SuppressWarnings("deprecation")
+ /* protected */ static HttpUriRequest createHttpRequest(Request<?> request,
+ Map<String, String> additionalHeaders) throws AuthFailureError {
+ switch (request.getMethod()) {
+ case Method.DEPRECATED_GET_OR_POST: {
+ // This is the deprecated way that needs to be handled for backwards compatibility.
+ // If the request's post body is null, then the assumption is that the request is
+ // GET. Otherwise, it is assumed that the request is a POST.
+ byte[] postBody = request.getPostBody();
+ if (postBody != null) {
+ HttpPost postRequest = new HttpPost(request.getUrl());
+ postRequest.addHeader(HEADER_CONTENT_TYPE, request.getPostBodyContentType());
+ HttpEntity entity;
+ entity = new ByteArrayEntity(postBody);
+ postRequest.setEntity(entity);
+ return postRequest;
+ } else {
+ return new HttpGet(request.getUrl());
+ }
+ }
+ case Method.GET:
+ return new HttpGet(request.getUrl());
+ case Method.DELETE:
+ return new HttpDelete(request.getUrl());
+ case Method.POST: {
+ HttpPost postRequest = new HttpPost(request.getUrl());
+ postRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
+ setEntityIfNonEmptyBody(postRequest, request);
+ return postRequest;
+ }
+ case Method.PUT: {
+ HttpPut putRequest = new HttpPut(request.getUrl());
+ putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
+ setEntityIfNonEmptyBody(putRequest, request);
+ return putRequest;
+ }
+ case Method.HEAD:
+ return new HttpHead(request.getUrl());
+ case Method.OPTIONS:
+ return new HttpOptions(request.getUrl());
+ case Method.TRACE:
+ return new HttpTrace(request.getUrl());
+ case Method.PATCH: {
+ HttpPatch patchRequest = new HttpPatch(request.getUrl());
+ patchRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
+ setEntityIfNonEmptyBody(patchRequest, request);
+ return patchRequest;
+ }
+ default:
+ throw new IllegalStateException("Unknown request method.");
+ }
+ }
+
+ private static void setEntityIfNonEmptyBody(HttpEntityEnclosingRequestBase httpRequest,
+ Request<?> request) throws AuthFailureError {
+ byte[] body = request.getBody();
+ if (body != null) {
+ HttpEntity entity = new ByteArrayEntity(body);
+ httpRequest.setEntity(entity);
+ }
+ }
+
+ /**
+ * Called before the request is executed using the underlying HttpClient.
+ *
+ * <p>Overwrite in subclasses to augment the request.</p>
+ */
+ protected void onPrepareRequest(HttpUriRequest request) throws IOException {
+ // Nothing.
+ }
+
+ /**
+ * The HttpPatch class does not exist in the Android framework, so this has been defined here.
+ */
+ public static final class HttpPatch extends HttpEntityEnclosingRequestBase {
+
+ public final static String METHOD_NAME = "PATCH";
+
+ public HttpPatch() {
+ super();
+ }
+
+ public HttpPatch(final URI uri) {
+ super();
+ setURI(uri);
+ }
+
+ /**
+ * @throws IllegalArgumentException if the uri is invalid.
+ */
+ public HttpPatch(final String uri) {
+ super();
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return METHOD_NAME;
+ }
+
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
new file mode 100644
index 0000000..cb08432
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.Cache;
+import com.android.volley.NetworkResponse;
+
+import org.apache.http.impl.cookie.DateParseException;
+import org.apache.http.impl.cookie.DateUtils;
+import org.apache.http.protocol.HTTP;
+
+import java.util.Map;
+
+/**
+ * Utility methods for parsing HTTP headers.
+ */
+public class HttpHeaderParser {
+
+ /**
+ * Extracts a {@link Cache.Entry} from a {@link NetworkResponse}.
+ *
+ * @param response The network response to parse headers from
+ * @return a cache entry for the given response, or null if the response is not cacheable.
+ */
+ public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
+ long now = System.currentTimeMillis();
+
+ Map<String, String> headers = response.headers;
+
+ long serverDate = 0;
+ long serverExpires = 0;
+ long softExpire = 0;
+ long maxAge = 0;
+ boolean hasCacheControl = false;
+
+ String serverEtag = null;
+ String headerValue;
+
+ headerValue = headers.get("Date");
+ if (headerValue != null) {
+ serverDate = parseDateAsEpoch(headerValue);
+ }
+
+ headerValue = headers.get("Cache-Control");
+ if (headerValue != null) {
+ hasCacheControl = true;
+ String[] tokens = headerValue.split(",");
+ for (int i = 0; i < tokens.length; i++) {
+ String token = tokens[i].trim();
+ if (token.equals("no-cache") || token.equals("no-store")) {
+ return null;
+ } else if (token.startsWith("max-age=")) {
+ try {
+ maxAge = Long.parseLong(token.substring(8));
+ } catch (Exception e) {
+ }
+ } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
+ maxAge = 0;
+ }
+ }
+ }
+
+ headerValue = headers.get("Expires");
+ if (headerValue != null) {
+ serverExpires = parseDateAsEpoch(headerValue);
+ }
+
+ serverEtag = headers.get("ETag");
+
+ // Cache-Control takes precedence over an Expires header, even if both exist and Expires
+ // is more restrictive.
+ if (hasCacheControl) {
+ softExpire = now + maxAge * 1000;
+ } else if (serverDate > 0 && serverExpires >= serverDate) {
+ // Default semantic for Expire header in HTTP specification is softExpire.
+ softExpire = now + (serverExpires - serverDate);
+ }
+
+ Cache.Entry entry = new Cache.Entry();
+ entry.data = response.data;
+ entry.etag = serverEtag;
+ entry.softTtl = softExpire;
+ entry.ttl = entry.softTtl;
+ entry.serverDate = serverDate;
+ entry.responseHeaders = headers;
+
+ return entry;
+ }
+
+ /**
+ * Parse date in RFC1123 format, and return its value as epoch
+ */
+ public static long parseDateAsEpoch(String dateStr) {
+ try {
+ // Parse date in RFC1123 format if this header contains one
+ return DateUtils.parseDate(dateStr).getTime();
+ } catch (DateParseException e) {
+ // Date in invalid format, fallback to 0
+ return 0;
+ }
+ }
+
+ /**
+ * Returns the charset specified in the Content-Type of this header,
+ * or the HTTP default (ISO-8859-1) if none can be found.
+ */
+ public static String parseCharset(Map<String, String> headers) {
+ String contentType = headers.get(HTTP.CONTENT_TYPE);
+ if (contentType != null) {
+ String[] params = contentType.split(";");
+ for (int i = 1; i < params.length; i++) {
+ String[] pair = params[i].trim().split("=");
+ if (pair.length == 2) {
+ if (pair[0].equals("charset")) {
+ return pair[1];
+ }
+ }
+ }
+ }
+
+ return HTTP.DEFAULT_CONTENT_CHARSET;
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/HttpStack.java b/src/main/java/com/android/volley/toolbox/HttpStack.java
new file mode 100644
index 0000000..a52fd06
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/HttpStack.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+
+import org.apache.http.HttpResponse;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * An HTTP stack abstraction.
+ */
+public interface HttpStack {
+ /**
+ * Performs an HTTP request with the given parameters.
+ *
+ * <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
+ * and the Content-Type header is set to request.getPostBodyContentType().</p>
+ *
+ * @param request the request to perform
+ * @param additionalHeaders additional headers to be sent together with
+ * {@link Request#getHeaders()}
+ * @return the HTTP response
+ */
+ public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError;
+
+}
diff --git a/src/main/java/com/android/volley/toolbox/HurlStack.java b/src/main/java/com/android/volley/toolbox/HurlStack.java
new file mode 100644
index 0000000..31d57f0
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/HurlStack.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.Request.Method;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.StatusLine;
+import org.apache.http.entity.BasicHttpEntity;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.message.BasicStatusLine;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * An {@link HttpStack} based on {@link HttpURLConnection}.
+ */
+public class HurlStack implements HttpStack {
+
+ private static final String HEADER_CONTENT_TYPE = "Content-Type";
+
+ /**
+ * An interface for transforming URLs before use.
+ */
+ public interface UrlRewriter {
+ /**
+ * Returns a URL to use instead of the provided one, or null to indicate
+ * this URL should not be used at all.
+ */
+ public String rewriteUrl(String originalUrl);
+ }
+
+ private final UrlRewriter mUrlRewriter;
+ private final SSLSocketFactory mSslSocketFactory;
+
+ public HurlStack() {
+ this(null);
+ }
+
+ /**
+ * @param urlRewriter Rewriter to use for request URLs
+ */
+ public HurlStack(UrlRewriter urlRewriter) {
+ this(urlRewriter, null);
+ }
+
+ /**
+ * @param urlRewriter Rewriter to use for request URLs
+ * @param sslSocketFactory SSL factory to use for HTTPS connections
+ */
+ public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) {
+ mUrlRewriter = urlRewriter;
+ mSslSocketFactory = sslSocketFactory;
+ }
+
+ @Override
+ public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ String url = request.getUrl();
+ HashMap<String, String> map = new HashMap<String, String>();
+ map.putAll(request.getHeaders());
+ map.putAll(additionalHeaders);
+ if (mUrlRewriter != null) {
+ String rewritten = mUrlRewriter.rewriteUrl(url);
+ if (rewritten == null) {
+ throw new IOException("URL blocked by rewriter: " + url);
+ }
+ url = rewritten;
+ }
+ URL parsedUrl = new URL(url);
+ HttpURLConnection connection = openConnection(parsedUrl, request);
+ for (String headerName : map.keySet()) {
+ connection.addRequestProperty(headerName, map.get(headerName));
+ }
+ setConnectionParametersForRequest(connection, request);
+ // Initialize HttpResponse with data from the HttpURLConnection.
+ ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
+ int responseCode = connection.getResponseCode();
+ if (responseCode == -1) {
+ // -1 is returned by getResponseCode() if the response code could not be retrieved.
+ // Signal to the caller that something was wrong with the connection.
+ throw new IOException("Could not retrieve response code from HttpUrlConnection.");
+ }
+ StatusLine responseStatus = new BasicStatusLine(protocolVersion,
+ connection.getResponseCode(), connection.getResponseMessage());
+ BasicHttpResponse response = new BasicHttpResponse(responseStatus);
+ response.setEntity(entityFromConnection(connection));
+ for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
+ if (header.getKey() != null) {
+ Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
+ response.addHeader(h);
+ }
+ }
+ return response;
+ }
+
+ /**
+ * Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}.
+ * @param connection
+ * @return an HttpEntity populated with data from <code>connection</code>.
+ */
+ private static HttpEntity entityFromConnection(HttpURLConnection connection) {
+ BasicHttpEntity entity = new BasicHttpEntity();
+ InputStream inputStream;
+ try {
+ inputStream = connection.getInputStream();
+ } catch (IOException ioe) {
+ inputStream = connection.getErrorStream();
+ }
+ entity.setContent(inputStream);
+ entity.setContentLength(connection.getContentLength());
+ entity.setContentEncoding(connection.getContentEncoding());
+ entity.setContentType(connection.getContentType());
+ return entity;
+ }
+
+ /**
+ * 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 = createConnection(url);
+
+ int timeoutMs = request.getTimeoutMs();
+ connection.setConnectTimeout(timeoutMs);
+ connection.setReadTimeout(timeoutMs);
+ connection.setUseCaches(false);
+ connection.setDoInput(true);
+
+ // use caller-provided custom SslSocketFactory, if any, for HTTPS
+ if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
+ ((HttpsURLConnection)connection).setSSLSocketFactory(mSslSocketFactory);
+ }
+
+ return connection;
+ }
+
+ @SuppressWarnings("deprecation")
+ /* package */ static void setConnectionParametersForRequest(HttpURLConnection connection,
+ Request<?> request) throws IOException, AuthFailureError {
+ switch (request.getMethod()) {
+ case Method.DEPRECATED_GET_OR_POST:
+ // This is the deprecated way that needs to be handled for backwards compatibility.
+ // If the request's post body is null, then the assumption is that the request is
+ // GET. Otherwise, it is assumed that the request is a POST.
+ byte[] postBody = request.getPostBody();
+ if (postBody != null) {
+ // Prepare output. There is no need to set Content-Length explicitly,
+ // since this is handled by HttpURLConnection using the size of the prepared
+ // output stream.
+ connection.setDoOutput(true);
+ connection.setRequestMethod("POST");
+ connection.addRequestProperty(HEADER_CONTENT_TYPE,
+ request.getPostBodyContentType());
+ DataOutputStream out = new DataOutputStream(connection.getOutputStream());
+ out.write(postBody);
+ out.close();
+ }
+ break;
+ case Method.GET:
+ // Not necessary to set the request method because connection defaults to GET but
+ // being explicit here.
+ connection.setRequestMethod("GET");
+ break;
+ case Method.DELETE:
+ connection.setRequestMethod("DELETE");
+ break;
+ case Method.POST:
+ connection.setRequestMethod("POST");
+ addBodyIfExists(connection, request);
+ break;
+ case Method.PUT:
+ connection.setRequestMethod("PUT");
+ addBodyIfExists(connection, request);
+ break;
+ case Method.HEAD:
+ connection.setRequestMethod("HEAD");
+ break;
+ case Method.OPTIONS:
+ connection.setRequestMethod("OPTIONS");
+ break;
+ case Method.TRACE:
+ connection.setRequestMethod("TRACE");
+ break;
+ case Method.PATCH:
+ connection.setRequestMethod("PATCH");
+ addBodyIfExists(connection, request);
+ break;
+ default:
+ throw new IllegalStateException("Unknown method type.");
+ }
+ }
+
+ private static void addBodyIfExists(HttpURLConnection connection, Request<?> request)
+ throws IOException, AuthFailureError {
+ byte[] body = request.getBody();
+ if (body != null) {
+ connection.setDoOutput(true);
+ connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType());
+ DataOutputStream out = new DataOutputStream(connection.getOutputStream());
+ out.write(body);
+ out.close();
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/ImageLoader.java b/src/main/java/com/android/volley/toolbox/ImageLoader.java
new file mode 100644
index 0000000..5348dc6
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/ImageLoader.java
@@ -0,0 +1,482 @@
+/**
+ * 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 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<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, cacheKey);
+
+ mRequestQueue.add(newRequest);
+ mInFlightRequests.put(cacheKey,
+ new BatchedImageRequest(newRequest, imageContainer));
+ return imageContainer;
+ }
+
+ protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight, final String cacheKey) {
+ return 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);
+ }
+ });
+ }
+
+ /**
+ * 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.
+ */
+ protected 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);
+ }
+ }
+
+ /**
+ * Handler for when an image failed to load.
+ * @param cacheKey The cache key that is associated with the image request.
+ */
+ protected 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) {
+ // Set the error for this request
+ request.setError(error);
+
+ // Send the batched response
+ batchResponse(cacheKey, request);
+ }
+ }
+
+ /**
+ * 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;
+
+ /** Error if one occurred for this response */
+ private VolleyError mError;
+
+ /** 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);
+ }
+
+ /**
+ * Set the error for this response
+ */
+ public void setError(VolleyError error) {
+ mError = error;
+ }
+
+ /**
+ * Get the error for this response
+ */
+ public VolleyError getError() {
+ return mError;
+ }
+
+ /**
+ * 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) {
+ 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 (bir.getError() == null) {
+ container.mBitmap = bir.mResponseBitmap;
+ container.mListener.onResponse(container, false);
+ } else {
+ container.mListener.onErrorResponse(bir.getError());
+ }
+ }
+ }
+ 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/main/java/com/android/volley/toolbox/ImageRequest.java b/src/main/java/com/android/volley/toolbox/ImageRequest.java
new file mode 100644
index 0000000..2ebe015
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/ImageRequest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.DefaultRetryPolicy;
+import com.android.volley.NetworkResponse;
+import com.android.volley.ParseError;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.VolleyLog;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+
+/**
+ * A canned request for getting an image at a given URL and calling
+ * back with a decoded Bitmap.
+ */
+public class ImageRequest extends Request<Bitmap> {
+ /** Socket timeout in milliseconds for image requests */
+ private static final int IMAGE_TIMEOUT_MS = 1000;
+
+ /** Default number of retries for image requests */
+ private static final int IMAGE_MAX_RETRIES = 2;
+
+ /** Default backoff multiplier for image requests */
+ private static final float IMAGE_BACKOFF_MULT = 2f;
+
+ private final Response.Listener<Bitmap> mListener;
+ private final Config mDecodeConfig;
+ private final int mMaxWidth;
+ private final int mMaxHeight;
+
+ /** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */
+ private static final Object sDecodeLock = new Object();
+
+ /**
+ * Creates a new image request, decoding to a maximum specified width and
+ * height. If both width and height are zero, the image will be decoded to
+ * its natural size. If one of the two is nonzero, that dimension will be
+ * clamped and the other one will be set to preserve the image's aspect
+ * ratio. If both width and height are nonzero, the image will be decoded to
+ * be fit in the rectangle of dimensions width x height while keeping its
+ * aspect ratio.
+ *
+ * @param url URL of the image
+ * @param listener Listener to receive the decoded bitmap
+ * @param maxWidth Maximum width to decode this bitmap to, or zero for none
+ * @param maxHeight Maximum height to decode this bitmap to, or zero for
+ * none
+ * @param decodeConfig Format to decode the bitmap to
+ * @param errorListener Error listener, or null to ignore errors
+ */
+ public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
+ Config decodeConfig, Response.ErrorListener errorListener) {
+ super(Method.GET, url, errorListener);
+ setRetryPolicy(
+ new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT));
+ mListener = listener;
+ mDecodeConfig = decodeConfig;
+ mMaxWidth = maxWidth;
+ mMaxHeight = maxHeight;
+ }
+
+ @Override
+ public Priority getPriority() {
+ return Priority.LOW;
+ }
+
+ /**
+ * Scales one side of a rectangle to fit aspect ratio.
+ *
+ * @param maxPrimary Maximum size of the primary dimension (i.e. width for
+ * max width), or zero to maintain aspect ratio with secondary
+ * dimension
+ * @param maxSecondary Maximum size of the secondary dimension, or zero to
+ * maintain aspect ratio with primary dimension
+ * @param actualPrimary Actual size of the primary dimension
+ * @param actualSecondary Actual size of the secondary dimension
+ */
+ private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
+ int actualSecondary) {
+ // If no dominant value at all, just return the actual.
+ if (maxPrimary == 0 && maxSecondary == 0) {
+ return actualPrimary;
+ }
+
+ // If primary is unspecified, scale primary to match secondary's scaling ratio.
+ if (maxPrimary == 0) {
+ double ratio = (double) maxSecondary / (double) actualSecondary;
+ return (int) (actualPrimary * ratio);
+ }
+
+ if (maxSecondary == 0) {
+ return maxPrimary;
+ }
+
+ double ratio = (double) actualSecondary / (double) actualPrimary;
+ int resized = maxPrimary;
+ if (resized * ratio > maxSecondary) {
+ resized = (int) (maxSecondary / ratio);
+ }
+ return resized;
+ }
+
+ @Override
+ protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
+ // Serialize all decode on a global lock to reduce concurrent heap usage.
+ synchronized (sDecodeLock) {
+ try {
+ return doParse(response);
+ } catch (OutOfMemoryError e) {
+ VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
+ return Response.error(new ParseError(e));
+ }
+ }
+ }
+
+ /**
+ * The real guts of parseNetworkResponse. Broken out for readability.
+ */
+ private Response<Bitmap> doParse(NetworkResponse response) {
+ byte[] data = response.data;
+ BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
+ Bitmap bitmap = null;
+ if (mMaxWidth == 0 && mMaxHeight == 0) {
+ decodeOptions.inPreferredConfig = mDecodeConfig;
+ bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
+ } else {
+ // If we have to resize this image, first get the natural bounds.
+ decodeOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
+ int actualWidth = decodeOptions.outWidth;
+ int actualHeight = decodeOptions.outHeight;
+
+ // Then compute the dimensions we would ideally like to decode to.
+ int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
+ actualWidth, actualHeight);
+ int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
+ actualHeight, actualWidth);
+
+ // Decode to the nearest power of two scaling factor.
+ decodeOptions.inJustDecodeBounds = false;
+ // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
+ // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
+ decodeOptions.inSampleSize =
+ findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
+ Bitmap tempBitmap =
+ BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
+
+ // If necessary, scale down to the maximal acceptable size.
+ if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
+ tempBitmap.getHeight() > desiredHeight)) {
+ bitmap = Bitmap.createScaledBitmap(tempBitmap,
+ desiredWidth, desiredHeight, true);
+ tempBitmap.recycle();
+ } else {
+ bitmap = tempBitmap;
+ }
+ }
+
+ if (bitmap == null) {
+ return Response.error(new ParseError(response));
+ } else {
+ return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
+ }
+ }
+
+ @Override
+ protected void deliverResponse(Bitmap response) {
+ mListener.onResponse(response);
+ }
+
+ /**
+ * Returns the largest power-of-two divisor for use in downscaling a bitmap
+ * that will not result in the scaling past the desired dimensions.
+ *
+ * @param actualWidth Actual width of the bitmap
+ * @param actualHeight Actual height of the bitmap
+ * @param desiredWidth Desired width of the bitmap
+ * @param desiredHeight Desired height of the bitmap
+ */
+ // Visible for testing.
+ static int findBestSampleSize(
+ int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
+ double wr = (double) actualWidth / desiredWidth;
+ double hr = (double) actualHeight / desiredHeight;
+ double ratio = Math.min(wr, hr);
+ float n = 1.0f;
+ while ((n * 2) <= ratio) {
+ n *= 2;
+ }
+
+ return (int) n;
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java b/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java
new file mode 100644
index 0000000..b1eae80
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.NetworkResponse;
+import com.android.volley.ParseError;
+import com.android.volley.Response;
+import com.android.volley.Response.ErrorListener;
+import com.android.volley.Response.Listener;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A request for retrieving a {@link JSONArray} response body at a given URL.
+ */
+public class JsonArrayRequest extends JsonRequest<JSONArray> {
+
+ /**
+ * Creates a new request.
+ * @param url URL to fetch the JSON from
+ * @param listener Listener to receive the JSON response
+ * @param errorListener Error listener, or null to ignore errors.
+ */
+ public JsonArrayRequest(String url, Listener<JSONArray> listener, ErrorListener errorListener) {
+ super(Method.GET, url, null, listener, errorListener);
+ }
+
+ @Override
+ protected Response<JSONArray> parseNetworkResponse(NetworkResponse response) {
+ try {
+ String jsonString =
+ new String(response.data, HttpHeaderParser.parseCharset(response.headers));
+ return Response.success(new JSONArray(jsonString),
+ HttpHeaderParser.parseCacheHeaders(response));
+ } catch (UnsupportedEncodingException e) {
+ return Response.error(new ParseError(e));
+ } catch (JSONException je) {
+ return Response.error(new ParseError(je));
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java b/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java
new file mode 100644
index 0000000..74821cb
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.NetworkResponse;
+import com.android.volley.ParseError;
+import com.android.volley.Response;
+import com.android.volley.Response.ErrorListener;
+import com.android.volley.Response.Listener;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A request for retrieving a {@link JSONObject} response body at a given URL, allowing for an
+ * optional {@link JSONObject} to be passed in as part of the request body.
+ */
+public class JsonObjectRequest extends JsonRequest<JSONObject> {
+
+ /**
+ * Creates a new request.
+ * @param method the HTTP method to use
+ * @param url URL to fetch the JSON from
+ * @param jsonRequest A {@link JSONObject} to post with the request. Null is allowed and
+ * indicates no parameters will be posted along with request.
+ * @param listener Listener to receive the JSON response
+ * @param errorListener Error listener, or null to ignore errors.
+ */
+ public JsonObjectRequest(int method, String url, JSONObject jsonRequest,
+ Listener<JSONObject> listener, ErrorListener errorListener) {
+ super(method, url, (jsonRequest == null) ? null : jsonRequest.toString(), listener,
+ errorListener);
+ }
+
+ /**
+ * Constructor which defaults to <code>GET</code> if <code>jsonRequest</code> is
+ * <code>null</code>, <code>POST</code> otherwise.
+ *
+ * @see #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener)
+ */
+ public JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener,
+ ErrorListener errorListener) {
+ this(jsonRequest == null ? Method.GET : Method.POST, url, jsonRequest,
+ listener, errorListener);
+ }
+
+ @Override
+ protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
+ try {
+ String jsonString =
+ new String(response.data, HttpHeaderParser.parseCharset(response.headers));
+ return Response.success(new JSONObject(jsonString),
+ HttpHeaderParser.parseCacheHeaders(response));
+ } catch (UnsupportedEncodingException e) {
+ return Response.error(new ParseError(e));
+ } catch (JSONException je) {
+ return Response.error(new ParseError(je));
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/JsonRequest.java b/src/main/java/com/android/volley/toolbox/JsonRequest.java
new file mode 100644
index 0000000..f11ac14
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/JsonRequest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.NetworkResponse;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.Response.ErrorListener;
+import com.android.volley.Response.Listener;
+import com.android.volley.VolleyLog;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A request for retrieving a T type response body at a given URL that also
+ * optionally sends along a JSON body in the request specified.
+ *
+ * @param <T> JSON type of response expected
+ */
+public abstract class JsonRequest<T> extends Request<T> {
+ /** Charset for request. */
+ private static final String PROTOCOL_CHARSET = "utf-8";
+
+ /** Content type for request. */
+ private static final String PROTOCOL_CONTENT_TYPE =
+ String.format("application/json; charset=%s", PROTOCOL_CHARSET);
+
+ private final Listener<T> mListener;
+ private final String mRequestBody;
+
+ /**
+ * Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()}
+ * or {@link #getPostParams()} is overridden (which defaults to POST).
+ *
+ * @deprecated Use {@link #JsonRequest(int, String, String, Listener, ErrorListener)}.
+ */
+ public JsonRequest(String url, String requestBody, Listener<T> listener,
+ ErrorListener errorListener) {
+ this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener);
+ }
+
+ public JsonRequest(int method, String url, String requestBody, Listener<T> listener,
+ ErrorListener errorListener) {
+ super(method, url, errorListener);
+ mListener = listener;
+ mRequestBody = requestBody;
+ }
+
+ @Override
+ protected void deliverResponse(T response) {
+ mListener.onResponse(response);
+ }
+
+ @Override
+ abstract protected Response<T> parseNetworkResponse(NetworkResponse response);
+
+ /**
+ * @deprecated Use {@link #getBodyContentType()}.
+ */
+ @Override
+ public String getPostBodyContentType() {
+ return getBodyContentType();
+ }
+
+ /**
+ * @deprecated Use {@link #getBody()}.
+ */
+ @Override
+ public byte[] getPostBody() {
+ return getBody();
+ }
+
+ @Override
+ public String getBodyContentType() {
+ return PROTOCOL_CONTENT_TYPE;
+ }
+
+ @Override
+ public byte[] getBody() {
+ try {
+ return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET);
+ } catch (UnsupportedEncodingException uee) {
+ VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s",
+ mRequestBody, PROTOCOL_CHARSET);
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/NetworkImageView.java b/src/main/java/com/android/volley/toolbox/NetworkImageView.java
new file mode 100644
index 0000000..692e988
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/NetworkImageView.java
@@ -0,0 +1,219 @@
+/**
+ * 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.view.ViewGroup.LayoutParams;
+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.
+ */
+ void loadImageIfNecessary(final boolean isInLayoutPass) {
+ int width = getWidth();
+ int height = getHeight();
+
+ boolean wrapWidth = false, wrapHeight = false;
+ if (getLayoutParams() != null) {
+ wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
+ wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
+ }
+
+ // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
+ // view, hold off on loading the image.
+ boolean isFullyWrapContent = wrapWidth && wrapHeight;
+ if (width == 0 && height == 0 && !isFullyWrapContent) {
+ 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;
+ }
+ setDefaultImageOrNull();
+ 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();
+ setDefaultImageOrNull();
+ }
+ }
+
+ // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
+ int maxWidth = wrapWidth ? 0 : width;
+ int maxHeight = wrapHeight ? 0 : height;
+
+ // 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);
+ }
+ }
+ }, maxWidth, maxHeight);
+
+ // update the ImageContainer to be the new bitmap container.
+ mImageContainer = newContainer;
+ }
+
+ private void setDefaultImageOrNull() {
+ if(mDefaultImageId != 0) {
+ setImageResource(mDefaultImageId);
+ }
+ else {
+ setImageBitmap(null);
+ }
+ }
+
+ @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/main/java/com/android/volley/toolbox/NoCache.java b/src/main/java/com/android/volley/toolbox/NoCache.java
new file mode 100644
index 0000000..ab66254
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/NoCache.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.Cache;
+
+/**
+ * A cache that doesn't.
+ */
+public class NoCache implements Cache {
+ @Override
+ public void clear() {
+ }
+
+ @Override
+ public Entry get(String key) {
+ return null;
+ }
+
+ @Override
+ public void put(String key, Entry entry) {
+ }
+
+ @Override
+ public void invalidate(String key, boolean fullExpire) {
+ }
+
+ @Override
+ public void remove(String key) {
+ }
+
+ @Override
+ public void initialize() {
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java b/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java
new file mode 100644
index 0000000..9971566
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2012 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 java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * A variation of {@link java.io.ByteArrayOutputStream} that uses a pool of byte[] buffers instead
+ * of always allocating them fresh, saving on heap churn.
+ */
+public class PoolingByteArrayOutputStream extends ByteArrayOutputStream {
+ /**
+ * If the {@link #PoolingByteArrayOutputStream(ByteArrayPool)} constructor is called, this is
+ * the default size to which the underlying byte array is initialized.
+ */
+ private static final int DEFAULT_SIZE = 256;
+
+ private final ByteArrayPool mPool;
+
+ /**
+ * Constructs a new PoolingByteArrayOutputStream with a default size. If more bytes are written
+ * to this instance, the underlying byte array will expand.
+ */
+ public PoolingByteArrayOutputStream(ByteArrayPool pool) {
+ this(pool, DEFAULT_SIZE);
+ }
+
+ /**
+ * Constructs a new {@code ByteArrayOutputStream} with a default size of {@code size} bytes. If
+ * more than {@code size} bytes are written to this instance, the underlying byte array will
+ * expand.
+ *
+ * @param size initial size for the underlying byte array. The value will be pinned to a default
+ * minimum size.
+ */
+ public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) {
+ mPool = pool;
+ buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE));
+ }
+
+ @Override
+ public void close() throws IOException {
+ mPool.returnBuf(buf);
+ buf = null;
+ super.close();
+ }
+
+ @Override
+ public void finalize() {
+ mPool.returnBuf(buf);
+ }
+
+ /**
+ * Ensures there is enough space in the buffer for the given number of additional bytes.
+ */
+ private void expand(int i) {
+ /* Can the buffer handle @i more bytes, if not expand it */
+ if (count + i <= buf.length) {
+ return;
+ }
+ byte[] newbuf = mPool.getBuf((count + i) * 2);
+ System.arraycopy(buf, 0, newbuf, 0, count);
+ mPool.returnBuf(buf);
+ buf = newbuf;
+ }
+
+ @Override
+ public synchronized void write(byte[] buffer, int offset, int len) {
+ expand(len);
+ super.write(buffer, offset, len);
+ }
+
+ @Override
+ public synchronized void write(int oneByte) {
+ expand(1);
+ super.write(oneByte);
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/RequestFuture.java b/src/main/java/com/android/volley/toolbox/RequestFuture.java
new file mode 100644
index 0000000..173c44c
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/RequestFuture.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A Future that represents a Volley request.
+ *
+ * Used by providing as your response and error listeners. For example:
+ * <pre>
+ * RequestFuture&lt;JSONObject&gt; future = RequestFuture.newFuture();
+ * MyRequest request = new MyRequest(URL, future, future);
+ *
+ * // If you want to be able to cancel the request:
+ * future.setRequest(requestQueue.add(request));
+ *
+ * // Otherwise:
+ * requestQueue.add(request);
+ *
+ * try {
+ * JSONObject response = future.get();
+ * // do something with response
+ * } catch (InterruptedException e) {
+ * // handle the error
+ * } catch (ExecutionException e) {
+ * // handle the error
+ * }
+ * </pre>
+ *
+ * @param <T> The type of parsed response this future expects.
+ */
+public class RequestFuture<T> implements Future<T>, Response.Listener<T>,
+ Response.ErrorListener {
+ private Request<?> mRequest;
+ private boolean mResultReceived = false;
+ private T mResult;
+ private VolleyError mException;
+
+ public static <E> RequestFuture<E> newFuture() {
+ return new RequestFuture<E>();
+ }
+
+ private RequestFuture() {}
+
+ public void setRequest(Request<?> request) {
+ mRequest = request;
+ }
+
+ @Override
+ public synchronized boolean cancel(boolean mayInterruptIfRunning) {
+ if (mRequest == null) {
+ return false;
+ }
+
+ if (!isDone()) {
+ mRequest.cancel();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ try {
+ return doGet(null);
+ } catch (TimeoutException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return doGet(TimeUnit.MILLISECONDS.convert(timeout, unit));
+ }
+
+ private synchronized T doGet(Long timeoutMs)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ if (mException != null) {
+ throw new ExecutionException(mException);
+ }
+
+ if (mResultReceived) {
+ return mResult;
+ }
+
+ if (timeoutMs == null) {
+ wait(0);
+ } else if (timeoutMs > 0) {
+ wait(timeoutMs);
+ }
+
+ if (mException != null) {
+ throw new ExecutionException(mException);
+ }
+
+ if (!mResultReceived) {
+ throw new TimeoutException();
+ }
+
+ return mResult;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ if (mRequest == null) {
+ return false;
+ }
+ return mRequest.isCanceled();
+ }
+
+ @Override
+ public synchronized boolean isDone() {
+ return mResultReceived || mException != null || isCancelled();
+ }
+
+ @Override
+ public synchronized void onResponse(T response) {
+ mResultReceived = true;
+ mResult = response;
+ notifyAll();
+ }
+
+ @Override
+ public synchronized void onErrorResponse(VolleyError error) {
+ mException = error;
+ notifyAll();
+ }
+}
+
diff --git a/src/main/java/com/android/volley/toolbox/StringRequest.java b/src/main/java/com/android/volley/toolbox/StringRequest.java
new file mode 100644
index 0000000..6b3dfcf
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/StringRequest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2011 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 com.android.volley.NetworkResponse;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.Response.ErrorListener;
+import com.android.volley.Response.Listener;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A canned request for retrieving the response body at a given URL as a String.
+ */
+public class StringRequest extends Request<String> {
+ private final Listener<String> mListener;
+
+ /**
+ * Creates a new request with the given method.
+ *
+ * @param method the request {@link Method} to use
+ * @param url URL to fetch the string at
+ * @param listener Listener to receive the String response
+ * @param errorListener Error listener, or null to ignore errors
+ */
+ public StringRequest(int method, String url, Listener<String> listener,
+ ErrorListener errorListener) {
+ super(method, url, errorListener);
+ mListener = listener;
+ }
+
+ /**
+ * Creates a new GET request.
+ *
+ * @param url URL to fetch the string at
+ * @param listener Listener to receive the String response
+ * @param errorListener Error listener, or null to ignore errors
+ */
+ public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
+ this(Method.GET, url, listener, errorListener);
+ }
+
+ @Override
+ protected void deliverResponse(String response) {
+ mListener.onResponse(response);
+ }
+
+ @Override
+ protected Response<String> parseNetworkResponse(NetworkResponse response) {
+ String parsed;
+ try {
+ parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
+ } catch (UnsupportedEncodingException e) {
+ parsed = new String(response.data);
+ }
+ return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/Volley.java b/src/main/java/com/android/volley/toolbox/Volley.java
new file mode 100644
index 0000000..0e04e87
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/Volley.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2012 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.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.http.AndroidHttpClient;
+import android.os.Build;
+
+import com.android.volley.Network;
+import com.android.volley.RequestQueue;
+
+import java.io.File;
+
+public class Volley {
+
+ /** Default on-disk cache directory. */
+ private static final String DEFAULT_CACHE_DIR = "volley";
+
+ /**
+ * 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, HttpStack stack) {
+ File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
+
+ String userAgent = "volley/0";
+ try {
+ String packageName = context.getPackageName();
+ PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+ userAgent = packageName + "/" + info.versionCode;
+ } catch (NameNotFoundException e) {
+ }
+
+ 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);
+
+ RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
+ queue.start();
+
+ 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);
+ }
+}