From ac8d9a1d940de5b5335c82c56dd42dec728dd443 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 25 Sep 2017 10:46:08 -0700 Subject: Import of Volley from GitHub to AOSP. - 5fb28f66748df4f89b49c1493693d1f65c6bb23e Fix NPEs/compile errors introduced by header changes. (#96) by Jeff Davidson - e16a426da3bcffb1a8de1700ddbe69201540d93c Fix RequestQueueIntegrationTest flakiness. (#94) by Jeff Davidson - 96feb3b09a6301b9573212027af21db7be5c8be1 Improve Volley header handling. (#91) by Jeff Davidson - a794c075a62ddf438178b2e15cc79ab5502588fb For waiting requests, use ArrayList instead of LinkedList... by Ulrike Hager - 787ef0cc731c28b528f744dea64bd5429f99e153 Specify .aar packaging in SNAPSHOT POM. by Jeff Davidson - b2bb59ab2ff08f4d468303071f050ce938349379 Fix soft TTL for duplicate requests (#73) by Ulrike Hager - b33a53f1793b475842f91a0fe166749118afcfc0 Deprecate Volley's use of Apache HTTP. (#75) by Jeff Davidson GitOrigin-RevId: 5fb28f66748df4f89b49c1493693d1f65c6bb23e Change-Id: Ia04d2967e9923d2430a04f2474aa69ce82e114ce --- src/main/java/com/android/volley/Cache.java | 16 +- .../java/com/android/volley/CacheDispatcher.java | 151 ++++++++++++++-- src/main/java/com/android/volley/Header.java | 60 +++++++ .../java/com/android/volley/NetworkDispatcher.java | 9 +- .../java/com/android/volley/NetworkResponse.java | 114 ++++++++++-- src/main/java/com/android/volley/Request.java | 54 ++++++ src/main/java/com/android/volley/RequestQueue.java | 62 +------ .../android/volley/toolbox/AdaptedHttpStack.java | 81 +++++++++ .../com/android/volley/toolbox/BaseHttpStack.java | 93 ++++++++++ .../com/android/volley/toolbox/BasicNetwork.java | 194 +++++++++++++++------ .../com/android/volley/toolbox/DiskBasedCache.java | 60 ++++--- .../android/volley/toolbox/HttpClientStack.java | 4 + .../android/volley/toolbox/HttpHeaderParser.java | 66 ++++++- .../com/android/volley/toolbox/HttpResponse.java | 81 +++++++++ .../java/com/android/volley/toolbox/HttpStack.java | 5 + .../java/com/android/volley/toolbox/HurlStack.java | 72 ++++---- .../java/com/android/volley/toolbox/Volley.java | 58 ++++-- 17 files changed, 950 insertions(+), 230 deletions(-) create mode 100644 src/main/java/com/android/volley/Header.java create mode 100644 src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java create mode 100644 src/main/java/com/android/volley/toolbox/BaseHttpStack.java create mode 100644 src/main/java/com/android/volley/toolbox/HttpResponse.java (limited to 'src/main/java/com') diff --git a/src/main/java/com/android/volley/Cache.java b/src/main/java/com/android/volley/Cache.java index 8482c22..fd7eea1 100644 --- a/src/main/java/com/android/volley/Cache.java +++ b/src/main/java/com/android/volley/Cache.java @@ -17,6 +17,7 @@ package com.android.volley; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -83,9 +84,22 @@ public interface Cache { /** Soft TTL for this record. */ public long softTtl; - /** Immutable response headers as received from server; must be non-null. */ + /** + * Response headers as received from server; must be non-null. Should not be mutated + * directly. + * + *

Note that if the server returns two headers with the same (case-insensitive) name, + * this map will only contain the one of them. {@link #allResponseHeaders} may contain all + * headers if the {@link Cache} implementation supports it. + */ public Map responseHeaders = Collections.emptyMap(); + /** + * All response headers. May be null depending on the {@link Cache} implementation. Should + * not be mutated directly. + */ + public List

allResponseHeaders; + /** True if the entry is expired. */ public boolean isExpired() { return this.ttl < System.currentTimeMillis(); diff --git a/src/main/java/com/android/volley/CacheDispatcher.java b/src/main/java/com/android/volley/CacheDispatcher.java index 1e7dfc4..51dfd9c 100644 --- a/src/main/java/com/android/volley/CacheDispatcher.java +++ b/src/main/java/com/android/volley/CacheDispatcher.java @@ -18,6 +18,10 @@ package com.android.volley; import android.os.Process; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.BlockingQueue; /** @@ -48,6 +52,9 @@ public class CacheDispatcher extends Thread { /** Used for telling us to die. */ private volatile boolean mQuit = false; + /** Manage list of waiting requests and de-duplicate requests with same cache key. */ + private final WaitingRequestManager mWaitingRequestManager; + /** * Creates a new cache triage dispatcher thread. You must call {@link #start()} * in order to begin processing. @@ -64,6 +71,7 @@ public class CacheDispatcher extends Thread { mNetworkQueue = networkQueue; mCache = cache; mDelivery = delivery; + mWaitingRequestManager = new WaitingRequestManager(this); } /** @@ -101,7 +109,9 @@ public class CacheDispatcher extends Thread { if (entry == null) { request.addMarker("cache-miss"); // Cache miss; send off to the network dispatcher. - mNetworkQueue.put(request); + if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { + mNetworkQueue.put(request); + } continue; } @@ -109,7 +119,9 @@ public class CacheDispatcher extends Thread { if (entry.isExpired()) { request.addMarker("cache-hit-expired"); request.setCacheEntry(entry); - mNetworkQueue.put(request); + if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { + mNetworkQueue.put(request); + } continue; } @@ -128,22 +140,28 @@ public class CacheDispatcher extends Thread { // 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. + if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { + // 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) { + // Restore the interrupted status + Thread.currentThread().interrupt(); + } } - } - }); + }); + } else { + // request has been added to list of waiting requests + // to receive the network response from the first request once it returns. + mDelivery.postResponse(request, response); + } } } catch (InterruptedException e) { @@ -154,4 +172,109 @@ public class CacheDispatcher extends Thread { } } } + + private static class WaitingRequestManager implements Request.NetworkRequestCompleteListener { + + /** + * 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<>(); + + private final CacheDispatcher mCacheDispatcher; + + WaitingRequestManager(CacheDispatcher cacheDispatcher) { + mCacheDispatcher = cacheDispatcher; + } + + /** Request received a valid response that can be used by other waiting requests. */ + @Override + public void onResponseReceived(Request request, Response response) { + if (response.cacheEntry == null || response.cacheEntry.isExpired()) { + onNoUsableResponseReceived(request); + return; + } + String cacheKey = request.getCacheKey(); + List> waitingRequests; + synchronized (this) { + 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. + for (Request waiting : waitingRequests) { + mCacheDispatcher.mDelivery.postResponse(waiting, response); + } + } + } + + /** No valid response received from network, release waiting requests. */ + @Override + public synchronized void onNoUsableResponseReceived(Request request) { + String cacheKey = request.getCacheKey(); + List> waitingRequests = mWaitingRequests.remove(cacheKey); + if (waitingRequests != null && !waitingRequests.isEmpty()) { + if (VolleyLog.DEBUG) { + VolleyLog.v("%d waiting requests for cacheKey=%s; resend to network", + waitingRequests.size(), cacheKey); + } + Request nextInLine = waitingRequests.remove(0); + mWaitingRequests.put(cacheKey, waitingRequests); + try { + mCacheDispatcher.mNetworkQueue.put(nextInLine); + } catch (InterruptedException iex) { + VolleyLog.e("Couldn't add request to queue. %s", iex.toString()); + // Restore the interrupted status of the calling thread (i.e. NetworkDispatcher) + Thread.currentThread().interrupt(); + // Quit the current CacheDispatcher thread. + mCacheDispatcher.quit(); + } + } + } + + /** + * For cacheable requests, if a request for the same cache key is already in flight, + * add it to a queue to wait for that in-flight request to finish. + * @return whether the request was queued. If false, we should continue issuing the request + * over the network. If true, we should put the request on hold to be processed when + * the in-flight request finishes. + */ + private synchronized boolean maybeAddToWaitingRequests(Request request) { + String cacheKey = request.getCacheKey(); + // Insert request into stage if there's already a request with the same cache key + // in flight. + if (mWaitingRequests.containsKey(cacheKey)) { + // There is already a request in flight. Queue up. + List> stagedRequests = mWaitingRequests.get(cacheKey); + if (stagedRequests == null) { + stagedRequests = new ArrayList>(); + } + request.addMarker("waiting-for-response"); + stagedRequests.add(request); + mWaitingRequests.put(cacheKey, stagedRequests); + if (VolleyLog.DEBUG) { + VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey); + } + return true; + } else { + // Insert 'null' queue for this cacheKey, indicating there is now a request in + // flight. + mWaitingRequests.put(cacheKey, null); + request.setNetworkRequestCompleteListener(this); + if (VolleyLog.DEBUG) { + VolleyLog.d("new request, sending to network %s", cacheKey); + } + return false; + } + } + } } diff --git a/src/main/java/com/android/volley/Header.java b/src/main/java/com/android/volley/Header.java new file mode 100644 index 0000000..ac8aa11 --- /dev/null +++ b/src/main/java/com/android/volley/Header.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2017 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.text.TextUtils; + +/** An HTTP header. */ +public final class Header { + private final String mName; + private final String mValue; + + public Header(String name, String value) { + mName = name; + mValue = value; + } + + public final String getName() { + return mName; + } + + public final String getValue() { + return mValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Header header = (Header) o; + + return TextUtils.equals(mName, header.mName) + && TextUtils.equals(mValue, header.mValue); + } + + @Override + public int hashCode() { + int result = mName.hashCode(); + result = 31 * result + mValue.hashCode(); + return result; + } + + @Override + public String toString() { + return "Header[name=" + mName + ",value=" + mValue + "]"; + } +} diff --git a/src/main/java/com/android/volley/NetworkDispatcher.java b/src/main/java/com/android/volley/NetworkDispatcher.java index beb7861..0384429 100644 --- a/src/main/java/com/android/volley/NetworkDispatcher.java +++ b/src/main/java/com/android/volley/NetworkDispatcher.java @@ -33,6 +33,7 @@ import java.util.concurrent.BlockingQueue; * 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. */ @@ -54,8 +55,7 @@ public class NetworkDispatcher extends Thread { * @param delivery Delivery interface to use for posting responses */ public NetworkDispatcher(BlockingQueue> queue, - Network network, Cache cache, - ResponseDelivery delivery) { + Network network, Cache cache, ResponseDelivery delivery) { mQueue = queue; mNetwork = network; mCache = cache; @@ -103,6 +103,7 @@ public class NetworkDispatcher extends Thread { // network request. if (request.isCanceled()) { request.finish("network-discard-cancelled"); + request.notifyListenerResponseNotUsable(); continue; } @@ -116,6 +117,7 @@ public class NetworkDispatcher extends Thread { // we're done -- don't deliver a second identical response. if (networkResponse.notModified && request.hasHadResponseDelivered()) { request.finish("not-modified"); + request.notifyListenerResponseNotUsable(); continue; } @@ -133,14 +135,17 @@ public class NetworkDispatcher extends Thread { // Post the response back. request.markDelivered(); mDelivery.postResponse(request, response); + request.notifyListenerResponseReceived(response); } catch (VolleyError volleyError) { volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); parseAndDeliverNetworkError(request, volleyError); + request.notifyListenerResponseNotUsable(); } 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); + request.notifyListenerResponseNotUsable(); } } } diff --git a/src/main/java/com/android/volley/NetworkResponse.java b/src/main/java/com/android/volley/NetworkResponse.java index a787fa7..f0fded3 100644 --- a/src/main/java/com/android/volley/NetworkResponse.java +++ b/src/main/java/com/android/volley/NetworkResponse.java @@ -16,15 +16,18 @@ package com.android.volley; -import org.apache.http.HttpStatus; - +import java.net.HttpURLConnection; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.TreeMap; /** * Data and headers returned from {@link Network#performRequest(Request)}. */ public class NetworkResponse { + /** * Creates a new network response. * @param statusCode the HTTP status code @@ -32,27 +35,78 @@ public class NetworkResponse { * @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 + * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor + * cannot handle server responses containing multiple headers with the same name. + * This constructor may be removed in a future release of Volley. */ + @Deprecated 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; + this(statusCode, data, headers, toAllHeaderList(headers), notModified, networkTimeMs); + } + + /** + * Creates a new network response. + * @param statusCode the HTTP status code + * @param data Response body + * @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 + * @param allHeaders All headers returned with this response, or null for none + */ + public NetworkResponse(int statusCode, byte[] data, boolean notModified, long networkTimeMs, + List
allHeaders) { + this(statusCode, data, toHeaderMap(allHeaders), allHeaders, notModified, networkTimeMs); } + /** + * 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 + * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor + * cannot handle server responses containing multiple headers with the same name. + * This constructor may be removed in a future release of Volley. + */ + @Deprecated public NetworkResponse(int statusCode, byte[] data, Map headers, boolean notModified) { this(statusCode, data, headers, notModified, 0); } + /** + * Creates a new network response for an OK response with no headers. + * @param data Response body + */ public NetworkResponse(byte[] data) { - this(HttpStatus.SC_OK, data, Collections.emptyMap(), false, 0); + this(HttpURLConnection.HTTP_OK, data, false, 0, Collections.
emptyList()); } + /** + * Creates a new network response for an OK response. + * @param data Response body + * @param headers Headers returned with this response, or null for none + * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor + * cannot handle server responses containing multiple headers with the same name. + * This constructor may be removed in a future release of Volley. + */ + @Deprecated public NetworkResponse(byte[] data, Map headers) { - this(HttpStatus.SC_OK, data, headers, false, 0); + this(HttpURLConnection.HTTP_OK, data, headers, false, 0); + } + + private NetworkResponse(int statusCode, byte[] data, Map headers, + List
allHeaders, boolean notModified, long networkTimeMs) { + this.statusCode = statusCode; + this.data = data; + this.headers = headers; + if (allHeaders == null) { + this.allHeaders = null; + } else { + this.allHeaders = Collections.unmodifiableList(allHeaders); + } + this.notModified = notModified; + this.networkTimeMs = networkTimeMs; } /** The HTTP status code. */ @@ -61,13 +115,53 @@ public class NetworkResponse { /** Raw data from this response. */ public final byte[] data; - /** Response headers. */ + /** + * Response headers. + * + *

This map is case-insensitive. It should not be mutated directly. + * + *

Note that if the server returns two headers with the same (case-insensitive) name, this + * map will only contain the last one. Use {@link #allHeaders} to inspect all headers returned + * by the server. + */ public final Map headers; + /** All response headers. Must not be mutated directly. */ + public final List

allHeaders; + /** True if the server returned a 304 (Not Modified). */ public final boolean notModified; /** Network roundtrip time in milliseconds. */ public final long networkTimeMs; + + private static Map toHeaderMap(List
allHeaders) { + if (allHeaders == null) { + return null; + } + if (allHeaders.isEmpty()) { + return Collections.emptyMap(); + } + Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + // Later elements in the list take precedence. + for (Header header : allHeaders) { + headers.put(header.getName(), header.getValue()); + } + return headers; + } + + private static List
toAllHeaderList(Map headers) { + if (headers == null) { + return null; + } + if (headers.isEmpty()) { + return Collections.emptyList(); + } + List
allHeaders = new ArrayList<>(headers.size()); + for (Map.Entry header : headers.entrySet()) { + allHeaders.add(new Header(header.getKey(), header.getValue())); + } + return allHeaders; + } } diff --git a/src/main/java/com/android/volley/Request.java b/src/main/java/com/android/volley/Request.java index 8200f6e..a98277e 100644 --- a/src/main/java/com/android/volley/Request.java +++ b/src/main/java/com/android/volley/Request.java @@ -56,6 +56,18 @@ public abstract class Request implements Comparable> { int PATCH = 7; } + /** + * Callback to notify when the network request returns. + */ + /* package */ interface NetworkRequestCompleteListener { + + /** Callback when a network response has been received. */ + void onResponseReceived(Request request, Response response); + + /** Callback when request returns from network without valid response. */ + void onNoUsableResponseReceived(Request request); + } + /** An event log tracing the lifetime of this request; for debugging. */ private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null; @@ -105,6 +117,12 @@ public abstract class Request implements Comparable> { /** An opaque token tagging this request; used for bulk cancellation. */ private Object mTag; + /** Listener that will be notified when a response has been delivered. */ + private NetworkRequestCompleteListener mRequestCompleteListener; + + /** Object to guard access to mRequestCompleteListener. */ + private final Object mLock = new Object(); + /** * 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 @@ -584,6 +602,42 @@ public abstract class Request implements Comparable> { } } + /** + * {@link NetworkRequestCompleteListener} that will receive callbacks when the request + * returns from the network. + */ + /* package */ void setNetworkRequestCompleteListener( + NetworkRequestCompleteListener requestCompleteListener) { + synchronized (mLock) { + mRequestCompleteListener = requestCompleteListener; + } + } + + /** + * Notify NetworkRequestCompleteListener that a valid response has been received + * which can be used for other, waiting requests. + * @param response received from the network + */ + /* package */ void notifyListenerResponseReceived(Response response) { + synchronized (mLock) { + if (mRequestCompleteListener != null) { + mRequestCompleteListener.onResponseReceived(this, response); + } + } + } + + /** + * Notify NetworkRequestCompleteListener that the network request did not result in + * a response which can be used for other, waiting requests. + */ + /* package */ void notifyListenerResponseNotUsable() { + synchronized (mLock) { + if (mRequestCompleteListener != null) { + mRequestCompleteListener.onNoUsableResponseReceived(this); + } + } + } + /** * Our comparator sorts from high to low priority, and secondarily by * sequence number to provide FIFO ordering. diff --git a/src/main/java/com/android/volley/RequestQueue.java b/src/main/java/com/android/volley/RequestQueue.java index 0f2e756..45679a5 100644 --- a/src/main/java/com/android/volley/RequestQueue.java +++ b/src/main/java/com/android/volley/RequestQueue.java @@ -20,12 +20,8 @@ import android.os.Handler; import android.os.Looper; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; -import java.util.Map; -import java.util.Queue; import java.util.Set; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; @@ -48,19 +44,6 @@ public class RequestQueue { /** Used for generating monotonically-increasing sequence numbers for requests. */ private final 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 @@ -240,37 +223,13 @@ public class RequestQueue { 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; - } - } + 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. @@ -283,21 +242,6 @@ public class RequestQueue { } } - 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); - } - } - } } public void addRequestFinishedListener(RequestFinishedListener listener) { diff --git a/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java b/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java new file mode 100644 index 0000000..e5dc62b --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2017 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.Header; +import com.android.volley.Request; + +import org.apache.http.conn.ConnectTimeoutException; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * {@link BaseHttpStack} implementation wrapping a {@link HttpStack}. + * + *

{@link BasicNetwork} uses this if it is provided a {@link HttpStack} at construction time, + * allowing it to have one implementation based atop {@link BaseHttpStack}. + */ +@SuppressWarnings("deprecation") +class AdaptedHttpStack extends BaseHttpStack { + + private final HttpStack mHttpStack; + + AdaptedHttpStack(HttpStack httpStack) { + mHttpStack = httpStack; + } + + @Override + public HttpResponse executeRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + org.apache.http.HttpResponse apacheResp; + try { + apacheResp = mHttpStack.performRequest(request, additionalHeaders); + } catch (ConnectTimeoutException e) { + // BasicNetwork won't know that this exception should be retried like a timeout, since + // it's an Apache-specific error, so wrap it in a standard timeout exception. + throw new SocketTimeoutException(e.getMessage()); + } + + int statusCode = apacheResp.getStatusLine().getStatusCode(); + + org.apache.http.Header[] headers = apacheResp.getAllHeaders(); + List

headerList = new ArrayList<>(headers.length); + for (org.apache.http.Header header : headers) { + headerList.add(new Header(header.getName(), header.getValue())); + } + + if (apacheResp.getEntity() == null) { + return new HttpResponse(statusCode, headerList); + } + + long contentLength = apacheResp.getEntity().getContentLength(); + if ((int) contentLength != contentLength) { + throw new IOException("Response too large: " + contentLength); + } + + return new HttpResponse( + statusCode, + headerList, + (int) apacheResp.getEntity().getContentLength(), + apacheResp.getEntity().getContent()); + } +} diff --git a/src/main/java/com/android/volley/toolbox/BaseHttpStack.java b/src/main/java/com/android/volley/toolbox/BaseHttpStack.java new file mode 100644 index 0000000..257f75c --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/BaseHttpStack.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2017 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.Header; +import com.android.volley.Request; + +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.IOException; +import java.io.InputStream; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** An HTTP stack abstraction. */ +@SuppressWarnings("deprecation") // for HttpStack +public abstract class BaseHttpStack implements 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 {@link HttpResponse} + * @throws SocketTimeoutException if the request times out + * @throws IOException if another I/O error occurs during the request + * @throws AuthFailureError if an authentication failure occurs during the request + */ + public abstract HttpResponse executeRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError; + + /** + * @deprecated use {@link #executeRequest} instead to avoid a dependency on the deprecated + * Apache HTTP library. Nothing in Volley's own source calls this method. However, since + * {@link BasicNetwork#mHttpStack} is exposed to subclasses, we provide this implementation in + * case legacy client apps are dependent on that field. This method may be removed in a future + * release of Volley. + */ + @Deprecated + @Override + public final org.apache.http.HttpResponse performRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + HttpResponse response = executeRequest(request, additionalHeaders); + + ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1); + StatusLine statusLine = new BasicStatusLine( + protocolVersion, response.getStatusCode(), "" /* reasonPhrase */); + BasicHttpResponse apacheResponse = new BasicHttpResponse(statusLine); + + List headers = new ArrayList<>(); + for (Header header : response.getHeaders()) { + headers.add(new BasicHeader(header.getName(), header.getValue())); + } + apacheResponse.setHeaders(headers.toArray(new org.apache.http.Header[headers.size()])); + + InputStream responseStream = response.getContent(); + if (responseStream != null) { + BasicHttpEntity entity = new BasicHttpEntity(); + entity.setContent(responseStream); + entity.setContentLength(response.getContentLength()); + apacheResponse.setEntity(entity); + } + + return apacheResponse; + } +} diff --git a/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/src/main/java/com/android/volley/toolbox/BasicNetwork.java index 96fb66e..5330733 100644 --- a/src/main/java/com/android/volley/toolbox/BasicNetwork.java +++ b/src/main/java/com/android/volley/toolbox/BasicNetwork.java @@ -22,6 +22,7 @@ import com.android.volley.AuthFailureError; import com.android.volley.Cache; import com.android.volley.Cache.Entry; import com.android.volley.ClientError; +import com.android.volley.Header; import com.android.volley.Network; import com.android.volley.NetworkError; import com.android.volley.NetworkResponse; @@ -33,23 +34,19 @@ 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.HttpURLConnection; import java.net.MalformedURLException; import java.net.SocketTimeoutException; +import java.util.ArrayList; import java.util.Collections; -import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; +import java.util.TreeSet; /** * A network performing Volley requests over an {@link HttpStack}. @@ -61,13 +58,23 @@ public class BasicNetwork implements Network { private static final int DEFAULT_POOL_SIZE = 4096; + /** + * @deprecated Should never have been exposed in the API. This field may be removed in a future + * release of Volley. + */ + @Deprecated protected final HttpStack mHttpStack; + private final BaseHttpStack mBaseHttpStack; + protected final ByteArrayPool mPool; /** * @param httpStack HTTP stack to be used + * @deprecated use {@link #BasicNetwork(BaseHttpStack)} instead to avoid depending on Apache + * HTTP. This method may be removed in a future release of Volley. */ + @Deprecated 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. @@ -77,8 +84,35 @@ public class BasicNetwork implements Network { /** * @param httpStack HTTP stack to be used * @param pool a buffer pool that improves GC performance in copy operations + * @deprecated use {@link #BasicNetwork(BaseHttpStack, ByteArrayPool)} instead to avoid + * depending on Apache HTTP. This method may be removed in a future release of + * Volley. */ + @Deprecated public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) { + mHttpStack = httpStack; + mBaseHttpStack = new AdaptedHttpStack(httpStack); + mPool = pool; + } + + /** + * @param httpStack HTTP stack to be used + */ + public BasicNetwork(BaseHttpStack 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(BaseHttpStack httpStack, ByteArrayPool pool) { + mBaseHttpStack = httpStack; + // Populate mHttpStack for backwards compatibility, since it is a protected field. However, + // we won't use it directly here, so clients which don't access it directly won't need to + // depend on Apache HTTP. mHttpStack = httpStack; mPool = pool; } @@ -89,39 +123,33 @@ public class BasicNetwork implements Network { while (true) { HttpResponse httpResponse = null; byte[] responseContents = null; - Map responseHeaders = Collections.emptyMap(); + List

responseHeaders = Collections.emptyList(); try { // Gather headers. - Map headers = new HashMap(); - addCacheHeaders(headers, request.getCacheEntry()); - httpResponse = mHttpStack.performRequest(request, headers); - StatusLine statusLine = httpResponse.getStatusLine(); - int statusCode = statusLine.getStatusCode(); + Map additionalRequestHeaders = + getCacheHeaders(request.getCacheEntry()); + httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders); + int statusCode = httpResponse.getStatusCode(); - responseHeaders = convertHeaders(httpResponse.getAllHeaders()); + responseHeaders = httpResponse.getHeaders(); // Handle cache validation. - if (statusCode == HttpStatus.SC_NOT_MODIFIED) { - + if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) { Entry entry = request.getCacheEntry(); if (entry == null) { - return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null, - responseHeaders, true, - SystemClock.elapsedRealtime() - requestStart); + return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED, null, true, + SystemClock.elapsedRealtime() - requestStart, responseHeaders); } - - // 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); + // Combine cached and response headers so the response will be complete. + List
combinedHeaders = combineHeaders(responseHeaders, entry); + return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED, entry.data, + true, SystemClock.elapsedRealtime() - requestStart, combinedHeaders); } // Some responses such as 204s do not have content. We must check. - if (httpResponse.getEntity() != null) { - responseContents = entityToBytes(httpResponse.getEntity()); + InputStream inputStream = httpResponse.getContent(); + if (inputStream != null) { + responseContents = + inputStreamToBytes(inputStream, httpResponse.getContentLength()); } else { // Add 0 byte response as a way of honestly representing a // no-content request. @@ -130,33 +158,31 @@ public class BasicNetwork implements Network { // if the request is slow, log it. long requestLifetime = SystemClock.elapsedRealtime() - requestStart; - logSlowRequests(requestLifetime, request, responseContents, statusLine); + logSlowRequests(requestLifetime, request, responseContents, statusCode); if (statusCode < 200 || statusCode > 299) { throw new IOException(); } - return new NetworkResponse(statusCode, responseContents, responseHeaders, false, - SystemClock.elapsedRealtime() - requestStart); + return new NetworkResponse(statusCode, responseContents, false, + SystemClock.elapsedRealtime() - requestStart, responseHeaders); } 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; if (httpResponse != null) { - statusCode = httpResponse.getStatusLine().getStatusCode(); + statusCode = httpResponse.getStatusCode(); } else { throw new NoConnectionError(e); } VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); NetworkResponse networkResponse; if (responseContents != null) { - networkResponse = new NetworkResponse(statusCode, responseContents, - responseHeaders, false, SystemClock.elapsedRealtime() - requestStart); - if (statusCode == HttpStatus.SC_UNAUTHORIZED || - statusCode == HttpStatus.SC_FORBIDDEN) { + networkResponse = new NetworkResponse(statusCode, responseContents, false, + SystemClock.elapsedRealtime() - requestStart, responseHeaders); + if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED || + statusCode == HttpURLConnection.HTTP_FORBIDDEN) { attemptRetryOnException("auth", request, new AuthFailureError(networkResponse)); } else if (statusCode >= 400 && statusCode <= 499) { @@ -184,12 +210,12 @@ public class BasicNetwork implements Network { * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. */ private void logSlowRequests(long requestLifetime, Request request, - byte[] responseContents, StatusLine statusLine) { + byte[] responseContents, int statusCode) { 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()); + statusCode, request.getRetryPolicy().getCurrentRetryCount()); } } @@ -213,20 +239,24 @@ public class BasicNetwork implements Network { request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout)); } - private void addCacheHeaders(Map headers, Cache.Entry entry) { + private Map getCacheHeaders(Cache.Entry entry) { // If there's no cache entry, we're done. if (entry == null) { - return; + return Collections.emptyMap(); } + Map headers = new HashMap<>(); + if (entry.etag != null) { headers.put("If-None-Match", entry.etag); } if (entry.lastModified > 0) { - Date refTime = new Date(entry.lastModified); - headers.put("If-Modified-Since", DateUtils.formatDate(refTime)); + headers.put("If-Modified-Since", + HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified)); } + + return headers; } protected void logError(String what, String url, long start) { @@ -234,13 +264,13 @@ public class BasicNetwork implements Network { 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 { + /** Reads the contents of an InputStream into a byte[]. */ + private byte[] inputStreamToBytes(InputStream in, int contentLength) + throws IOException, ServerError { PoolingByteArrayOutputStream bytes = - new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength()); + new PoolingByteArrayOutputStream(mPool, contentLength); byte[] buffer = null; try { - InputStream in = entity.getContent(); if (in == null) { throw new ServerError(); } @@ -253,11 +283,13 @@ public class BasicNetwork implements Network { } finally { try { // Close the InputStream and release the resources by "consuming the content". - entity.consumeContent(); + if (in != null) { + in.close(); + } } catch (IOException e) { - // This can happen if there was an exception above that left the entity in + // This can happen if there was an exception above that left the stream in // an invalid state. - VolleyLog.v("Error occurred when calling consumingContent"); + VolleyLog.v("Error occurred when closing InputStream"); } mPool.returnBuf(buffer); bytes.close(); @@ -266,12 +298,62 @@ public class BasicNetwork implements Network { /** * Converts Headers[] to Map<String, String>. + * + * @deprecated Should never have been exposed in the API. This method may be removed in a future + * release of Volley. */ + @Deprecated protected static Map convertHeaders(Header[] headers) { - Map result = new TreeMap(String.CASE_INSENSITIVE_ORDER); + 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; } + + /** + * Combine cache headers with network response headers for an HTTP 304 response. + * + *

An 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. See also: + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 + * + * @param responseHeaders Headers from the network response. + * @param entry The cached response. + * @return The combined list of headers. + */ + private static List

combineHeaders(List
responseHeaders, Entry entry) { + // First, create a case-insensitive set of header names from the network + // response. + Set headerNamesFromNetworkResponse = + new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + if (!responseHeaders.isEmpty()) { + for (Header header : responseHeaders) { + headerNamesFromNetworkResponse.add(header.getName()); + } + } + + // Second, add headers from the cache entry to the network response as long as + // they didn't appear in the network response, which should take precedence. + List
combinedHeaders = new ArrayList<>(responseHeaders); + if (entry.allResponseHeaders != null) { + if (!entry.allResponseHeaders.isEmpty()) { + for (Header header : entry.allResponseHeaders) { + if (!headerNamesFromNetworkResponse.contains(header.getName())) { + combinedHeaders.add(header); + } + } + } + } else { + // Legacy caches only have entry.responseHeaders. + if (!entry.responseHeaders.isEmpty()) { + for (Map.Entry header : entry.responseHeaders.entrySet()) { + if (!headerNamesFromNetworkResponse.contains(header.getKey())) { + combinedHeaders.add(new Header(header.getKey(), header.getValue())); + } + } + } + } + return combinedHeaders; + } } diff --git a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java index 0e65183..a6cd960 100644 --- a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java +++ b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java @@ -20,6 +20,7 @@ import android.os.SystemClock; import android.text.TextUtils; import com.android.volley.Cache; +import com.android.volley.Header; import com.android.volley.VolleyLog; import java.io.BufferedInputStream; @@ -34,15 +35,18 @@ import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; 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. + * + *

This cache supports the {@link Entry#allResponseHeaders} headers field. */ public class DiskBasedCache implements Cache { @@ -379,30 +383,40 @@ public class DiskBasedCache implements Cache { final long softTtl; /** Headers from the response resulting in this cache entry. */ - final Map responseHeaders; + final List

allResponseHeaders; private CacheHeader(String key, String etag, long serverDate, long lastModified, long ttl, - long softTtl, Map responseHeaders) { + long softTtl, List
allResponseHeaders) { this.key = key; this.etag = ("".equals(etag)) ? null : etag; this.serverDate = serverDate; this.lastModified = lastModified; this.ttl = ttl; this.softTtl = softTtl; - this.responseHeaders = responseHeaders; + this.allResponseHeaders = allResponseHeaders; } /** - * Instantiates a new CacheHeader object + * Instantiates a new CacheHeader object. * @param key The key that identifies the cache entry * @param entry The cache entry. */ CacheHeader(String key, Entry entry) { this(key, entry.etag, entry.serverDate, entry.lastModified, entry.ttl, entry.softTtl, - entry.responseHeaders); + getAllResponseHeaders(entry)); size = entry.data.length; } + private static List
getAllResponseHeaders(Entry entry) { + // If the entry contains all the response headers, use that field directly. + if (entry.allResponseHeaders != null) { + return entry.allResponseHeaders; + } + + // Legacy fallback - copy headers from the map. + return HttpHeaderParser.toAllHeaderList(entry.responseHeaders); + } + /** * Reads the header from a CountingInputStream and returns a CacheHeader object. * @param is The InputStream to read from. @@ -420,9 +434,9 @@ public class DiskBasedCache implements Cache { long lastModified = readLong(is); long ttl = readLong(is); long softTtl = readLong(is); - Map responseHeaders = readStringStringMap(is); + List
allResponseHeaders = readHeaderList(is); return new CacheHeader( - key, etag, serverDate, lastModified, ttl, softTtl, responseHeaders); + key, etag, serverDate, lastModified, ttl, softTtl, allResponseHeaders); } /** @@ -436,11 +450,11 @@ public class DiskBasedCache implements Cache { e.lastModified = lastModified; e.ttl = ttl; e.softTtl = softTtl; - e.responseHeaders = responseHeaders; + e.responseHeaders = HttpHeaderParser.toHeaderMap(allResponseHeaders); + e.allResponseHeaders = Collections.unmodifiableList(allResponseHeaders); return e; } - /** * Writes the contents of this CacheHeader to the specified OutputStream. */ @@ -453,7 +467,7 @@ public class DiskBasedCache implements Cache { writeLong(os, lastModified); writeLong(os, ttl); writeLong(os, softTtl); - writeStringStringMap(responseHeaders, os); + writeHeaderList(allResponseHeaders, os); os.flush(); return true; } catch (IOException e) { @@ -574,27 +588,27 @@ public class DiskBasedCache implements Cache { 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()); + static void writeHeaderList(List
headers, OutputStream os) throws IOException { + if (headers != null) { + writeInt(os, headers.size()); + for (Header header : headers) { + writeString(os, header.getName()); + writeString(os, header.getValue()); } } else { writeInt(os, 0); } } - static Map readStringStringMap(CountingInputStream cis) throws IOException { + static List
readHeaderList(CountingInputStream cis) throws IOException { int size = readInt(cis); - Map result = (size == 0) - ? Collections.emptyMap() - : new HashMap(size); + List
result = (size == 0) + ? Collections.
emptyList() + : new ArrayList
(size); for (int i = 0; i < size; i++) { - String key = readString(cis).intern(); + String name = readString(cis).intern(); String value = readString(cis).intern(); - result.put(key, value); + result.add(new Header(name, 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 index 377110e..023ee21 100644 --- a/src/main/java/com/android/volley/toolbox/HttpClientStack.java +++ b/src/main/java/com/android/volley/toolbox/HttpClientStack.java @@ -46,7 +46,11 @@ import java.util.Map; /** * An HttpStack that performs request over an {@link HttpClient}. + * + * @deprecated The Apache HTTP library on Android is deprecated. Use {@link HurlStack} or another + * {@link BaseHttpStack} implementation. */ +@Deprecated public class HttpClientStack implements HttpStack { protected final HttpClient mClient; diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java index f53063c..211c329 100644 --- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java +++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java @@ -17,19 +17,31 @@ package com.android.volley.toolbox; import com.android.volley.Cache; +import com.android.volley.Header; 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 com.android.volley.VolleyLog; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.TimeZone; +import java.util.TreeMap; /** * Utility methods for parsing HTTP headers. */ public class HttpHeaderParser { + static final String HEADER_CONTENT_TYPE = "Content-Type"; + + private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1"; + + private static final String RFC1123_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; + /** * Extracts a {@link com.android.volley.Cache.Entry} from a {@link NetworkResponse}. * @@ -116,6 +128,7 @@ public class HttpHeaderParser { entry.serverDate = serverDate; entry.lastModified = lastModified; entry.responseHeaders = headers; + entry.allResponseHeaders = response.allHeaders; return entry; } @@ -126,13 +139,26 @@ public class HttpHeaderParser { 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) { + return newRfc1123Formatter().parse(dateStr).getTime(); + } catch (ParseException e) { // Date in invalid format, fallback to 0 + VolleyLog.e(e, "Unable to parse dateStr: %s, falling back to 0", dateStr); return 0; } } + /** Format an epoch date in RFC1123 format. */ + static String formatEpochAsRfc1123(long epoch) { + return newRfc1123Formatter().format(new Date(epoch)); + } + + private static SimpleDateFormat newRfc1123Formatter() { + SimpleDateFormat formatter = + new SimpleDateFormat(RFC1123_FORMAT, Locale.US); + formatter.setTimeZone(TimeZone.getTimeZone("GMT")); + return formatter; + } + /** * Retrieve a charset from headers * @@ -142,7 +168,7 @@ public class HttpHeaderParser { * or the defaultCharset if none can be found. */ public static String parseCharset(Map headers, String defaultCharset) { - String contentType = headers.get(HTTP.CONTENT_TYPE); + String contentType = headers.get(HEADER_CONTENT_TYPE); if (contentType != null) { String[] params = contentType.split(";"); for (int i = 1; i < params.length; i++) { @@ -163,6 +189,28 @@ public class HttpHeaderParser { * or the HTTP default (ISO-8859-1) if none can be found. */ public static String parseCharset(Map headers) { - return parseCharset(headers, HTTP.DEFAULT_CONTENT_CHARSET); + return parseCharset(headers, DEFAULT_CONTENT_CHARSET); + } + + // Note - these are copied from NetworkResponse to avoid making them public (as needed to access + // them from the .toolbox package), which would mean they'd become part of the Volley API. + // TODO: Consider obfuscating official releases so we can share utility methods between Volley + // and Toolbox without making them public APIs. + + static Map toHeaderMap(List
allHeaders) { + Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + // Later elements in the list take precedence. + for (Header header : allHeaders) { + headers.put(header.getName(), header.getValue()); + } + return headers; + } + + static List
toAllHeaderList(Map headers) { + List
allHeaders = new ArrayList<>(headers.size()); + for (Map.Entry header : headers.entrySet()) { + allHeaders.add(new Header(header.getKey(), header.getValue())); + } + return allHeaders; } } diff --git a/src/main/java/com/android/volley/toolbox/HttpResponse.java b/src/main/java/com/android/volley/toolbox/HttpResponse.java new file mode 100644 index 0000000..db719bc --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/HttpResponse.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2017 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.Header; + +import java.io.InputStream; +import java.util.Collections; +import java.util.List; + +/** A response from an HTTP server. */ +public final class HttpResponse { + + private final int mStatusCode; + private final List
mHeaders; + private final int mContentLength; + private final InputStream mContent; + + /** + * Construct a new HttpResponse for an empty response body. + * + * @param statusCode the HTTP status code of the response + * @param headers the response headers + */ + public HttpResponse(int statusCode, List
headers) { + this(statusCode, headers, -1 /* contentLength */, null /* content */); + } + + /** + * Construct a new HttpResponse. + * + * @param statusCode the HTTP status code of the response + * @param headers the response headers + * @param contentLength the length of the response content. Ignored if there is no content. + * @param content an {@link InputStream} of the response content. May be null to indicate that + * the response has no content. + */ + public HttpResponse( + int statusCode, List
headers, int contentLength, InputStream content) { + mStatusCode = statusCode; + mHeaders = headers; + mContentLength = contentLength; + mContent = content; + } + + /** Returns the HTTP status code of the response. */ + public final int getStatusCode() { + return mStatusCode; + } + + /** Returns the response headers. Must not be mutated directly. */ + public final List
getHeaders() { + return Collections.unmodifiableList(mHeaders); + } + + /** Returns the length of the content. Only valid if {@link #getContent} is non-null. */ + public final int getContentLength() { + return mContentLength; + } + + /** + * Returns an {@link InputStream} of the response content. May be null to indicate that the + * response has no content. + */ + public final InputStream getContent() { + return mContent; + } +} diff --git a/src/main/java/com/android/volley/toolbox/HttpStack.java b/src/main/java/com/android/volley/toolbox/HttpStack.java index 06f6017..5d34b44 100644 --- a/src/main/java/com/android/volley/toolbox/HttpStack.java +++ b/src/main/java/com/android/volley/toolbox/HttpStack.java @@ -26,7 +26,12 @@ import java.util.Map; /** * An HTTP stack abstraction. + * + * @deprecated This interface should be avoided as it depends on the deprecated Apache HTTP library. + * Use {@link BaseHttpStack} to avoid this dependency. This class may be removed in a future + * release of Volley. */ +@Deprecated public interface HttpStack { /** * Performs an HTTP request with the given parameters. diff --git a/src/main/java/com/android/volley/toolbox/HurlStack.java b/src/main/java/com/android/volley/toolbox/HurlStack.java index 66f441d..a975a71 100644 --- a/src/main/java/com/android/volley/toolbox/HurlStack.java +++ b/src/main/java/com/android/volley/toolbox/HurlStack.java @@ -17,29 +17,19 @@ package com.android.volley.toolbox; import com.android.volley.AuthFailureError; +import com.android.volley.Header; 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.HttpStatus; -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.ArrayList; 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; @@ -47,9 +37,9 @@ import javax.net.ssl.SSLSocketFactory; /** * An {@link HttpStack} based on {@link HttpURLConnection}. */ -public class HurlStack implements HttpStack { +public class HurlStack extends BaseHttpStack { - private static final String HEADER_CONTENT_TYPE = "Content-Type"; + private static final int HTTP_CONTINUE = 100; /** * An interface for transforming URLs before use. @@ -86,10 +76,10 @@ public class HurlStack implements HttpStack { } @Override - public HttpResponse performRequest(Request request, Map additionalHeaders) + public HttpResponse executeRequest(Request request, Map additionalHeaders) throws IOException, AuthFailureError { String url = request.getUrl(); - HashMap map = new HashMap(); + HashMap map = new HashMap<>(); map.putAll(request.getHeaders()); map.putAll(additionalHeaders); if (mUrlRewriter != null) { @@ -106,26 +96,34 @@ public class HurlStack implements HttpStack { } 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); - if (hasResponseBody(request.getMethod(), responseStatus.getStatusCode())) { - response.setEntity(entityFromConnection(connection)); + + if (!hasResponseBody(request.getMethod(), responseCode)) { + return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields())); } - for (Entry> header : connection.getHeaderFields().entrySet()) { - if (header.getKey() != null) { - Header h = new BasicHeader(header.getKey(), header.getValue().get(0)); - response.addHeader(h); + + return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields()), + connection.getContentLength(), inputStreamFromConnection(connection)); + } + + // VisibleForTesting + static List
convertHeaders(Map> responseHeaders) { + List
headerList = new ArrayList<>(responseHeaders.size()); + for (Map.Entry> entry : responseHeaders.entrySet()) { + // HttpUrlConnection includes the status line as a header with a null key; omit it here + // since it's not really a header and the rest of Volley assumes non-null keys. + if (entry.getKey() != null) { + for (String value : entry.getValue()) { + headerList.add(new Header(entry.getKey(), value)); + } } } - return response; + return headerList; } /** @@ -137,29 +135,24 @@ public class HurlStack implements HttpStack { */ private static boolean hasResponseBody(int requestMethod, int responseCode) { return requestMethod != Request.Method.HEAD - && !(HttpStatus.SC_CONTINUE <= responseCode && responseCode < HttpStatus.SC_OK) - && responseCode != HttpStatus.SC_NO_CONTENT - && responseCode != HttpStatus.SC_NOT_MODIFIED; + && !(HTTP_CONTINUE <= responseCode && responseCode < HttpURLConnection.HTTP_OK) + && responseCode != HttpURLConnection.HTTP_NO_CONTENT + && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED; } /** - * Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}. + * Initializes an {@link InputStream} 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(); + private static InputStream inputStreamFromConnection(HttpURLConnection connection) { 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; + return inputStream; } /** @@ -261,7 +254,8 @@ public class HurlStack implements HttpStack { // since this is handled by HttpURLConnection using the size of the prepared // output stream. connection.setDoOutput(true); - connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType()); + connection.addRequestProperty( + HttpHeaderParser.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/Volley.java b/src/main/java/com/android/volley/toolbox/Volley.java index 0e04e87..6ec08b1 100644 --- a/src/main/java/com/android/volley/toolbox/Volley.java +++ b/src/main/java/com/android/volley/toolbox/Volley.java @@ -36,35 +36,59 @@ public class 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. + * @param stack A {@link BaseHttpStack} 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) { - } - + public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) { + BasicNetwork network; if (stack == null) { if (Build.VERSION.SDK_INT >= 9) { - stack = new HurlStack(); + network = new BasicNetwork(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)); + // At some point in the future we'll move our minSdkVersion past Froyo and can + // delete this fallback (along with all Apache HTTP code). + String userAgent = "volley/0"; + try { + String packageName = context.getPackageName(); + PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); + userAgent = packageName + "/" + info.versionCode; + } catch (NameNotFoundException e) { + } + + network = new BasicNetwork( + new HttpClientStack(AndroidHttpClient.newInstance(userAgent))); } + } else { + network = new BasicNetwork(stack); } - Network network = new BasicNetwork(stack); + return newRequestQueue(context, network); + } + + /** + * 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. + * @deprecated Use {@link #newRequestQueue(Context, BaseHttpStack)} instead to avoid depending + * on Apache HTTP. This method may be removed in a future release of Volley. + */ + @Deprecated + @SuppressWarnings("deprecation") + public static RequestQueue newRequestQueue(Context context, HttpStack stack) { + if (stack == null) { + return newRequestQueue(context, (BaseHttpStack) null); + } + return newRequestQueue(context, new BasicNetwork(stack)); + } + private static RequestQueue newRequestQueue(Context context, Network network) { + File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR); RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network); queue.start(); - return queue; } @@ -75,6 +99,6 @@ public class Volley { * @return A started {@link RequestQueue} instance. */ public static RequestQueue newRequestQueue(Context context) { - return newRequestQueue(context, null); + return newRequestQueue(context, (BaseHttpStack) null); } } -- cgit v1.2.3