From b9b8dc3d98fb1a8c3f02c2c2fcc18cbd344c05cb Mon Sep 17 00:00:00 2001 From: Ficus Kirkpatrick Date: Sat, 29 Nov 2014 09:32:54 -0800 Subject: 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 --- .../java/com/android/volley/AuthFailureError.java | 61 +++ src/main/java/com/android/volley/Cache.java | 97 ++++ .../java/com/android/volley/CacheDispatcher.java | 158 ++++++ .../com/android/volley/DefaultRetryPolicy.java | 105 ++++ .../java/com/android/volley/ExecutorDelivery.java | 118 ++++ src/main/java/com/android/volley/Network.java | 30 + .../java/com/android/volley/NetworkDispatcher.java | 152 ++++++ src/main/java/com/android/volley/NetworkError.java | 38 ++ .../java/com/android/volley/NetworkResponse.java | 73 +++ .../java/com/android/volley/NoConnectionError.java | 31 ++ src/main/java/com/android/volley/ParseError.java | 36 ++ src/main/java/com/android/volley/Request.java | 603 +++++++++++++++++++++ src/main/java/com/android/volley/RequestQueue.java | 286 ++++++++++ src/main/java/com/android/volley/Response.java | 85 +++ .../java/com/android/volley/ResponseDelivery.java | 35 ++ src/main/java/com/android/volley/RetryPolicy.java | 41 ++ src/main/java/com/android/volley/ServerError.java | 34 ++ src/main/java/com/android/volley/TimeoutError.java | 23 + src/main/java/com/android/volley/VolleyError.java | 57 ++ src/main/java/com/android/volley/VolleyLog.java | 176 ++++++ .../volley/toolbox/AndroidAuthenticator.java | 102 ++++ .../com/android/volley/toolbox/Authenticator.java | 36 ++ .../com/android/volley/toolbox/BasicNetwork.java | 266 +++++++++ .../com/android/volley/toolbox/ByteArrayPool.java | 135 +++++ .../android/volley/toolbox/ClearCacheRequest.java | 70 +++ .../com/android/volley/toolbox/DiskBasedCache.java | 562 +++++++++++++++++++ .../android/volley/toolbox/HttpClientStack.java | 194 +++++++ .../android/volley/toolbox/HttpHeaderParser.java | 137 +++++ .../java/com/android/volley/toolbox/HttpStack.java | 45 ++ .../java/com/android/volley/toolbox/HurlStack.java | 245 +++++++++ .../com/android/volley/toolbox/ImageLoader.java | 482 ++++++++++++++++ .../com/android/volley/toolbox/ImageRequest.java | 211 +++++++ .../android/volley/toolbox/JsonArrayRequest.java | 58 ++ .../android/volley/toolbox/JsonObjectRequest.java | 76 +++ .../com/android/volley/toolbox/JsonRequest.java | 102 ++++ .../android/volley/toolbox/NetworkImageView.java | 219 ++++++++ .../java/com/android/volley/toolbox/NoCache.java | 49 ++ .../toolbox/PoolingByteArrayOutputStream.java | 93 ++++ .../com/android/volley/toolbox/RequestFuture.java | 153 ++++++ .../com/android/volley/toolbox/StringRequest.java | 73 +++ .../java/com/android/volley/toolbox/Volley.java | 80 +++ 41 files changed, 5627 insertions(+) create mode 100644 src/main/java/com/android/volley/AuthFailureError.java create mode 100644 src/main/java/com/android/volley/Cache.java create mode 100644 src/main/java/com/android/volley/CacheDispatcher.java create mode 100644 src/main/java/com/android/volley/DefaultRetryPolicy.java create mode 100644 src/main/java/com/android/volley/ExecutorDelivery.java create mode 100644 src/main/java/com/android/volley/Network.java create mode 100644 src/main/java/com/android/volley/NetworkDispatcher.java create mode 100644 src/main/java/com/android/volley/NetworkError.java create mode 100644 src/main/java/com/android/volley/NetworkResponse.java create mode 100644 src/main/java/com/android/volley/NoConnectionError.java create mode 100644 src/main/java/com/android/volley/ParseError.java create mode 100644 src/main/java/com/android/volley/Request.java create mode 100644 src/main/java/com/android/volley/RequestQueue.java create mode 100644 src/main/java/com/android/volley/Response.java create mode 100644 src/main/java/com/android/volley/ResponseDelivery.java create mode 100644 src/main/java/com/android/volley/RetryPolicy.java create mode 100644 src/main/java/com/android/volley/ServerError.java create mode 100644 src/main/java/com/android/volley/TimeoutError.java create mode 100644 src/main/java/com/android/volley/VolleyError.java create mode 100644 src/main/java/com/android/volley/VolleyLog.java create mode 100644 src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java create mode 100644 src/main/java/com/android/volley/toolbox/Authenticator.java create mode 100644 src/main/java/com/android/volley/toolbox/BasicNetwork.java create mode 100644 src/main/java/com/android/volley/toolbox/ByteArrayPool.java create mode 100644 src/main/java/com/android/volley/toolbox/ClearCacheRequest.java create mode 100644 src/main/java/com/android/volley/toolbox/DiskBasedCache.java create mode 100644 src/main/java/com/android/volley/toolbox/HttpClientStack.java create mode 100644 src/main/java/com/android/volley/toolbox/HttpHeaderParser.java create mode 100644 src/main/java/com/android/volley/toolbox/HttpStack.java create mode 100644 src/main/java/com/android/volley/toolbox/HurlStack.java create mode 100644 src/main/java/com/android/volley/toolbox/ImageLoader.java create mode 100644 src/main/java/com/android/volley/toolbox/ImageRequest.java create mode 100644 src/main/java/com/android/volley/toolbox/JsonArrayRequest.java create mode 100644 src/main/java/com/android/volley/toolbox/JsonObjectRequest.java create mode 100644 src/main/java/com/android/volley/toolbox/JsonRequest.java create mode 100644 src/main/java/com/android/volley/toolbox/NetworkImageView.java create mode 100644 src/main/java/com/android/volley/toolbox/NoCache.java create mode 100644 src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java create mode 100644 src/main/java/com/android/volley/toolbox/RequestFuture.java create mode 100644 src/main/java/com/android/volley/toolbox/StringRequest.java create mode 100644 src/main/java/com/android/volley/toolbox/Volley.java (limited to 'src/main/java/com/android') 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 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> mCacheQueue; + + /** The queue of requests going out to the network. */ + private final BlockingQueue> 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> cacheQueue, BlockingQueue> 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> 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> 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 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 headers, + boolean notModified) { + this(statusCode, data, headers, notModified, 0); + } + + public NetworkResponse(byte[] data) { + this(HttpStatus.SC_OK, data, Collections.emptyMap(), false, 0); + } + + public NetworkResponse(byte[] data, Map 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 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 The type of parsed response this request expects. + */ +public abstract class Request implements Comparable> { + + /** + * 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). + * + *

Also dumps all events from this request's event log; for debugging.

+ */ + 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 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. + * + *

Note that only one of getPostParams() and getPostBody() can return a non-null + * value.

+ * @throws AuthFailureError In the event of auth failure + * + * @deprecated Use {@link #getParams()} instead. + */ + @Deprecated + protected Map getPostParams() throws AuthFailureError { + return getParams(); + } + + /** + * Returns which encoding should be used when converting POST parameters returned by + * {@link #getPostParams()} into a raw POST body. + * + *

This controls both encodings: + *

    + *
  1. The string encoding used when converting parameter names and values into bytes prior + * to URL encoding them.
  2. + *
  3. The string encoding used when converting the URL encoded parameters into a raw + * byte array.
  4. + *
+ * + * @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 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. + * + *

Note that you can directly override {@link #getBody()} for custom data.

+ * + * @throws AuthFailureError in the event of auth failure + */ + protected Map 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. + * + *

This controls both encodings: + *

    + *
  1. The string encoding used when converting parameter names and values into bytes prior + * to URL encoding them.
  2. + *
  3. The string encoding used when converting the URL encoded parameters into a raw + * byte array.
  4. + *
+ */ + 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. + * + *

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 params = getParams(); + if (params != null && params.size() > 0) { + return encodeParameters(params, getParamsEncoding()); + } + return null; + } + + /** + * Converts params into an application/x-www-form-urlencoded encoded string. + */ + private byte[] encodeParameters(Map params, String paramsEncoding) { + StringBuilder encodedParams = new StringBuilder(); + try { + for (Map.Entry 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 parseNetworkResponse(NetworkResponse response); + + /** + * Subclasses can override this method to parse 'networkError' and return a more specific error. + * + *

The default implementation just returns the passed 'networkError'.

+ * + * @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 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. + * + *
    + *
  • containsKey(cacheKey) indicates that there is a request in flight for the given cache + * key.
  • + *
  • get(cacheKey) returns waiting requests for the given cache key. The in flight request + * is not contained in that list. Is null if no requests are staged.
  • + *
+ */ + private final Map>> mWaitingRequests = + new HashMap>>(); + + /** + * 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> mCurrentRequests = new HashSet>(); + + /** The cache triage queue. */ + private final PriorityBlockingQueue> mCacheQueue = + new PriorityBlockingQueue>(); + + /** The queue of requests that are actually going out to the network. */ + private final PriorityBlockingQueue> mNetworkQueue = + new PriorityBlockingQueue>(); + + /** 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 Request add(Request 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> stagedRequests = mWaitingRequests.get(cacheKey); + if (stagedRequests == null) { + stagedRequests = new LinkedList>(); + } + 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. + * + *

Releases waiting requests for request.getCacheKey() if + * request.shouldCache().

+ */ + 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> 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 Parsed type of this response + */ +public class Response { + + /** Callback interface for delivering parsed responses. */ + public interface Listener { + /** 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 Response success(T result, Cache.Entry cacheEntry) { + return new Response(result, cacheEntry); + } + + /** + * Returns a failed response containing the given error code and an optional + * localized message displayed to the user. + */ + public static Response error(VolleyError error) { + return new Response(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 errorCode != OK. */ + 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. + *
+ * Enable the log property for your tag before starting your app: + *
+ * {@code adb shell setprop log.tag.<tag>} + */ + 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 = ""; + // 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 mMarkers = new ArrayList(); + 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 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 responseHeaders = Collections.emptyMap(); + try { + // Gather headers. + Map headers = new HashMap(); + 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 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. + */ + protected static Map convertHeaders(Header[] headers) { + Map result = new TreeMap(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 byte[] 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. + *

+ * A good candidate user for this class is something like an I/O system that uses large temporary + * byte[] 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. + *

+ * 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. + *

+ * 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. + *

+ * 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 mBuffersByLastUse = new LinkedList(); + private List mBuffersBySize = new ArrayList(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 BUF_COMPARATOR = new Comparator() { + @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 { + 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 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 mEntries = + new LinkedHashMap(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> iterator = mEntries.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry 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 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 map, OutputStream os) throws IOException { + if (map != null) { + writeInt(os, map.size()); + for (Map.Entry entry : map.entrySet()) { + writeString(os, entry.getKey()); + writeString(os, entry.getValue()); + } + } else { + writeInt(os, 0); + } + } + + static Map readStringStringMap(InputStream is) throws IOException { + int size = readInt(is); + Map result = (size == 0) + ? Collections.emptyMap() + : new HashMap(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 headers) { + for (String key : headers.keySet()) { + httpRequest.setHeader(key, headers.get(key)); + } + } + + @SuppressWarnings("unused") + private static List getPostParameterPairs(Map postParams) { + List result = new ArrayList(postParams.size()); + for (String key : postParams.keySet()) { + result.add(new BasicNameValuePair(key, postParams.get(key))); + } + return result; + } + + @Override + public HttpResponse performRequest(Request request, Map 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 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. + * + *

Overwrite in subclasses to augment the request.

+ */ + 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 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 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. + * + *

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().

+ * + * @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 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 additionalHeaders) + throws IOException, AuthFailureError { + String url = request.getUrl(); + HashMap map = new HashMap(); + 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> 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 connection. + */ + 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 mInFlightRequests = + new HashMap(); + + /** HashMap of the currently pending responses (waiting to be delivered). */ + private final HashMap mBatchedResponses = + new HashMap(); + + /** Handler to the main thread. */ + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + /** Runnable for in-flight response delivery. */ + private Runnable mRunnable; + + /** + * Simple cache adapter interface. If provided to the ImageLoader, it + * will be used as an L1 cache before dispatch to Volley. Implementations + * must not block. Implementation with an LruCache is recommended. + */ + public interface ImageCache { + public Bitmap getBitmap(String url); + public void putBitmap(String url, Bitmap bitmap); + } + + /** + * Constructs a new ImageLoader. + * @param queue The RequestQueue to use for making image requests. + * @param imageCache The cache to use as an L1 cache. + */ + public ImageLoader(RequestQueue queue, ImageCache imageCache) { + mRequestQueue = queue; + mCache = imageCache; + } + + /** + * The default implementation of ImageListener which handles basic functionality + * of showing a default image until the network response is received, at which point + * it will switch to either the actual image or the error image. + * @param imageView The imageView that the listener is associated with. + * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist. + * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist. + */ + public static ImageListener getImageListener(final ImageView view, + final int defaultImageResId, final int errorImageResId) { + return new ImageListener() { + @Override + public void onErrorResponse(VolleyError error) { + if (errorImageResId != 0) { + view.setImageResource(errorImageResId); + } + } + + @Override + public void onResponse(ImageContainer response, boolean isImmediate) { + if (response.getBitmap() != null) { + view.setImageBitmap(response.getBitmap()); + } else if (defaultImageResId != 0) { + view.setImageResource(defaultImageResId); + } + } + }; + } + + /** + * Interface for the response handlers on image requests. + * + * The call flow is this: + * 1. Upon being attached to a request, onResponse(response, true) will + * be invoked to reflect any cached data that was already available. If the + * data was available, response.getBitmap() will be non-null. + * + * 2. After a network response returns, only one of the following cases will happen: + * - onResponse(response, false) will be called if the image was loaded. + * or + * - onErrorResponse will be called if there was an error loading the image. + */ + public interface ImageListener extends ErrorListener { + /** + * Listens for non-error changes to the loading of the image request. + * + * @param response Holds all information pertaining to the request, as well + * as the bitmap (if it is loaded). + * @param isImmediate True if this was called during ImageLoader.get() variants. + * This can be used to differentiate between a cached image loading and a network + * image loading in order to, for example, run an animation to fade in network loaded + * images. + */ + public void onResponse(ImageContainer response, boolean isImmediate); + } + + /** + * Checks if the item is available in the cache. + * @param requestUrl The url of the remote image + * @param maxWidth The maximum width of the returned image. + * @param maxHeight The maximum height of the returned image. + * @return True if the item exists in cache, false otherwise. + */ + public boolean isCached(String requestUrl, int maxWidth, int maxHeight) { + throwIfNotOnMainThread(); + + String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight); + return mCache.getBitmap(cacheKey) != null; + } + + /** + * Returns an ImageContainer for the requested URL. + * + * The ImageContainer will contain either the specified default bitmap or the loaded bitmap. + * If the default was returned, the {@link ImageLoader} will be invoked when the + * request is fulfilled. + * + * @param requestUrl The URL of the image to be loaded. + * @param defaultImage Optional default image to return until the actual image is loaded. + */ + public ImageContainer get(String requestUrl, final ImageListener listener) { + return get(requestUrl, listener, 0, 0); + } + + /** + * Issues a bitmap request with the given URL if that image is not available + * in the cache, and returns a bitmap container that contains all of the data + * relating to the request (as well as the default image if the requested + * image is not available). + * @param requestUrl The url of the remote image + * @param imageListener The listener to call when the remote image is loaded + * @param maxWidth The maximum width of the returned image. + * @param maxHeight The maximum height of the returned image. + * @return A container object that contains all of the properties of the request, as well as + * the currently available image (default if remote is not loaded). + */ + public ImageContainer get(String requestUrl, ImageListener imageListener, + int maxWidth, int maxHeight) { + // only fulfill requests that were initiated from the main thread. + throwIfNotOnMainThread(); + + final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight); + + // Try to look up the request in the cache of remote images. + Bitmap cachedBitmap = mCache.getBitmap(cacheKey); + if (cachedBitmap != null) { + // Return the cached bitmap. + ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null); + imageListener.onResponse(container, true); + return container; + } + + // The bitmap did not exist in the cache, fetch it! + ImageContainer imageContainer = + new ImageContainer(null, requestUrl, cacheKey, imageListener); + + // Update the caller to let them know that they should use the default bitmap. + imageListener.onResponse(imageContainer, true); + + // Check to see if a request is already in-flight. + BatchedImageRequest request = mInFlightRequests.get(cacheKey); + if (request != null) { + // If it is, add this request to the list of listeners. + request.addContainer(imageContainer); + return imageContainer; + } + + // The request is not already in flight. Send the new request to the network and + // track it. + Request newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, cacheKey); + + mRequestQueue.add(newRequest); + mInFlightRequests.put(cacheKey, + new BatchedImageRequest(newRequest, imageContainer)); + return imageContainer; + } + + protected Request makeImageRequest(String requestUrl, int maxWidth, int maxHeight, final String cacheKey) { + return new ImageRequest(requestUrl, new Listener() { + @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 mContainers = new LinkedList(); + + /** + * 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 { + /** 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 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 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 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 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 { + + /** + * 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 listener, ErrorListener errorListener) { + super(Method.GET, url, null, listener, errorListener); + } + + @Override + protected Response 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 { + + /** + * 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 listener, ErrorListener errorListener) { + super(method, url, (jsonRequest == null) ? null : jsonRequest.toString(), listener, + errorListener); + } + + /** + * Constructor which defaults to GET if jsonRequest is + * null, POST otherwise. + * + * @see #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener) + */ + public JsonObjectRequest(String url, JSONObject jsonRequest, Listener listener, + ErrorListener errorListener) { + this(jsonRequest == null ? Method.GET : Method.POST, url, jsonRequest, + listener, errorListener); + } + + @Override + protected Response 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 JSON type of response expected + */ +public abstract class JsonRequest extends Request { + /** 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 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 listener, + ErrorListener errorListener) { + this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener); + } + + public JsonRequest(int method, String url, String requestBody, Listener listener, + ErrorListener errorListener) { + super(method, url, errorListener); + mListener = listener; + mRequestBody = requestBody; + } + + @Override + protected void deliverResponse(T response) { + mListener.onResponse(response); + } + + @Override + abstract protected Response 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: + *
+ * RequestFuture<JSONObject> 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
+ * }
+ * 
+ * + * @param The type of parsed response this future expects. + */ +public class RequestFuture implements Future, Response.Listener, + Response.ErrorListener { + private Request mRequest; + private boolean mResultReceived = false; + private T mResult; + private VolleyError mException; + + public static RequestFuture newFuture() { + return new RequestFuture(); + } + + 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 { + private final Listener 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 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 listener, ErrorListener errorListener) { + this(Method.GET, url, listener, errorListener); + } + + @Override + protected void deliverResponse(String response) { + mListener.onResponse(response); + } + + @Override + protected Response 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); + } +} -- cgit v1.2.3