aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandroid-build-team Robot <android-build-team-robot@google.com>2017-09-26 08:08:56 +0000
committerandroid-build-team Robot <android-build-team-robot@google.com>2017-09-26 08:08:56 +0000
commitb04d0fc3fb4ac5b516d2588f1f487722fdaec054 (patch)
treefb13707e6dfdd4c3a4cbee1f66749bf6066b0dad
parent46e3caed6d37c133e1e22e9db70dfd2e1d404c52 (diff)
parent3b20577713cf9d63af606ff369440bd730d3b6b4 (diff)
downloadvolley-b04d0fc3fb4ac5b516d2588f1f487722fdaec054.tar.gz
release-request-37e26775-0485-4a3d-a06c-026b3663c922-for-git_pi-release-4359872 snap-temp-L83600000106066915
Change-Id: Ib34cc76c4539cc5c72eece3a803353bb4527781c
-rw-r--r--bintray.gradle5
-rw-r--r--src/main/java/com/android/volley/Cache.java16
-rw-r--r--src/main/java/com/android/volley/CacheDispatcher.java151
-rw-r--r--src/main/java/com/android/volley/Header.java60
-rw-r--r--src/main/java/com/android/volley/NetworkDispatcher.java9
-rw-r--r--src/main/java/com/android/volley/NetworkResponse.java114
-rw-r--r--src/main/java/com/android/volley/Request.java54
-rw-r--r--src/main/java/com/android/volley/RequestQueue.java62
-rw-r--r--src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java81
-rw-r--r--src/main/java/com/android/volley/toolbox/BaseHttpStack.java93
-rw-r--r--src/main/java/com/android/volley/toolbox/BasicNetwork.java194
-rw-r--r--src/main/java/com/android/volley/toolbox/DiskBasedCache.java60
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpClientStack.java4
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpHeaderParser.java66
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpResponse.java81
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpStack.java5
-rw-r--r--src/main/java/com/android/volley/toolbox/HurlStack.java72
-rw-r--r--src/main/java/com/android/volley/toolbox/Volley.java58
-rw-r--r--src/test/java/com/android/volley/CacheDispatcherTest.java56
-rw-r--r--src/test/java/com/android/volley/NetworkResponseTest.java63
-rw-r--r--src/test/java/com/android/volley/RequestQueueIntegrationTest.java114
-rw-r--r--src/test/java/com/android/volley/mock/MockHttpClient.java114
-rw-r--r--src/test/java/com/android/volley/mock/MockHttpStack.java11
-rw-r--r--src/test/java/com/android/volley/mock/MockResponseDelivery.java3
-rw-r--r--src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java135
-rw-r--r--src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java108
-rw-r--r--src/test/java/com/android/volley/toolbox/BasicNetworkTest.java137
-rw-r--r--src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java46
-rw-r--r--src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java24
-rw-r--r--src/test/java/com/android/volley/toolbox/HurlStackTest.java23
30 files changed, 1534 insertions, 485 deletions
diff --git a/bintray.gradle b/bintray.gradle
index 8914c1d..df0e49b 100644
--- a/bintray.gradle
+++ b/bintray.gradle
@@ -55,6 +55,9 @@ publishing {
groupId 'com.android.volley'
artifactId 'volley'
version project.ext.version
+ pom {
+ packaging 'aar'
+ }
// Release AAR, Sources, and JavaDoc
artifact "$buildDir/outputs/aar/volley-release.aar"
@@ -84,4 +87,4 @@ artifactory {
resolve {
repoKey = 'jcenter'
}
-} \ No newline at end of file
+}
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.
+ *
+ * <p>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<String, String> responseHeaders = Collections.emptyMap();
+ /**
+ * All response headers. May be null depending on the {@link Cache} implementation. Should
+ * not be mutated directly.
+ */
+ public List<Header> 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.
+ *
+ * <ul>
+ * <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache
+ * key.</li>
+ * <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request
+ * is <em>not</em> contained in that list. Is null if no requests are staged.</li>
+ * </ul>
+ */
+ private final Map<String, List<Request<?>>> 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<Request<?>> 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<Request<?>> 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<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
+ if (stagedRequests == null) {
+ stagedRequests = new ArrayList<Request<?>>();
+ }
+ 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<Request<?>> 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<Request<?>> 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<String, String> 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<Header> 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<String, String> 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.<String, String>emptyMap(), false, 0);
+ this(HttpURLConnection.HTTP_OK, data, false, 0, Collections.<Header>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<String, String> headers) {
- this(HttpStatus.SC_OK, data, headers, false, 0);
+ this(HttpURLConnection.HTTP_OK, data, headers, false, 0);
+ }
+
+ private NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,
+ List<Header> 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.
+ *
+ * <p>This map is case-insensitive. It should not be mutated directly.
+ *
+ * <p>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<String, String> headers;
+ /** All response headers. Must not be mutated directly. */
+ public final List<Header> 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<String, String> toHeaderMap(List<Header> allHeaders) {
+ if (allHeaders == null) {
+ return null;
+ }
+ if (allHeaders.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map<String, String> 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<Header> toAllHeaderList(Map<String, String> headers) {
+ if (headers == null) {
+ return null;
+ }
+ if (headers.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List<Header> allHeaders = new ArrayList<>(headers.size());
+ for (Map.Entry<String, String> 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<T> implements Comparable<Request<T>> {
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<T> implements Comparable<Request<T>> {
/** 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
@@ -585,6 +603,42 @@ public abstract class Request<T> implements Comparable<Request<T>> {
}
/**
+ * {@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;
@@ -49,19 +45,6 @@ public class RequestQueue {
private final AtomicInteger mSequenceGenerator = new AtomicInteger();
/**
- * Staging area for requests that already have a duplicate request in flight.
- *
- * <ul>
- * <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache
- * key.</li>
- * <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request
- * is <em>not</em> contained in that list. Is null if no requests are staged.</li>
- * </ul>
- */
- private final Map<String, Queue<Request<?>>> mWaitingRequests =
- new HashMap<>();
-
- /**
* 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.
@@ -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<Request<?>> 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.
- *
- * <p>Releases waiting requests for <code>request.getCacheKey()</code> if
- * <code>request.shouldCache()</code>.</p>
*/
<T> void finish(Request<T> 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<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
- if (waitingRequests != null) {
- if (VolleyLog.DEBUG) {
- VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
- waitingRequests.size(), cacheKey);
- }
- // Process all queued up requests. They won't be considered as in flight, but
- // that's not a problem as the cache has been primed by 'request'.
- mCacheQueue.addAll(waitingRequests);
- }
- }
- }
}
public <T> void addRequestFinishedListener(RequestFinishedListener<T> 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}.
+ *
+ * <p>{@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<String, String> 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<Header> 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.
+ *
+ * <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
+ * and the Content-Type header is set to request.getPostBodyContentType().
+ *
+ * @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<String, String> 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<String, String> 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<org.apache.http.Header> 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,9 +84,36 @@ 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<String, String> responseHeaders = Collections.emptyMap();
+ List<Header> responseHeaders = Collections.emptyList();
try {
// Gather headers.
- Map<String, String> headers = new HashMap<String, String>();
- addCacheHeaders(headers, request.getCacheEntry());
- httpResponse = mHttpStack.performRequest(request, headers);
- StatusLine statusLine = httpResponse.getStatusLine();
- int statusCode = statusLine.getStatusCode();
+ Map<String, String> 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<Header> 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<String, String> headers, Cache.Entry entry) {
+ private Map<String, String> getCacheHeaders(Cache.Entry entry) {
// If there's no cache entry, we're done.
if (entry == null) {
- return;
+ return Collections.emptyMap();
}
+ Map<String, String> 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&lt;String, String&gt;.
+ *
+ * @deprecated Should never have been exposed in the API. This method may be removed in a future
+ * release of Volley.
*/
+ @Deprecated
protected static Map<String, String> convertHeaders(Header[] headers) {
- Map<String, String> result = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
+ Map<String, String> 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.
+ *
+ * <p>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<Header> combineHeaders(List<Header> responseHeaders, Entry entry) {
+ // First, create a case-insensitive set of header names from the network
+ // response.
+ Set<String> 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<Header> 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<String, String> 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.
+ *
+ * <p>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<String, String> responseHeaders;
+ final List<Header> allResponseHeaders;
private CacheHeader(String key, String etag, long serverDate, long lastModified, long ttl,
- long softTtl, Map<String, String> responseHeaders) {
+ long softTtl, List<Header> 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<Header> 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<String, String> responseHeaders = readStringStringMap(is);
+ List<Header> 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<String, String> map, OutputStream os) throws IOException {
- if (map != null) {
- writeInt(os, map.size());
- for (Map.Entry<String, String> entry : map.entrySet()) {
- writeString(os, entry.getKey());
- writeString(os, entry.getValue());
+ static void writeHeaderList(List<Header> 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<String, String> readStringStringMap(CountingInputStream cis) throws IOException {
+ static List<Header> readHeaderList(CountingInputStream cis) throws IOException {
int size = readInt(cis);
- Map<String, String> result = (size == 0)
- ? Collections.<String, String>emptyMap()
- : new HashMap<String, String>(size);
+ List<Header> result = (size == 0)
+ ? Collections.<Header>emptyList()
+ : new ArrayList<Header>(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<String, String> 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<String, String> 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<String, String> toHeaderMap(List<Header> allHeaders) {
+ Map<String, String> 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<Header> toAllHeaderList(Map<String, String> headers) {
+ List<Header> allHeaders = new ArrayList<>(headers.size());
+ for (Map.Entry<String, String> 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<Header> 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<Header> 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<Header> 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<Header> 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<String, String> additionalHeaders)
+ public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
String url = request.getUrl();
- HashMap<String, String> map = new HashMap<String, String>();
+ HashMap<String, String> 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<String, List<String>> header : connection.getHeaderFields().entrySet()) {
- if (header.getKey() != null) {
- Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
- response.addHeader(h);
+
+ return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields()),
+ connection.getContentLength(), inputStreamFromConnection(connection));
+ }
+
+ // VisibleForTesting
+ static List<Header> convertHeaders(Map<String, List<String>> responseHeaders) {
+ List<Header> headerList = new ArrayList<>(responseHeaders.size());
+ for (Map.Entry<String, List<String>> 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 <code>connection</code>.
*/
- 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);
}
}
diff --git a/src/test/java/com/android/volley/CacheDispatcherTest.java b/src/test/java/com/android/volley/CacheDispatcherTest.java
index 42bdda0..54886f8 100644
--- a/src/test/java/com/android/volley/CacheDispatcherTest.java
+++ b/src/test/java/com/android/volley/CacheDispatcherTest.java
@@ -112,4 +112,60 @@ public class CacheDispatcherTest {
Request request = mNetworkQueue.take();
assertSame(entry, request.getCacheEntry());
}
+
+ @Test public void duplicateCacheMiss() throws Exception {
+ MockRequest secondRequest = new MockRequest();
+ mRequest.setSequence(1);
+ secondRequest.setSequence(2);
+ mCacheQueue.add(mRequest);
+ mCacheQueue.add(secondRequest);
+ mCacheQueue.waitUntilEmpty(TIMEOUT_MILLIS);
+ assertTrue(mNetworkQueue.size() == 1);
+ assertFalse(mDelivery.postResponse_called);
+ }
+
+ @Test public void duplicateSoftExpiredCacheHit_failedRequest() throws Exception {
+ Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
+ mCache.setEntryToReturn(entry);
+
+ MockRequest secondRequest = new MockRequest();
+ mRequest.setSequence(1);
+ secondRequest.setSequence(2);
+
+ mCacheQueue.add(mRequest);
+ mCacheQueue.add(secondRequest);
+ mCacheQueue.waitUntilEmpty(TIMEOUT_MILLIS);
+
+ assertTrue(mNetworkQueue.size() == 1);
+ assertTrue(mDelivery.postResponse_calledNtimes == 2);
+
+ Request request = mNetworkQueue.take();
+ request.notifyListenerResponseNotUsable();
+ // Second request should now be in network queue.
+ assertTrue(mNetworkQueue.size() == 1);
+ request = mNetworkQueue.take();
+ assertTrue(request.equals(secondRequest));
+ }
+
+ @Test public void duplicateSoftExpiredCacheHit_successfulRequest() throws Exception {
+ Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
+ mCache.setEntryToReturn(entry);
+
+ MockRequest secondRequest = new MockRequest();
+ mRequest.setSequence(1);
+ secondRequest.setSequence(2);
+
+ mCacheQueue.add(mRequest);
+ mCacheQueue.add(secondRequest);
+ mCacheQueue.waitUntilEmpty(TIMEOUT_MILLIS);
+
+ assertTrue(mNetworkQueue.size() == 1);
+ assertTrue(mDelivery.postResponse_calledNtimes == 2);
+
+ Request request = mNetworkQueue.take();
+ request.notifyListenerResponseReceived(Response.success(null, entry));
+ // Second request should have delivered response.
+ assertTrue(mNetworkQueue.size() == 0);
+ assertTrue(mDelivery.postResponse_calledNtimes == 3);
+ }
}
diff --git a/src/test/java/com/android/volley/NetworkResponseTest.java b/src/test/java/com/android/volley/NetworkResponseTest.java
new file mode 100644
index 0000000..be34143
--- /dev/null
+++ b/src/test/java/com/android/volley/NetworkResponseTest.java
@@ -0,0 +1,63 @@
+package com.android.volley;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+@RunWith(RobolectricTestRunner.class)
+public class NetworkResponseTest {
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void mapToList() {
+ Map<String, String> headers = new HashMap<>();
+ headers.put("key1", "value1");
+ headers.put("key2", "value2");
+
+ NetworkResponse resp = new NetworkResponse(200, null, headers, false);
+
+ List<Header> expectedHeaders = new ArrayList<>();
+ expectedHeaders.add(new Header("key1", "value1"));
+ expectedHeaders.add(new Header("key2", "value2"));
+
+ assertThat(expectedHeaders,
+ containsInAnyOrder(resp.allHeaders.toArray(new Header[resp.allHeaders.size()])));
+ }
+
+ @Test
+ public void listToMap() {
+ List<Header> headers = new ArrayList<>();
+ headers.add(new Header("key1", "value1"));
+ // Later values should be preferred.
+ headers.add(new Header("key2", "ignoredvalue"));
+ headers.add(new Header("key2", "value2"));
+
+ NetworkResponse resp = new NetworkResponse(200, null, false, 0L, headers);
+
+ Map<String, String> expectedHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ expectedHeaders.put("key1", "value1");
+ expectedHeaders.put("key2", "value2");
+
+ assertEquals(expectedHeaders, resp.headers);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void nullValuesDontCrash() {
+ new NetworkResponse(null);
+ new NetworkResponse(null, null);
+ new NetworkResponse(200, null, null, false);
+ new NetworkResponse(200, null, null, false, 0L);
+ new NetworkResponse(200, null, false, 0L, null);
+ }
+}
diff --git a/src/test/java/com/android/volley/RequestQueueIntegrationTest.java b/src/test/java/com/android/volley/RequestQueueIntegrationTest.java
index a73435c..304a1ab 100644
--- a/src/test/java/com/android/volley/RequestQueueIntegrationTest.java
+++ b/src/test/java/com/android/volley/RequestQueueIntegrationTest.java
@@ -22,29 +22,30 @@ import com.android.volley.mock.MockRequest;
import com.android.volley.mock.ShadowSystemClock;
import com.android.volley.toolbox.NoCache;
import com.android.volley.utils.ImmediateResponseDelivery;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Random;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
/**
- * Integration tests for {@link RequestQueue}, that verify its behavior in conjunction with real dispatcher, queues and
- * Requests. Network is mocked out
+ * Integration tests for {@link RequestQueue} that verify its behavior in conjunction with real
+ * dispatcher, queues and Requests.
+ *
+ * <p>The Network is mocked out.
*/
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowSystemClock.class})
@@ -52,6 +53,8 @@ public class RequestQueueIntegrationTest {
private ResponseDelivery mDelivery;
@Mock private Network mMockNetwork;
+ @Mock private RequestFinishedListener<byte[]> mMockListener;
+ @Mock private RequestFinishedListener<byte[]> mMockListener2;
@Before public void setUp() throws Exception {
mDelivery = new ImmediateResponseDelivery();
@@ -59,9 +62,10 @@ public class RequestQueueIntegrationTest {
}
@Test public void add_requestProcessedInCorrectOrder() throws Exception {
- // Enqueue 2 requests with different cache keys, and different priorities. The second, higher priority request
- // takes 20ms.
- // Assert that first request is only handled after the first one has been parsed and delivered.
+ // Enqueue 2 requests with different cache keys, and different priorities. The second,
+ // higher priority request takes 20ms.
+ // Assert that the first request is only handled after the first one has been parsed and
+ // delivered.
MockRequest lowerPriorityReq = new MockRequest();
MockRequest higherPriorityReq = new MockRequest();
lowerPriorityReq.setCacheKey("1");
@@ -69,7 +73,6 @@ public class RequestQueueIntegrationTest {
lowerPriorityReq.setPriority(Priority.LOW);
higherPriorityReq.setPriority(Priority.HIGH);
- RequestFinishedListener listener = mock(RequestFinishedListener.class);
Answer<NetworkResponse> delayAnswer = new Answer<NetworkResponse>() {
@Override
public NetworkResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
@@ -77,37 +80,31 @@ public class RequestQueueIntegrationTest {
return mock(NetworkResponse.class);
}
};
- //delay only for higher request
+ // delay only for higher request
when(mMockNetwork.performRequest(higherPriorityReq)).thenAnswer(delayAnswer);
when(mMockNetwork.performRequest(lowerPriorityReq)).thenReturn(mock(NetworkResponse.class));
RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);
- queue.addRequestFinishedListener(listener);
+ queue.addRequestFinishedListener(mMockListener);
queue.add(lowerPriorityReq);
queue.add(higherPriorityReq);
queue.start();
- // you cannot do strict order verification in combination with timeouts with mockito 1.9.5 :(
- // as an alternative, first verify no requests have finished, while higherPriorityReq should be processing
- verifyNoMoreInteractions(listener);
+ InOrder inOrder = inOrder(mMockListener);
// verify higherPriorityReq goes through first
- verify(listener, timeout(100)).onRequestFinished(higherPriorityReq);
+ inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(higherPriorityReq);
// verify lowerPriorityReq goes last
- verify(listener, timeout(10)).onRequestFinished(lowerPriorityReq);
+ inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(lowerPriorityReq);
+
queue.stop();
}
- /**
- * Asserts that requests with same cache key are processed in order.
- *
- * Needs to be an integration test because relies on complex interations between various queues
- */
+ /** Asserts that requests with same cache key are processed in order. */
@Test public void add_dedupeByCacheKey() throws Exception {
// Enqueue 2 requests with the same cache key. The first request takes 20ms. Assert that the
// second request is only handled after the first one has been parsed and delivered.
- Request req1 = new MockRequest();
- Request req2 = new MockRequest();
- RequestFinishedListener listener = mock(RequestFinishedListener.class);
+ MockRequest req1 = new MockRequest();
+ MockRequest req2 = new MockRequest();
Answer<NetworkResponse> delayAnswer = new Answer<NetworkResponse>() {
@Override
public NetworkResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
@@ -120,27 +117,23 @@ public class RequestQueueIntegrationTest {
when(mMockNetwork.performRequest(req2)).thenReturn(mock(NetworkResponse.class));
RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 3, mDelivery);
- queue.addRequestFinishedListener(listener);
+ queue.addRequestFinishedListener(mMockListener);
queue.add(req1);
queue.add(req2);
queue.start();
- // you cannot do strict order verification with mockito 1.9.5 :(
- // as an alternative, first verify no requests have finished, then verify req1 goes through
- verifyNoMoreInteractions(listener);
- verify(listener, timeout(100)).onRequestFinished(req1);
- verify(listener, timeout(10)).onRequestFinished(req2);
+ InOrder inOrder = inOrder(mMockListener);
+ // verify req1 goes through first
+ inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(req1);
+ // verify req2 goes last
+ inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(req2);
+
queue.stop();
}
- /**
- * Verify RequestFinishedListeners are informed when requests are canceled
- *
- * Needs to be an integration test because relies on Request -> dispatcher -> RequestQueue interaction
- */
+ /** Verify RequestFinishedListeners are informed when requests are canceled. */
@Test public void add_requestFinishedListenerCanceled() throws Exception {
- RequestFinishedListener listener = mock(RequestFinishedListener.class);
- Request request = new MockRequest();
+ MockRequest request = new MockRequest();
Answer<NetworkResponse> delayAnswer = new Answer<NetworkResponse>() {
@Override
public NetworkResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
@@ -152,56 +145,43 @@ public class RequestQueueIntegrationTest {
when(mMockNetwork.performRequest(request)).thenAnswer(delayAnswer);
- queue.addRequestFinishedListener(listener);
+ queue.addRequestFinishedListener(mMockListener);
queue.start();
queue.add(request);
request.cancel();
- verify(listener, timeout(100)).onRequestFinished(request);
+ verify(mMockListener, timeout(10000)).onRequestFinished(request);
queue.stop();
}
- /**
- * Verify RequestFinishedListeners are informed when requests are successfully delivered
- *
- * Needs to be an integration test because relies on Request -> dispatcher -> RequestQueue interaction
- */
+ /** Verify RequestFinishedListeners are informed when requests are successfully delivered. */
@Test public void add_requestFinishedListenerSuccess() throws Exception {
- NetworkResponse response = mock(NetworkResponse.class);
- Request request = new MockRequest();
- RequestFinishedListener listener = mock(RequestFinishedListener.class);
- RequestFinishedListener listener2 = mock(RequestFinishedListener.class);
+ MockRequest request = new MockRequest();
RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);
- queue.addRequestFinishedListener(listener);
- queue.addRequestFinishedListener(listener2);
+ queue.addRequestFinishedListener(mMockListener);
+ queue.addRequestFinishedListener(mMockListener2);
queue.start();
queue.add(request);
- verify(listener, timeout(100)).onRequestFinished(request);
- verify(listener2, timeout(100)).onRequestFinished(request);
+ verify(mMockListener, timeout(10000)).onRequestFinished(request);
+ verify(mMockListener2, timeout(10000)).onRequestFinished(request);
queue.stop();
}
- /**
- * Verify RequestFinishedListeners are informed when request errors
- *
- * Needs to be an integration test because relies on Request -> dispatcher -> RequestQueue interaction
- */
+ /** Verify RequestFinishedListeners are informed when request errors. */
@Test public void add_requestFinishedListenerError() throws Exception {
- RequestFinishedListener listener = mock(RequestFinishedListener.class);
- Request request = new MockRequest();
+ MockRequest request = new MockRequest();
RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);
when(mMockNetwork.performRequest(request)).thenThrow(new VolleyError());
- queue.addRequestFinishedListener(listener);
+ queue.addRequestFinishedListener(mMockListener);
queue.start();
queue.add(request);
- verify(listener, timeout(100)).onRequestFinished(request);
+ verify(mMockListener, timeout(10000)).onRequestFinished(request);
queue.stop();
}
-
}
diff --git a/src/test/java/com/android/volley/mock/MockHttpClient.java b/src/test/java/com/android/volley/mock/MockHttpClient.java
deleted file mode 100644
index c2a36bc..0000000
--- a/src/test/java/com/android/volley/mock/MockHttpClient.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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.mock;
-
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpHost;
-import org.apache.http.HttpRequest;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.ProtocolVersion;
-import org.apache.http.StatusLine;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.ResponseHandler;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.conn.ClientConnectionManager;
-import org.apache.http.message.BasicHttpResponse;
-import org.apache.http.message.BasicStatusLine;
-import org.apache.http.params.HttpParams;
-import org.apache.http.protocol.HttpContext;
-
-
-public class MockHttpClient implements HttpClient {
- private int mStatusCode = HttpStatus.SC_OK;
- private HttpEntity mResponseEntity = null;
-
- public void setResponseData(HttpEntity entity) {
- mStatusCode = HttpStatus.SC_OK;
- mResponseEntity = entity;
- }
-
- public void setErrorCode(int statusCode) {
- if (statusCode == HttpStatus.SC_OK) {
- throw new IllegalArgumentException("statusCode cannot be 200 for an error");
- }
- mStatusCode = statusCode;
- }
-
- public HttpUriRequest requestExecuted = null;
-
- // This is the only one we actually use.
- @Override
- public HttpResponse execute(HttpUriRequest request, HttpContext context) {
- requestExecuted = request;
- StatusLine statusLine = new BasicStatusLine(
- new ProtocolVersion("HTTP", 1, 1), mStatusCode, "");
- HttpResponse response = new BasicHttpResponse(statusLine);
- response.setEntity(mResponseEntity);
-
- return response;
- }
-
-
- // Unimplemented methods ahoy
-
- @Override
- public HttpResponse execute(HttpUriRequest request) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public HttpResponse execute(HttpHost target, HttpRequest request) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public <T> T execute(HttpUriRequest arg0, ResponseHandler<? extends T> arg1) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public <T> T execute(HttpUriRequest arg0, ResponseHandler<? extends T> arg1, HttpContext arg2) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public <T> T execute(HttpHost arg0, HttpRequest arg1, ResponseHandler<? extends T> arg2) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public <T> T execute(HttpHost arg0, HttpRequest arg1, ResponseHandler<? extends T> arg2,
- HttpContext arg3) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public ClientConnectionManager getConnectionManager() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public HttpParams getParams() {
- throw new UnsupportedOperationException();
- }
-}
diff --git a/src/test/java/com/android/volley/mock/MockHttpStack.java b/src/test/java/com/android/volley/mock/MockHttpStack.java
index 91872d3..56b29f1 100644
--- a/src/test/java/com/android/volley/mock/MockHttpStack.java
+++ b/src/test/java/com/android/volley/mock/MockHttpStack.java
@@ -18,15 +18,14 @@ package com.android.volley.mock;
import com.android.volley.AuthFailureError;
import com.android.volley.Request;
-import com.android.volley.toolbox.HttpStack;
-
-import org.apache.http.HttpResponse;
+import com.android.volley.toolbox.BaseHttpStack;
+import com.android.volley.toolbox.HttpResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
-public class MockHttpStack implements HttpStack {
+public class MockHttpStack extends BaseHttpStack {
private HttpResponse mResponseToReturn;
@@ -59,13 +58,13 @@ public class MockHttpStack implements HttpStack {
}
@Override
- public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+ public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
if (mExceptionToThrow != null) {
throw mExceptionToThrow;
}
mLastUrl = request.getUrl();
- mLastHeaders = new HashMap<String, String>();
+ mLastHeaders = new HashMap<>();
if (request.getHeaders() != null) {
mLastHeaders.putAll(request.getHeaders());
}
diff --git a/src/test/java/com/android/volley/mock/MockResponseDelivery.java b/src/test/java/com/android/volley/mock/MockResponseDelivery.java
index 4dbfd5c..e923c1a 100644
--- a/src/test/java/com/android/volley/mock/MockResponseDelivery.java
+++ b/src/test/java/com/android/volley/mock/MockResponseDelivery.java
@@ -25,6 +25,7 @@ public class MockResponseDelivery implements ResponseDelivery {
public boolean postResponse_called = false;
public boolean postError_called = false;
+ public long postResponse_calledNtimes = 0;
public boolean wasEitherResponseCalled() {
return postResponse_called || postError_called;
@@ -34,12 +35,14 @@ public class MockResponseDelivery implements ResponseDelivery {
@Override
public void postResponse(Request<?> request, Response<?> response) {
postResponse_called = true;
+ postResponse_calledNtimes++;
responsePosted = response;
}
@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
postResponse_called = true;
+ postResponse_calledNtimes++;
responsePosted = response;
runnable.run();
}
diff --git a/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java b/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java
new file mode 100644
index 0000000..615687d
--- /dev/null
+++ b/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java
@@ -0,0 +1,135 @@
+package com.android.volley.toolbox;
+
+import android.util.Pair;
+
+import com.android.volley.Header;
+import com.android.volley.Request;
+import com.android.volley.mock.TestRequest;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.message.BasicHeader;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.when;
+
+@RunWith(RobolectricTestRunner.class)
+public class AdaptedHttpStackTest {
+ private static final Request<?> REQUEST = new TestRequest.Get();
+ private static final Map<String, String> ADDITIONAL_HEADERS = Collections.emptyMap();
+
+ @Mock
+ private HttpStack mHttpStack;
+ @Mock
+ private HttpResponse mHttpResponse;
+ @Mock
+ private StatusLine mStatusLine;
+ @Mock
+ private HttpEntity mHttpEntity;
+ @Mock
+ private InputStream mContent;
+
+ private AdaptedHttpStack mAdaptedHttpStack;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mAdaptedHttpStack = new AdaptedHttpStack(mHttpStack);
+ when(mHttpResponse.getStatusLine()).thenReturn(mStatusLine);
+ }
+
+ @Test(expected = SocketTimeoutException.class)
+ public void requestTimeout() throws Exception {
+ when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS))
+ .thenThrow(new ConnectTimeoutException());
+
+ mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+ }
+
+ @Test
+ public void emptyResponse() throws Exception {
+ when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse);
+ when(mStatusLine.getStatusCode()).thenReturn(12345);
+ when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]);
+
+ com.android.volley.toolbox.HttpResponse response =
+ mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+
+ assertEquals(12345, response.getStatusCode());
+ assertEquals(Collections.emptyList(), response.getHeaders());
+ assertNull(response.getContent());
+ }
+
+ @Test
+ public void nonEmptyResponse() throws Exception {
+ when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse);
+ when(mStatusLine.getStatusCode()).thenReturn(12345);
+ when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]);
+ when(mHttpResponse.getEntity()).thenReturn(mHttpEntity);
+ when(mHttpEntity.getContentLength()).thenReturn((long) Integer.MAX_VALUE);
+ when(mHttpEntity.getContent()).thenReturn(mContent);
+
+ com.android.volley.toolbox.HttpResponse response =
+ mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+
+ assertEquals(12345, response.getStatusCode());
+ assertEquals(Collections.emptyList(), response.getHeaders());
+ assertEquals(Integer.MAX_VALUE, response.getContentLength());
+ assertSame(mContent, response.getContent());
+ }
+
+ @Test(expected = IOException.class)
+ public void responseTooBig() throws Exception {
+ when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse);
+ when(mStatusLine.getStatusCode()).thenReturn(12345);
+ when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]);
+ when(mHttpResponse.getEntity()).thenReturn(mHttpEntity);
+ when(mHttpEntity.getContentLength()).thenReturn(Integer.MAX_VALUE + 1L);
+ when(mHttpEntity.getContent()).thenReturn(mContent);
+
+ mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+ }
+
+ @Test
+ public void responseWithHeaders() throws Exception {
+ when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse);
+ when(mStatusLine.getStatusCode()).thenReturn(12345);
+ when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[] {
+ new BasicHeader("header1", "value1_B"),
+ new BasicHeader("header3", "value3"),
+ new BasicHeader("HEADER2", "value2"),
+ new BasicHeader("header1", "value1_A")
+ });
+
+ com.android.volley.toolbox.HttpResponse response =
+ mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+
+ assertEquals(12345, response.getStatusCode());
+ assertNull(response.getContent());
+
+ List<Header> expectedHeaders = new ArrayList<>();
+ expectedHeaders.add(new Header("header1", "value1_B"));
+ expectedHeaders.add(new Header("header3", "value3"));
+ expectedHeaders.add(new Header("HEADER2", "value2"));
+ expectedHeaders.add(new Header("header1", "value1_A"));
+ assertEquals(expectedHeaders, response.getHeaders());
+ }
+}
diff --git a/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java b/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java
new file mode 100644
index 0000000..3ae145c
--- /dev/null
+++ b/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java
@@ -0,0 +1,108 @@
+package com.android.volley.toolbox;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.Request;
+import com.android.volley.mock.TestRequest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+@RunWith(RobolectricTestRunner.class)
+public class BaseHttpStackTest {
+ private static final Request<?> REQUEST = new TestRequest.Get();
+ private static final Map<String, String> ADDITIONAL_HEADERS = Collections.emptyMap();
+
+ @Mock
+ private InputStream mContent;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void legacyRequestWithoutBody() throws Exception {
+ BaseHttpStack stack = new BaseHttpStack() {
+ @Override
+ public HttpResponse executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ assertSame(REQUEST, request);
+ assertSame(ADDITIONAL_HEADERS, additionalHeaders);
+ return new HttpResponse(12345, Collections.<Header>emptyList());
+ }
+ };
+ org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS);
+ assertEquals(12345, resp.getStatusLine().getStatusCode());
+ assertEquals(0, resp.getAllHeaders().length);
+ assertNull(resp.getEntity());
+ }
+
+ @Test
+ public void legacyResponseWithBody() throws Exception {
+ BaseHttpStack stack = new BaseHttpStack() {
+ @Override
+ public HttpResponse executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ assertSame(REQUEST, request);
+ assertSame(ADDITIONAL_HEADERS, additionalHeaders);
+ return new HttpResponse(
+ 12345,
+ Collections.<Header>emptyList(),
+ 555,
+ mContent);
+ }
+ };
+ org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS);
+ assertEquals(12345, resp.getStatusLine().getStatusCode());
+ assertEquals(0, resp.getAllHeaders().length);
+ assertEquals(555L, resp.getEntity().getContentLength());
+ assertSame(mContent, resp.getEntity().getContent());
+ }
+
+ @Test
+ public void legacyResponseHeaders() throws Exception {
+ BaseHttpStack stack = new BaseHttpStack() {
+ @Override
+ public HttpResponse executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ assertSame(REQUEST, request);
+ assertSame(ADDITIONAL_HEADERS, additionalHeaders);
+ List<Header> headers = new ArrayList<>();
+ headers.add(new Header("HeaderA", "ValueA"));
+ headers.add(new Header("HeaderB", "ValueB_1"));
+ headers.add(new Header("HeaderB", "ValueB_2"));
+ return new HttpResponse(12345, headers);
+ }
+ };
+ org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS);
+ assertEquals(12345, resp.getStatusLine().getStatusCode());
+ assertEquals(3, resp.getAllHeaders().length);
+ assertEquals("HeaderA", resp.getAllHeaders()[0].getName());
+ assertEquals("ValueA", resp.getAllHeaders()[0].getValue());
+ assertEquals("HeaderB", resp.getAllHeaders()[1].getName());
+ assertEquals("ValueB_1", resp.getAllHeaders()[1].getValue());
+ assertEquals("HeaderB", resp.getAllHeaders()[2].getName());
+ assertEquals("ValueB_2", resp.getAllHeaders()[2].getValue());
+ assertNull(resp.getEntity());
+ }
+}
diff --git a/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java b/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
index c01d9b0..7f0d5e2 100644
--- a/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
+++ b/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
@@ -17,6 +17,8 @@
package com.android.volley.toolbox;
import com.android.volley.AuthFailureError;
+import com.android.volley.Cache.Entry;
+import com.android.volley.Header;
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.Response;
@@ -26,23 +28,31 @@ import com.android.volley.TimeoutError;
import com.android.volley.VolleyError;
import com.android.volley.mock.MockHttpStack;
-import org.apache.http.ProtocolVersion;
-import org.apache.http.conn.ConnectTimeoutException;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.message.BasicHttpResponse;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.robolectric.RobolectricTestRunner;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;
@RunWith(RobolectricTestRunner.class)
@@ -50,7 +60,6 @@ public class BasicNetworkTest {
@Mock private Request<String> mMockRequest;
@Mock private RetryPolicy mMockRetryPolicy;
- private BasicNetwork mNetwork;
@Before public void setUp() throws Exception {
initMocks(this);
@@ -58,36 +67,98 @@ public class BasicNetworkTest {
@Test public void headersAndPostParams() throws Exception {
MockHttpStack mockHttpStack = new MockHttpStack();
- BasicHttpResponse fakeResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1),
- 200, "OK");
- fakeResponse.setEntity(new StringEntity("foobar"));
+ InputStream responseStream =
+ new ByteArrayInputStream("foobar".getBytes());
+ HttpResponse fakeResponse =
+ new HttpResponse(200, Collections.<Header>emptyList(), 6, responseStream);
mockHttpStack.setResponseToReturn(fakeResponse);
BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
Request<String> request = buildRequest();
+ Entry entry = new Entry();
+ entry.etag = "foobar";
+ entry.lastModified = 1503102002000L;
+ request.setCacheEntry(entry);
httpNetwork.performRequest(request);
assertEquals("foo", mockHttpStack.getLastHeaders().get("requestheader"));
+ assertEquals("foobar", mockHttpStack.getLastHeaders().get("If-None-Match"));
+ assertEquals("Sat, 19 Aug 2017 00:20:02 GMT",
+ mockHttpStack.getLastHeaders().get("If-Modified-Since"));
assertEquals("requestpost=foo&", new String(mockHttpStack.getLastPostBody()));
}
- @Test public void socketTimeout() throws Exception {
+ @Test public void notModified() throws Exception {
MockHttpStack mockHttpStack = new MockHttpStack();
- mockHttpStack.setExceptionToThrow(new SocketTimeoutException());
+ List<Header> headers = new ArrayList<>();
+ headers.add(new Header("ServerKeyA", "ServerValueA"));
+ headers.add(new Header("ServerKeyB", "ServerValueB"));
+ headers.add(new Header("SharedKey", "ServerValueShared"));
+ headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1"));
+ headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2"));
+ HttpResponse fakeResponse =
+ new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers);
+ mockHttpStack.setResponseToReturn(fakeResponse);
BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
Request<String> request = buildRequest();
- request.setRetryPolicy(mMockRetryPolicy);
- doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
- try {
- httpNetwork.performRequest(request);
- } catch (VolleyError e) {
- // expected
- }
- // should retry socket timeouts
- verify(mMockRetryPolicy).retry(any(TimeoutError.class));
+ Entry entry = new Entry();
+ entry.allResponseHeaders = new ArrayList<>();
+ entry.allResponseHeaders.add(new Header("CachedKeyA", "CachedValueA"));
+ entry.allResponseHeaders.add(new Header("CachedKeyB", "CachedValueB"));
+ entry.allResponseHeaders.add(new Header("SharedKey", "CachedValueShared"));
+ entry.allResponseHeaders.add(new Header("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1"));
+ entry.allResponseHeaders.add(new Header("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2"));
+ request.setCacheEntry(entry);
+ NetworkResponse response = httpNetwork.performRequest(request);
+ List<Header> expectedHeaders = new ArrayList<>();
+ // Should have all server headers + cache headers that didn't show up in server response.
+ expectedHeaders.add(new Header("ServerKeyA", "ServerValueA"));
+ expectedHeaders.add(new Header("ServerKeyB", "ServerValueB"));
+ expectedHeaders.add(new Header("SharedKey", "ServerValueShared"));
+ expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1"));
+ expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2"));
+ expectedHeaders.add(new Header("CachedKeyA", "CachedValueA"));
+ expectedHeaders.add(new Header("CachedKeyB", "CachedValueB"));
+ assertThat(expectedHeaders, containsInAnyOrder(
+ response.allHeaders.toArray(new Header[response.allHeaders.size()])));
+ }
+
+ @Test public void notModified_legacyCache() throws Exception {
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ List<Header> headers = new ArrayList<>();
+ headers.add(new Header("ServerKeyA", "ServerValueA"));
+ headers.add(new Header("ServerKeyB", "ServerValueB"));
+ headers.add(new Header("SharedKey", "ServerValueShared"));
+ headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1"));
+ headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2"));
+ HttpResponse fakeResponse =
+ new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers);
+ mockHttpStack.setResponseToReturn(fakeResponse);
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ Entry entry = new Entry();
+ entry.responseHeaders = new HashMap<>();
+ entry.responseHeaders.put("CachedKeyA", "CachedValueA");
+ entry.responseHeaders.put("CachedKeyB", "CachedValueB");
+ entry.responseHeaders.put("SharedKey", "CachedValueShared");
+ entry.responseHeaders.put("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1");
+ entry.responseHeaders.put("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2");
+ request.setCacheEntry(entry);
+ NetworkResponse response = httpNetwork.performRequest(request);
+ List<Header> expectedHeaders = new ArrayList<>();
+ // Should have all server headers + cache headers that didn't show up in server response.
+ expectedHeaders.add(new Header("ServerKeyA", "ServerValueA"));
+ expectedHeaders.add(new Header("ServerKeyB", "ServerValueB"));
+ expectedHeaders.add(new Header("SharedKey", "ServerValueShared"));
+ expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1"));
+ expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2"));
+ expectedHeaders.add(new Header("CachedKeyA", "CachedValueA"));
+ expectedHeaders.add(new Header("CachedKeyB", "CachedValueB"));
+ assertThat(expectedHeaders, containsInAnyOrder(
+ response.allHeaders.toArray(new Header[response.allHeaders.size()])));
}
- @Test public void connectTimeout() throws Exception {
+ @Test public void socketTimeout() throws Exception {
MockHttpStack mockHttpStack = new MockHttpStack();
- mockHttpStack.setExceptionToThrow(new ConnectTimeoutException());
+ mockHttpStack.setExceptionToThrow(new SocketTimeoutException());
BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
Request<String> request = buildRequest();
request.setRetryPolicy(mMockRetryPolicy);
@@ -97,7 +168,7 @@ public class BasicNetworkTest {
} catch (VolleyError e) {
// expected
}
- // should retry connection timeouts
+ // should retry socket timeouts
verify(mMockRetryPolicy).retry(any(TimeoutError.class));
}
@@ -119,8 +190,7 @@ public class BasicNetworkTest {
@Test public void unauthorized() throws Exception {
MockHttpStack mockHttpStack = new MockHttpStack();
- BasicHttpResponse fakeResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1),
- 401, "Unauthorized");
+ HttpResponse fakeResponse = new HttpResponse(401, Collections.<Header>emptyList());
mockHttpStack.setResponseToReturn(fakeResponse);
BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
Request<String> request = buildRequest();
@@ -137,8 +207,7 @@ public class BasicNetworkTest {
@Test public void forbidden() throws Exception {
MockHttpStack mockHttpStack = new MockHttpStack();
- BasicHttpResponse fakeResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1),
- 403, "Forbidden");
+ HttpResponse fakeResponse = new HttpResponse(403, Collections.<Header>emptyList());
mockHttpStack.setResponseToReturn(fakeResponse);
BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
Request<String> request = buildRequest();
@@ -156,8 +225,7 @@ public class BasicNetworkTest {
@Test public void redirect() throws Exception {
for (int i = 300; i <= 399; i++) {
MockHttpStack mockHttpStack = new MockHttpStack();
- BasicHttpResponse fakeResponse =
- new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, "");
+ HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
mockHttpStack.setResponseToReturn(fakeResponse);
BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
Request<String> request = buildRequest();
@@ -181,8 +249,7 @@ public class BasicNetworkTest {
continue;
}
MockHttpStack mockHttpStack = new MockHttpStack();
- BasicHttpResponse fakeResponse =
- new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, "");
+ HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
mockHttpStack.setResponseToReturn(fakeResponse);
BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
Request<String> request = buildRequest();
@@ -202,8 +269,7 @@ public class BasicNetworkTest {
@Test public void serverError_enableRetries() throws Exception {
for (int i = 500; i <= 599; i++) {
MockHttpStack mockHttpStack = new MockHttpStack();
- BasicHttpResponse fakeResponse =
- new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, "");
+ HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
mockHttpStack.setResponseToReturn(fakeResponse);
BasicNetwork httpNetwork =
new BasicNetwork(mockHttpStack, new ByteArrayPool(4096));
@@ -225,8 +291,7 @@ public class BasicNetworkTest {
@Test public void serverError_disableRetries() throws Exception {
for (int i = 500; i <= 599; i++) {
MockHttpStack mockHttpStack = new MockHttpStack();
- BasicHttpResponse fakeResponse =
- new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, "");
+ HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
mockHttpStack.setResponseToReturn(fakeResponse);
BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
Request<String> request = buildRequest();
diff --git a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
index 3d8d1f1..04c071e 100644
--- a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
+++ b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
@@ -17,6 +17,7 @@
package com.android.volley.toolbox;
import com.android.volley.Cache;
+import com.android.volley.Header;
import com.android.volley.toolbox.DiskBasedCache.CacheHeader;
import com.android.volley.toolbox.DiskBasedCache.CountingInputStream;
@@ -38,7 +39,9 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Random;
@@ -428,28 +431,33 @@ public class DiskBasedCacheTest {
assertEquals(DiskBasedCache.readString(cis), "ファイカス");
}
- @Test public void serializeMap() throws Exception {
+ @Test public void serializeHeaders() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
- Map<String, String> empty = new HashMap<>();
- DiskBasedCache.writeStringStringMap(empty, baos);
- DiskBasedCache.writeStringStringMap(null, baos);
- Map<String, String> twoThings = new HashMap<>();
- twoThings.put("first", "thing");
- twoThings.put("second", "item");
- DiskBasedCache.writeStringStringMap(twoThings, baos);
- Map<String, String> emptyKey = new HashMap<>();
- emptyKey.put("", "value");
- DiskBasedCache.writeStringStringMap(emptyKey, baos);
- Map<String, String> emptyValue = new HashMap<>();
- emptyValue.put("key", "");
- DiskBasedCache.writeStringStringMap(emptyValue, baos);
+ List<Header> empty = new ArrayList<>();
+ DiskBasedCache.writeHeaderList(empty, baos);
+ DiskBasedCache.writeHeaderList(null, baos);
+ List<Header> twoThings = new ArrayList<>();
+ twoThings.add(new Header("first", "thing"));
+ twoThings.add(new Header("second", "item"));
+ DiskBasedCache.writeHeaderList(twoThings, baos);
+ List<Header> emptyKey = new ArrayList<>();
+ emptyKey.add(new Header("", "value"));
+ DiskBasedCache.writeHeaderList(emptyKey, baos);
+ List<Header> emptyValue = new ArrayList<>();
+ emptyValue.add(new Header("key", ""));
+ DiskBasedCache.writeHeaderList(emptyValue, baos);
+ List<Header> sameKeys = new ArrayList<>();
+ sameKeys.add(new Header("key", "value"));
+ sameKeys.add(new Header("key", "value2"));
+ DiskBasedCache.writeHeaderList(sameKeys, baos);
CountingInputStream cis =
new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size());
- assertEquals(DiskBasedCache.readStringStringMap(cis), empty);
- assertEquals(DiskBasedCache.readStringStringMap(cis), empty); // null reads back empty
- assertEquals(DiskBasedCache.readStringStringMap(cis), twoThings);
- assertEquals(DiskBasedCache.readStringStringMap(cis), emptyKey);
- assertEquals(DiskBasedCache.readStringStringMap(cis), emptyValue);
+ assertEquals(DiskBasedCache.readHeaderList(cis), empty);
+ assertEquals(DiskBasedCache.readHeaderList(cis), empty); // null reads back empty
+ assertEquals(DiskBasedCache.readHeaderList(cis), twoThings);
+ assertEquals(DiskBasedCache.readHeaderList(cis), emptyKey);
+ assertEquals(DiskBasedCache.readHeaderList(cis), emptyValue);
+ assertEquals(DiskBasedCache.readHeaderList(cis), sameKeys);
}
@Test
diff --git a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
index fd8cf51..9ccac05 100644
--- a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
+++ b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
@@ -17,10 +17,9 @@
package com.android.volley.toolbox;
import com.android.volley.Cache;
+import com.android.volley.Header;
import com.android.volley.NetworkResponse;
-import org.apache.http.Header;
-import org.apache.http.message.BasicHeader;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -28,8 +27,10 @@ import org.robolectric.RobolectricTestRunner;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -269,24 +270,23 @@ public class HttpHeaderParserTest {
}
@Test public void parseCaseInsensitive() {
-
long now = System.currentTimeMillis();
- Header[] headersArray = new Header[5];
- headersArray[0] = new BasicHeader("eTAG", "Yow!");
- headersArray[1] = new BasicHeader("DATE", rfc1123Date(now));
- headersArray[2] = new BasicHeader("expires", rfc1123Date(now + ONE_HOUR_MILLIS));
- headersArray[3] = new BasicHeader("cache-control", "public, max-age=86400");
- headersArray[4] = new BasicHeader("content-type", "text/plain");
+ List<Header> headers = new ArrayList<>();
+ headers.add(new Header("eTAG", "Yow!"));
+ headers.add(new Header("DATE", rfc1123Date(now)));
+ headers.add(new Header("expires", rfc1123Date(now + ONE_HOUR_MILLIS)));
+ headers.add(new Header("cache-control", "public, max-age=86400"));
+ headers.add(new Header("content-type", "text/plain"));
- Map<String, String> headers = BasicNetwork.convertHeaders(headersArray);
- NetworkResponse response = new NetworkResponse(0, null, headers, false);
+ NetworkResponse response = new NetworkResponse(0, null, false, 0, headers);
Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
assertNotNull(entry);
assertEquals("Yow!", entry.etag);
assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
assertEquals(entry.softTtl, entry.ttl);
- assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers));
+ assertEquals("ISO-8859-1",
+ HttpHeaderParser.parseCharset(HttpHeaderParser.toHeaderMap(headers)));
}
}
diff --git a/src/test/java/com/android/volley/toolbox/HurlStackTest.java b/src/test/java/com/android/volley/toolbox/HurlStackTest.java
index 42aeea8..c8dd6f1 100644
--- a/src/test/java/com/android/volley/toolbox/HurlStackTest.java
+++ b/src/test/java/com/android/volley/toolbox/HurlStackTest.java
@@ -16,6 +16,7 @@
package com.android.volley.toolbox;
+import com.android.volley.Header;
import com.android.volley.Request.Method;
import com.android.volley.mock.MockHttpURLConnection;
import com.android.volley.mock.TestRequest;
@@ -25,6 +26,12 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
import static org.junit.Assert.*;
@RunWith(RobolectricTestRunner.class)
@@ -152,4 +159,20 @@ public class HurlStackTest {
assertEquals("PATCH", mMockConnection.getRequestMethod());
assertTrue(mMockConnection.getDoOutput());
}
+
+ @Test public void convertHeaders() {
+ Map<String, List<String>> headers = new HashMap<>();
+ headers.put(null, Collections.singletonList("Ignored"));
+ headers.put("HeaderA", Collections.singletonList("ValueA"));
+ List<String> values = new ArrayList<>();
+ values.add("ValueB_1");
+ values.add("ValueB_2");
+ headers.put("HeaderB", values);
+ List<Header> result = HurlStack.convertHeaders(headers);
+ List<Header> expected = new ArrayList<>();
+ expected.add(new Header("HeaderA", "ValueA"));
+ expected.add(new Header("HeaderB", "ValueB_1"));
+ expected.add(new Header("HeaderB", "ValueB_2"));
+ assertEquals(expected, result);
+ }
}