aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnonymous <no-reply@google.com>2020-10-07 17:22:55 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2020-10-07 17:22:55 +0000
commit82c0ddbc31cffa3a4580bba39c970c914cbc01a7 (patch)
treeb9b7a6b9928136e83fda62aaa8b8887df88839b5
parentac0ecd4e10f680027224ddb392ec87701007dfa4 (diff)
parenta0f072f60a3017d852d79ca73601723045071139 (diff)
downloadvolley-82c0ddbc31cffa3a4580bba39c970c914cbc01a7.tar.gz
Import of Volley from GitHub to AOSP. am: b6bd7aa39d am: 839f7a7fb2 am: a0f072f60a
Original change: https://android-review.googlesource.com/c/platform/external/volley/+/1452115 Change-Id: Ia09e4f359a6aa3580b18190e6a5617143df2e243
-rw-r--r--Android.bp4
-rw-r--r--bintray.gradle6
-rw-r--r--build.gradle1
-rw-r--r--gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--rules.gradle12
-rw-r--r--src/main/AndroidManifest.xml15
-rw-r--r--src/main/java/com/android/volley/AsyncCache.java89
-rw-r--r--src/main/java/com/android/volley/AsyncNetwork.java140
-rw-r--r--src/main/java/com/android/volley/AsyncRequestQueue.java626
-rw-r--r--src/main/java/com/android/volley/CacheDispatcher.java115
-rw-r--r--src/main/java/com/android/volley/Request.java22
-rw-r--r--src/main/java/com/android/volley/RequestQueue.java20
-rw-r--r--src/main/java/com/android/volley/RequestTask.java15
-rw-r--r--src/main/java/com/android/volley/WaitingRequestManager.java176
-rw-r--r--src/main/java/com/android/volley/cronet/CronetHttpStack.java631
-rw-r--r--src/main/java/com/android/volley/toolbox/AsyncHttpStack.java170
-rw-r--r--src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java288
-rw-r--r--src/main/java/com/android/volley/toolbox/BasicNetwork.java230
-rw-r--r--src/main/java/com/android/volley/toolbox/FileSupplier.java24
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpHeaderParser.java74
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpResponse.java42
-rw-r--r--src/main/java/com/android/volley/toolbox/HurlStack.java8
-rw-r--r--src/main/java/com/android/volley/toolbox/NetworkUtility.java196
-rw-r--r--src/main/java/com/android/volley/toolbox/NoAsyncCache.java37
-rw-r--r--src/main/java/com/android/volley/toolbox/UrlRewriter.java29
-rw-r--r--src/test/java/com/android/volley/AsyncRequestQueueTest.java164
-rw-r--r--src/test/java/com/android/volley/cronet/CronetHttpStackTest.java381
-rw-r--r--src/test/java/com/android/volley/mock/MockAsyncStack.java86
-rw-r--r--src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java508
-rw-r--r--src/test/java/com/android/volley/toolbox/BasicNetworkTest.java40
-rw-r--r--src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java2
-rw-r--r--src/test/java/com/android/volley/utils/CacheTestUtils.java35
32 files changed, 3819 insertions, 369 deletions
diff --git a/Android.bp b/Android.bp
index c13de29..a6f715c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -21,6 +21,10 @@ java_library {
min_sdk_version: "8",
srcs: ["src/main/java/**/*.java"],
+ // Exclude Cronet support for now. Can be enabled later if/when Cronet is made available as a
+ // compilation dependency for Volley clients.
+ exclude_srcs: ["src/main/java/com/android/volley/cronet/**/*"],
+
libs: [
// Only needed at compile-time.
"androidx.annotation_annotation",
diff --git a/bintray.gradle b/bintray.gradle
index 9007c31..b642b41 100644
--- a/bintray.gradle
+++ b/bintray.gradle
@@ -43,6 +43,12 @@ publishing {
version project.version
pom {
packaging 'aar'
+ licenses {
+ license {
+ name = "The Apache License, Version 2.0"
+ url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
+ }
+ }
}
// Release AAR, Sources, and JavaDoc
diff --git a/build.gradle b/build.gradle
index 828a192..544771c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -62,7 +62,6 @@ android {
buildToolsVersion = '28.0.3'
defaultConfig {
- // Keep in sync with src/main/AndroidManifest.xml
minSdkVersion 8
}
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 9d8a946..104b82e 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
diff --git a/rules.gradle b/rules.gradle
index fd660cd..e0aef80 100644
--- a/rules.gradle
+++ b/rules.gradle
@@ -21,14 +21,16 @@ tasks.withType(JavaCompile) {
dependencies {
implementation "androidx.annotation:annotation:1.0.1"
+ compileOnly "org.chromium.net:cronet-embedded:76.3809.111"
}
// Check if the android plugin version supports unit testing.
-if (configurations.findByName("testCompile")) {
+if (configurations.findByName("testImplementation")) {
dependencies {
- testCompile "junit:junit:4.12"
- testCompile "org.hamcrest:hamcrest-library:1.3"
- testCompile "org.mockito:mockito-core:2.19.0"
- testCompile "org.robolectric:robolectric:3.4.2"
+ testImplementation "org.chromium.net:cronet-embedded:76.3809.111"
+ testImplementation "junit:junit:4.12"
+ testImplementation "org.hamcrest:hamcrest-library:1.3"
+ testImplementation "org.mockito:mockito-core:2.19.0"
+ testImplementation "org.robolectric:robolectric:3.4.2"
}
}
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index da8d33e..ba3a2a7 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -1,15 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- package="com.android.volley"
- android:versionCode="1"
- android:versionName="1.0" >
-
- <!-- Keep in sync with build.gradle -->
- <uses-sdk
- android:minSdkVersion="8"
- tools:ignore="GradleOverrides" />
-
- <application />
-
-</manifest>
+<manifest package="com.android.volley" />
diff --git a/src/main/java/com/android/volley/AsyncCache.java b/src/main/java/com/android/volley/AsyncCache.java
new file mode 100644
index 0000000..3cddb4b
--- /dev/null
+++ b/src/main/java/com/android/volley/AsyncCache.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 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 androidx.annotation.Nullable;
+
+/** Asynchronous equivalent to the {@link Cache} interface. */
+public abstract class AsyncCache {
+
+ public interface OnGetCompleteCallback {
+ /**
+ * Invoked when the read from the cache is complete.
+ *
+ * @param entry The entry read from the cache, or null if the read failed or the key did not
+ * exist in the cache.
+ */
+ void onGetComplete(@Nullable Cache.Entry entry);
+ }
+
+ /**
+ * Retrieves an entry from the cache and sends it back through the {@link
+ * OnGetCompleteCallback#onGetComplete} function
+ *
+ * @param key Cache key
+ * @param callback Callback that will be notified when the information has been retrieved
+ */
+ public abstract void get(String key, OnGetCompleteCallback callback);
+
+ public interface OnWriteCompleteCallback {
+ /** Invoked when the cache operation is complete */
+ void onWriteComplete();
+ }
+
+ /**
+ * Writes a {@link Cache.Entry} to the cache, and calls {@link
+ * OnWriteCompleteCallback#onWriteComplete} after the operation is finished.
+ *
+ * @param key Cache key
+ * @param entry The entry to be written to the cache
+ * @param callback Callback that will be notified when the information has been written
+ */
+ public abstract void put(String key, Cache.Entry entry, OnWriteCompleteCallback callback);
+
+ /**
+ * Clears the cache. Deletes all cached files from disk. Calls {@link
+ * OnWriteCompleteCallback#onWriteComplete} after the operation is finished.
+ */
+ public abstract void clear(OnWriteCompleteCallback callback);
+
+ /**
+ * Initializes the cache and calls {@link OnWriteCompleteCallback#onWriteComplete} after the
+ * operation is finished.
+ */
+ public abstract void initialize(OnWriteCompleteCallback callback);
+
+ /**
+ * Invalidates an entry in the cache and calls {@link OnWriteCompleteCallback#onWriteComplete}
+ * after the operation is finished.
+ *
+ * @param key Cache key
+ * @param fullExpire True to fully expire the entry, false to soft expire
+ * @param callback Callback that's invoked once the entry has been invalidated
+ */
+ public abstract void invalidate(
+ String key, boolean fullExpire, OnWriteCompleteCallback callback);
+
+ /**
+ * Removes a {@link Cache.Entry} from the cache, and calls {@link
+ * OnWriteCompleteCallback#onWriteComplete} after the operation is finished.
+ *
+ * @param key Cache key
+ * @param callback Callback that's invoked once the entry has been removed
+ */
+ public abstract void remove(String key, OnWriteCompleteCallback callback);
+}
diff --git a/src/main/java/com/android/volley/AsyncNetwork.java b/src/main/java/com/android/volley/AsyncNetwork.java
new file mode 100644
index 0000000..ad19c03
--- /dev/null
+++ b/src/main/java/com/android/volley/AsyncNetwork.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2020 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 androidx.annotation.RestrictTo;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** An asynchronous implementation of {@link Network} to perform requests. */
+public abstract class AsyncNetwork implements Network {
+ private ExecutorService mBlockingExecutor;
+ private ExecutorService mNonBlockingExecutor;
+ private ScheduledExecutorService mNonBlockingScheduledExecutor;
+
+ protected AsyncNetwork() {}
+
+ /** Interface for callback to be called after request is processed. */
+ public interface OnRequestComplete {
+ /** Method to be called after successful network request. */
+ void onSuccess(NetworkResponse networkResponse);
+
+ /** Method to be called after unsuccessful network request. */
+ void onError(VolleyError volleyError);
+ }
+
+ /**
+ * Non-blocking method to perform the specified request.
+ *
+ * @param request Request to process
+ * @param callback to be called once NetworkResponse is received
+ */
+ public abstract void performRequest(Request<?> request, OnRequestComplete callback);
+
+ /**
+ * Blocking method to perform network request.
+ *
+ * @param request Request to process
+ * @return response retrieved from the network
+ * @throws VolleyError in the event of an error
+ */
+ @Override
+ public NetworkResponse performRequest(Request<?> request) throws VolleyError {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference<NetworkResponse> response = new AtomicReference<>();
+ final AtomicReference<VolleyError> error = new AtomicReference<>();
+ performRequest(
+ request,
+ new OnRequestComplete() {
+ @Override
+ public void onSuccess(NetworkResponse networkResponse) {
+ response.set(networkResponse);
+ latch.countDown();
+ }
+
+ @Override
+ public void onError(VolleyError volleyError) {
+ error.set(volleyError);
+ latch.countDown();
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ VolleyLog.e(e, "while waiting for CountDownLatch");
+ Thread.currentThread().interrupt();
+ throw new VolleyError(e);
+ }
+
+ if (response.get() != null) {
+ return response.get();
+ } else if (error.get() != null) {
+ throw error.get();
+ } else {
+ throw new VolleyError("Neither response entry was set");
+ }
+ }
+
+ /**
+ * This method sets the non blocking executor to be used by the network for non-blocking tasks.
+ *
+ * <p>This method must be called before performing any requests.
+ */
+ @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+ public void setNonBlockingExecutor(ExecutorService executor) {
+ mNonBlockingExecutor = executor;
+ }
+
+ /**
+ * This method sets the blocking executor to be used by the network for potentially blocking
+ * tasks.
+ *
+ * <p>This method must be called before performing any requests.
+ */
+ @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+ public void setBlockingExecutor(ExecutorService executor) {
+ mBlockingExecutor = executor;
+ }
+
+ /**
+ * This method sets the scheduled executor to be used by the network for non-blocking tasks to
+ * be scheduled.
+ *
+ * <p>This method must be called before performing any requests.
+ */
+ @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+ public void setNonBlockingScheduledExecutor(ScheduledExecutorService executor) {
+ mNonBlockingScheduledExecutor = executor;
+ }
+
+ /** Gets blocking executor to perform any potentially blocking tasks. */
+ protected ExecutorService getBlockingExecutor() {
+ return mBlockingExecutor;
+ }
+
+ /** Gets non-blocking executor to perform any non-blocking tasks. */
+ protected ExecutorService getNonBlockingExecutor() {
+ return mNonBlockingExecutor;
+ }
+
+ /** Gets scheduled executor to perform any non-blocking tasks that need to be scheduled. */
+ protected ScheduledExecutorService getNonBlockingScheduledExecutor() {
+ return mNonBlockingScheduledExecutor;
+ }
+}
diff --git a/src/main/java/com/android/volley/AsyncRequestQueue.java b/src/main/java/com/android/volley/AsyncRequestQueue.java
new file mode 100644
index 0000000..3754866
--- /dev/null
+++ b/src/main/java/com/android/volley/AsyncRequestQueue.java
@@ -0,0 +1,626 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.android.volley.AsyncCache.OnGetCompleteCallback;
+import com.android.volley.AsyncNetwork.OnRequestComplete;
+import com.android.volley.Cache.Entry;
+import java.net.HttpURLConnection;
+import java.util.Comparator;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An asynchronous request dispatch queue.
+ *
+ * <p>Add requests to the queue with {@link #add(Request)}. Once completed, responses will be
+ * delivered on the main thread (unless a custom {@link ResponseDelivery} has been provided)
+ */
+public class AsyncRequestQueue extends RequestQueue {
+ /** Default number of blocking threads to start. */
+ private static final int DEFAULT_BLOCKING_THREAD_POOL_SIZE = 4;
+
+ /**
+ * AsyncCache used to retrieve and store responses.
+ *
+ * <p>{@code null} indicates use of blocking Cache.
+ */
+ @Nullable private final AsyncCache mAsyncCache;
+
+ /** AsyncNetwork used to perform nework requests. */
+ private final AsyncNetwork mNetwork;
+
+ /** Executor for non-blocking tasks. */
+ private ExecutorService mNonBlockingExecutor;
+
+ /** Executor to be used for non-blocking tasks that need to be scheduled. */
+ private ScheduledExecutorService mNonBlockingScheduledExecutor;
+
+ /**
+ * Executor for blocking tasks.
+ *
+ * <p>Some tasks in handling requests may not be easy to implement in a non-blocking way, such
+ * as reading or parsing the response data. This executor is used to run these tasks.
+ */
+ private ExecutorService mBlockingExecutor;
+
+ /**
+ * This interface may be used by advanced applications to provide custom executors according to
+ * their needs. Apps must create ExecutorServices dynamically given a blocking queue rather than
+ * providing them directly so that Volley can provide a PriorityQueue which will prioritize
+ * requests according to Request#getPriority.
+ */
+ private ExecutorFactory mExecutorFactory;
+
+ /** Manage list of waiting requests and de-duplicate requests with same cache key. */
+ private final WaitingRequestManager mWaitingRequestManager = new WaitingRequestManager(this);
+
+ /**
+ * Sets all the variables, but processing does not begin until {@link #start()} is called.
+ *
+ * @param cache to use for persisting responses to disk. If an AsyncCache was provided, then
+ * this will be a {@link ThrowingCache}
+ * @param network to perform HTTP requests
+ * @param asyncCache to use for persisting responses to disk. May be null to indicate use of
+ * blocking cache
+ * @param responseDelivery interface for posting responses and errors
+ * @param executorFactory Interface to be used to provide custom executors according to the
+ * users needs.
+ */
+ private AsyncRequestQueue(
+ Cache cache,
+ AsyncNetwork network,
+ @Nullable AsyncCache asyncCache,
+ ResponseDelivery responseDelivery,
+ ExecutorFactory executorFactory) {
+ super(cache, network, /* threadPoolSize= */ 0, responseDelivery);
+ mAsyncCache = asyncCache;
+ mNetwork = network;
+ mExecutorFactory = executorFactory;
+ }
+
+ /** Sets the executors and initializes the cache. */
+ @Override
+ public void start() {
+ stop(); // Make sure any currently running threads are stopped
+
+ // Create blocking / non-blocking executors and set them in the network and stack.
+ mNonBlockingExecutor = mExecutorFactory.createNonBlockingExecutor(getBlockingQueue());
+ mBlockingExecutor = mExecutorFactory.createBlockingExecutor(getBlockingQueue());
+ mNonBlockingScheduledExecutor = mExecutorFactory.createNonBlockingScheduledExecutor();
+ mNetwork.setBlockingExecutor(mBlockingExecutor);
+ mNetwork.setNonBlockingExecutor(mNonBlockingExecutor);
+ mNetwork.setNonBlockingScheduledExecutor(mNonBlockingScheduledExecutor);
+
+ mNonBlockingExecutor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ // This is intentionally blocking, because we don't want to process any
+ // requests until the cache is initialized.
+ if (mAsyncCache != null) {
+ final CountDownLatch latch = new CountDownLatch(1);
+ mAsyncCache.initialize(
+ new AsyncCache.OnWriteCompleteCallback() {
+ @Override
+ public void onWriteComplete() {
+ latch.countDown();
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ VolleyLog.e(
+ e, "Thread was interrupted while initializing the cache.");
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ }
+ } else {
+ getCache().initialize();
+ }
+ }
+ });
+ }
+
+ /** Shuts down and nullifies both executors */
+ @Override
+ public void stop() {
+ if (mNonBlockingExecutor != null) {
+ mNonBlockingExecutor.shutdownNow();
+ mNonBlockingExecutor = null;
+ }
+ if (mBlockingExecutor != null) {
+ mBlockingExecutor.shutdownNow();
+ mBlockingExecutor = null;
+ }
+ if (mNonBlockingScheduledExecutor != null) {
+ mNonBlockingScheduledExecutor.shutdownNow();
+ mNonBlockingScheduledExecutor = null;
+ }
+ }
+
+ /** Begins the request by sending it to the Cache or Network. */
+ @Override
+ <T> void beginRequest(Request<T> request) {
+ // If the request is uncacheable, send it over the network.
+ if (request.shouldCache()) {
+ if (mAsyncCache != null) {
+ mNonBlockingExecutor.execute(new CacheTask<>(request));
+ } else {
+ mBlockingExecutor.execute(new CacheTask<>(request));
+ }
+ } else {
+ sendRequestOverNetwork(request);
+ }
+ }
+
+ @Override
+ <T> void sendRequestOverNetwork(Request<T> request) {
+ mNonBlockingExecutor.execute(new NetworkTask<>(request));
+ }
+
+ /** Runnable that gets an entry from the cache. */
+ private class CacheTask<T> extends RequestTask<T> {
+ CacheTask(Request<T> request) {
+ super(request);
+ }
+
+ @Override
+ public void run() {
+ // If the request has been canceled, don't bother dispatching it.
+ if (mRequest.isCanceled()) {
+ mRequest.finish("cache-discard-canceled");
+ return;
+ }
+
+ mRequest.addMarker("cache-queue-take");
+
+ // Attempt to retrieve this item from cache.
+ if (mAsyncCache != null) {
+ mAsyncCache.get(
+ mRequest.getCacheKey(),
+ new OnGetCompleteCallback() {
+ @Override
+ public void onGetComplete(Entry entry) {
+ handleEntry(entry, mRequest);
+ }
+ });
+ } else {
+ Entry entry = getCache().get(mRequest.getCacheKey());
+ handleEntry(entry, mRequest);
+ }
+ }
+ }
+
+ /** Helper method that handles the cache entry after getting it from the Cache. */
+ private void handleEntry(final Entry entry, final Request<?> mRequest) {
+ if (entry == null) {
+ mRequest.addMarker("cache-miss");
+ // Cache miss; send off to the network dispatcher.
+ if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) {
+ sendRequestOverNetwork(mRequest);
+ }
+ return;
+ }
+
+ // If it is completely expired, just send it to the network.
+ if (entry.isExpired()) {
+ mRequest.addMarker("cache-hit-expired");
+ mRequest.setCacheEntry(entry);
+ if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) {
+ sendRequestOverNetwork(mRequest);
+ }
+ return;
+ }
+
+ // We have a cache hit; parse its data for delivery back to the request.
+ mBlockingExecutor.execute(new CacheParseTask<>(mRequest, entry));
+ }
+
+ private class CacheParseTask<T> extends RequestTask<T> {
+ Cache.Entry entry;
+
+ CacheParseTask(Request<T> request, Cache.Entry entry) {
+ super(request);
+ this.entry = entry;
+ }
+
+ @Override
+ public void run() {
+ mRequest.addMarker("cache-hit");
+ Response<?> response =
+ mRequest.parseNetworkResponse(
+ new NetworkResponse(
+ HttpURLConnection.HTTP_OK,
+ entry.data,
+ /* notModified= */ false,
+ /* networkTimeMs= */ 0,
+ entry.allResponseHeaders));
+ mRequest.addMarker("cache-hit-parsed");
+
+ if (!entry.refreshNeeded()) {
+ // Completely unexpired cache hit. Just deliver the response.
+ getResponseDelivery().postResponse(mRequest, response);
+ } else {
+ // Soft-expired cache hit. We can deliver the cached response,
+ // but we need to also send the request to the network for
+ // refreshing.
+ mRequest.addMarker("cache-hit-refresh-needed");
+ mRequest.setCacheEntry(entry);
+ // Mark the response as intermediate.
+ response.intermediate = true;
+
+ if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) {
+ // Post the intermediate response back to the user and have
+ // the delivery then forward the request along to the network.
+ getResponseDelivery()
+ .postResponse(
+ mRequest,
+ response,
+ new Runnable() {
+ @Override
+ public void run() {
+ sendRequestOverNetwork(mRequest);
+ }
+ });
+ } else {
+ // request has been added to list of waiting requests
+ // to receive the network response from the first request once it
+ // returns.
+ getResponseDelivery().postResponse(mRequest, response);
+ }
+ }
+ }
+ }
+
+ private class ParseErrorTask<T> extends RequestTask<T> {
+ VolleyError volleyError;
+
+ ParseErrorTask(Request<T> request, VolleyError volleyError) {
+ super(request);
+ this.volleyError = volleyError;
+ }
+
+ @Override
+ public void run() {
+ VolleyError parsedError = mRequest.parseNetworkError(volleyError);
+ getResponseDelivery().postError(mRequest, parsedError);
+ mRequest.notifyListenerResponseNotUsable();
+ }
+ }
+
+ /** Runnable that performs the network request */
+ private class NetworkTask<T> extends RequestTask<T> {
+ NetworkTask(Request<T> request) {
+ super(request);
+ }
+
+ @Override
+ public void run() {
+ // If the request was cancelled already, do not perform the network request.
+ if (mRequest.isCanceled()) {
+ mRequest.finish("network-discard-cancelled");
+ mRequest.notifyListenerResponseNotUsable();
+ return;
+ }
+
+ final long startTimeMs = SystemClock.elapsedRealtime();
+ mRequest.addMarker("network-queue-take");
+
+ // TODO: Figure out what to do with traffic stats tags. Can this be pushed to the
+ // HTTP stack, or is it no longer feasible to support?
+
+ // Perform the network request.
+ mNetwork.performRequest(
+ mRequest,
+ new OnRequestComplete() {
+ @Override
+ public void onSuccess(final NetworkResponse networkResponse) {
+ mRequest.addMarker("network-http-complete");
+
+ // If the server returned 304 AND we delivered a response already,
+ // we're done -- don't deliver a second identical response.
+ if (networkResponse.notModified && mRequest.hasHadResponseDelivered()) {
+ mRequest.finish("not-modified");
+ mRequest.notifyListenerResponseNotUsable();
+ return;
+ }
+
+ // Parse the response here on the worker thread.
+ mBlockingExecutor.execute(
+ new NetworkParseTask<>(mRequest, networkResponse));
+ }
+
+ @Override
+ public void onError(final VolleyError volleyError) {
+ volleyError.setNetworkTimeMs(
+ SystemClock.elapsedRealtime() - startTimeMs);
+ mBlockingExecutor.execute(new ParseErrorTask<>(mRequest, volleyError));
+ }
+ });
+ }
+ }
+
+ /** Runnable that parses a network response. */
+ private class NetworkParseTask<T> extends RequestTask<T> {
+ NetworkResponse networkResponse;
+
+ NetworkParseTask(Request<T> request, NetworkResponse networkResponse) {
+ super(request);
+ this.networkResponse = networkResponse;
+ }
+
+ @Override
+ public void run() {
+ final Response<?> response = mRequest.parseNetworkResponse(networkResponse);
+ mRequest.addMarker("network-parse-complete");
+
+ // Write to cache if applicable.
+ // TODO: Only update cache metadata instead of entire
+ // record for 304s.
+ if (mRequest.shouldCache() && response.cacheEntry != null) {
+ if (mAsyncCache != null) {
+ mNonBlockingExecutor.execute(new CachePutTask<>(mRequest, response));
+ } else {
+ mBlockingExecutor.execute(new CachePutTask<>(mRequest, response));
+ }
+ } else {
+ finishRequest(mRequest, response, /* cached= */ false);
+ }
+ }
+ }
+
+ private class CachePutTask<T> extends RequestTask<T> {
+ Response<?> response;
+
+ CachePutTask(Request<T> request, Response<?> response) {
+ super(request);
+ this.response = response;
+ }
+
+ @Override
+ public void run() {
+ if (mAsyncCache != null) {
+ mAsyncCache.put(
+ mRequest.getCacheKey(),
+ response.cacheEntry,
+ new AsyncCache.OnWriteCompleteCallback() {
+ @Override
+ public void onWriteComplete() {
+ finishRequest(mRequest, response, /* cached= */ true);
+ }
+ });
+ } else {
+ getCache().put(mRequest.getCacheKey(), response.cacheEntry);
+ finishRequest(mRequest, response, /* cached= */ true);
+ }
+ }
+ }
+
+ /** Posts response and notifies listener */
+ private void finishRequest(Request<?> mRequest, Response<?> response, boolean cached) {
+ if (cached) {
+ mRequest.addMarker("network-cache-written");
+ }
+ // Post the response back.
+ mRequest.markDelivered();
+ getResponseDelivery().postResponse(mRequest, response);
+ mRequest.notifyListenerResponseReceived(response);
+ }
+
+ /**
+ * This class may be used by advanced applications to provide custom executors according to
+ * their needs. Apps must create ExecutorServices dynamically given a blocking queue rather than
+ * providing them directly so that Volley can provide a PriorityQueue which will prioritize
+ * requests according to Request#getPriority.
+ */
+ public abstract static class ExecutorFactory {
+ abstract ExecutorService createNonBlockingExecutor(BlockingQueue<Runnable> taskQueue);
+
+ abstract ExecutorService createBlockingExecutor(BlockingQueue<Runnable> taskQueue);
+
+ abstract ScheduledExecutorService createNonBlockingScheduledExecutor();
+ }
+
+ /** Provides a BlockingQueue to be used to create executors. */
+ private static PriorityBlockingQueue<Runnable> getBlockingQueue() {
+ return new PriorityBlockingQueue<>(
+ /* initialCapacity= */ 11,
+ new Comparator<Runnable>() {
+ @Override
+ public int compare(Runnable r1, Runnable r2) {
+ // Vanilla runnables are prioritized first, then RequestTasks are ordered
+ // by the underlying Request.
+ if (r1 instanceof RequestTask) {
+ if (r2 instanceof RequestTask) {
+ return ((RequestTask<?>) r1).compareTo(((RequestTask<?>) r2));
+ }
+ return 1;
+ }
+ return r2 instanceof RequestTask ? -1 : 0;
+ }
+ });
+ }
+
+ /**
+ * Builder is used to build an instance of {@link AsyncRequestQueue} from values configured by
+ * the setters.
+ */
+ public static class Builder {
+ @Nullable private AsyncCache mAsyncCache = null;
+ private final AsyncNetwork mNetwork;
+ @Nullable private Cache mCache = null;
+ @Nullable private ExecutorFactory mExecutorFactory = null;
+ @Nullable private ResponseDelivery mResponseDelivery = null;
+
+ public Builder(AsyncNetwork asyncNetwork) {
+ if (asyncNetwork == null) {
+ throw new IllegalArgumentException("Network cannot be null");
+ }
+ mNetwork = asyncNetwork;
+ }
+
+ /**
+ * Sets the executor factory to be used by the AsyncRequestQueue. If this is not called,
+ * Volley will create suitable private thread pools.
+ */
+ public Builder setExecutorFactory(ExecutorFactory executorFactory) {
+ mExecutorFactory = executorFactory;
+ return this;
+ }
+
+ /**
+ * Sets the response deliver to be used by the AsyncRequestQueue. If this is not called, we
+ * will default to creating a new {@link ExecutorDelivery} with the application's main
+ * thread.
+ */
+ public Builder setResponseDelivery(ResponseDelivery responseDelivery) {
+ mResponseDelivery = responseDelivery;
+ return this;
+ }
+
+ /** Sets the AsyncCache to be used by the AsyncRequestQueue. */
+ public Builder setAsyncCache(AsyncCache asyncCache) {
+ mAsyncCache = asyncCache;
+ return this;
+ }
+
+ /** Sets the Cache to be used by the AsyncRequestQueue. */
+ public Builder setCache(Cache cache) {
+ mCache = cache;
+ return this;
+ }
+
+ /** Provides a default ExecutorFactory to use, if one is never set. */
+ private ExecutorFactory getDefaultExecutorFactory() {
+ return new ExecutorFactory() {
+ @Override
+ public ExecutorService createNonBlockingExecutor(
+ BlockingQueue<Runnable> taskQueue) {
+ return getNewThreadPoolExecutor(
+ /* maximumPoolSize= */ 1,
+ /* threadNameSuffix= */ "Non-BlockingExecutor",
+ taskQueue);
+ }
+
+ @Override
+ public ExecutorService createBlockingExecutor(BlockingQueue<Runnable> taskQueue) {
+ return getNewThreadPoolExecutor(
+ /* maximumPoolSize= */ DEFAULT_BLOCKING_THREAD_POOL_SIZE,
+ /* threadNameSuffix= */ "BlockingExecutor",
+ taskQueue);
+ }
+
+ @Override
+ public ScheduledExecutorService createNonBlockingScheduledExecutor() {
+ return new ScheduledThreadPoolExecutor(
+ /* corePoolSize= */ 0, getThreadFactory("ScheduledExecutor"));
+ }
+
+ private ThreadPoolExecutor getNewThreadPoolExecutor(
+ int maximumPoolSize,
+ final String threadNameSuffix,
+ BlockingQueue<Runnable> taskQueue) {
+ return new ThreadPoolExecutor(
+ /* corePoolSize= */ 0,
+ /* maximumPoolSize= */ maximumPoolSize,
+ /* keepAliveTime= */ 60,
+ /* unit= */ TimeUnit.SECONDS,
+ taskQueue,
+ getThreadFactory(threadNameSuffix));
+ }
+
+ private ThreadFactory getThreadFactory(final String threadNameSuffix) {
+ return new ThreadFactory() {
+ @Override
+ public Thread newThread(@NonNull Runnable runnable) {
+ Thread t = Executors.defaultThreadFactory().newThread(runnable);
+ t.setName("Volley-" + threadNameSuffix);
+ return t;
+ }
+ };
+ }
+ };
+ }
+
+ public AsyncRequestQueue build() {
+ // If neither cache is set by the caller, throw an illegal argument exception.
+ if (mCache == null && mAsyncCache == null) {
+ throw new IllegalArgumentException("You must set one of the cache objects");
+ }
+ if (mCache == null) {
+ // if no cache is provided, we will provide one that throws
+ // UnsupportedOperationExceptions to pass into the parent class.
+ mCache = new ThrowingCache();
+ }
+ if (mResponseDelivery == null) {
+ mResponseDelivery = new ExecutorDelivery(new Handler(Looper.getMainLooper()));
+ }
+ if (mExecutorFactory == null) {
+ mExecutorFactory = getDefaultExecutorFactory();
+ }
+ return new AsyncRequestQueue(
+ mCache, mNetwork, mAsyncCache, mResponseDelivery, mExecutorFactory);
+ }
+ }
+
+ /** A cache that throws an error if a method is called. */
+ private static class ThrowingCache implements Cache {
+ @Override
+ public Entry get(String key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void put(String key, Entry entry) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void initialize() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void invalidate(String key, boolean fullExpire) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void remove(String key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/CacheDispatcher.java b/src/main/java/com/android/volley/CacheDispatcher.java
index 12b1035..1bfc0ea 100644
--- a/src/main/java/com/android/volley/CacheDispatcher.java
+++ b/src/main/java/com/android/volley/CacheDispatcher.java
@@ -18,10 +18,6 @@ package com.android.volley;
import android.os.Process;
import androidx.annotation.VisibleForTesting;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
import java.util.concurrent.BlockingQueue;
/**
@@ -72,7 +68,7 @@ public class CacheDispatcher extends Thread {
mNetworkQueue = networkQueue;
mCache = cache;
mDelivery = delivery;
- mWaitingRequestManager = new WaitingRequestManager(this);
+ mWaitingRequestManager = new WaitingRequestManager(this, networkQueue, delivery);
}
/**
@@ -207,113 +203,4 @@ public class CacheDispatcher extends Thread {
request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED);
}
}
-
- 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>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.
- * </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);
- nextInLine.setNetworkRequestCompleteListener(this);
- 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.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/Request.java b/src/main/java/com/android/volley/Request.java
index 2b53f96..b60dc74 100644
--- a/src/main/java/com/android/volley/Request.java
+++ b/src/main/java/com/android/volley/Request.java
@@ -107,6 +107,9 @@ public abstract class Request<T> implements Comparable<Request<T>> {
/** Whether the request should be retried in the event of an HTTP 5xx (server) error. */
private boolean mShouldRetryServerErrors = false;
+ /** Whether the request should be retried in the event of a {@link NoConnectionError}. */
+ private boolean mShouldRetryConnectionErrors = false;
+
/** The retry policy for this request. */
private RetryPolicy mRetryPolicy;
@@ -534,6 +537,25 @@ public abstract class Request<T> implements Comparable<Request<T>> {
}
/**
+ * Sets whether or not the request should be retried in the event that no connection could be
+ * established.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public final Request<?> setShouldRetryConnectionErrors(boolean shouldRetryConnectionErrors) {
+ mShouldRetryConnectionErrors = shouldRetryConnectionErrors;
+ return this;
+ }
+
+ /**
+ * Returns true if this request should be retried in the event that no connection could be
+ * established.
+ */
+ public final boolean shouldRetryConnectionErrors() {
+ return mShouldRetryConnectionErrors;
+ }
+
+ /**
* Priority values. Requests will be processed from higher priorities to lower priorities, in
* FIFO order.
*/
diff --git a/src/main/java/com/android/volley/RequestQueue.java b/src/main/java/com/android/volley/RequestQueue.java
index c127c7f..6db0b1c 100644
--- a/src/main/java/com/android/volley/RequestQueue.java
+++ b/src/main/java/com/android/volley/RequestQueue.java
@@ -263,13 +263,17 @@ public class RequestQueue {
request.addMarker("add-to-queue");
sendRequestEvent(request, RequestEvent.REQUEST_QUEUED);
+ beginRequest(request);
+ return request;
+ }
+
+ <T> void beginRequest(Request<T> request) {
// If the request is uncacheable, skip the cache queue and go straight to the network.
if (!request.shouldCache()) {
- mNetworkQueue.add(request);
- return request;
+ sendRequestOverNetwork(request);
+ } else {
+ mCacheQueue.add(request);
}
- mCacheQueue.add(request);
- return request;
}
/**
@@ -327,4 +331,12 @@ public class RequestQueue {
mFinishedListeners.remove(listener);
}
}
+
+ public ResponseDelivery getResponseDelivery() {
+ return mDelivery;
+ }
+
+ <T> void sendRequestOverNetwork(Request<T> request) {
+ mNetworkQueue.add(request);
+ }
}
diff --git a/src/main/java/com/android/volley/RequestTask.java b/src/main/java/com/android/volley/RequestTask.java
new file mode 100644
index 0000000..8eeaf2c
--- /dev/null
+++ b/src/main/java/com/android/volley/RequestTask.java
@@ -0,0 +1,15 @@
+package com.android.volley;
+
+/** Abstract runnable that's a task to be completed by the RequestQueue. */
+public abstract class RequestTask<T> implements Runnable {
+ final Request<T> mRequest;
+
+ public RequestTask(Request<T> request) {
+ mRequest = request;
+ }
+
+ @SuppressWarnings("unchecked")
+ public int compareTo(RequestTask<?> other) {
+ return mRequest.compareTo((Request<T>) other.mRequest);
+ }
+}
diff --git a/src/main/java/com/android/volley/WaitingRequestManager.java b/src/main/java/com/android/volley/WaitingRequestManager.java
new file mode 100644
index 0000000..682e339
--- /dev/null
+++ b/src/main/java/com/android/volley/WaitingRequestManager.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2020 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 androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Callback to notify the caller when the network request returns. Valid responses can be used by
+ * all duplicate requests.
+ */
+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>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.
+ * </ul>
+ */
+ private final Map<String, List<Request<?>>> mWaitingRequests = new HashMap<>();
+
+ private final ResponseDelivery mResponseDelivery;
+
+ /**
+ * RequestQueue that is passed in by the AsyncRequestQueue. This is null when this instance is
+ * initialized by the {@link CacheDispatcher}
+ */
+ @Nullable private final RequestQueue mRequestQueue;
+
+ /**
+ * CacheDispacter that is passed in by the CacheDispatcher. This is null when this instance is
+ * initialized by the {@link AsyncRequestQueue}
+ */
+ @Nullable private final CacheDispatcher mCacheDispatcher;
+
+ /**
+ * BlockingQueue that is passed in by the CacheDispatcher. This is null when this instance is
+ * initialized by the {@link AsyncRequestQueue}
+ */
+ @Nullable private final BlockingQueue<Request<?>> mNetworkQueue;
+
+ WaitingRequestManager(@NonNull RequestQueue requestQueue) {
+ mRequestQueue = requestQueue;
+ mResponseDelivery = mRequestQueue.getResponseDelivery();
+ mCacheDispatcher = null;
+ mNetworkQueue = null;
+ }
+
+ WaitingRequestManager(
+ @NonNull CacheDispatcher cacheDispatcher,
+ @NonNull BlockingQueue<Request<?>> networkQueue,
+ ResponseDelivery responseDelivery) {
+ mRequestQueue = null;
+ mResponseDelivery = responseDelivery;
+ mCacheDispatcher = cacheDispatcher;
+ mNetworkQueue = networkQueue;
+ }
+
+ /** 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) {
+ mResponseDelivery.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);
+ nextInLine.setNetworkRequestCompleteListener(this);
+ // RequestQueue will be non-null if this instance was created in AsyncRequestQueue.
+ if (mRequestQueue != null) {
+ // Will send the network request from the RequestQueue.
+ mRequestQueue.sendRequestOverNetwork(nextInLine);
+ } else if (mCacheDispatcher != null && mNetworkQueue != null) {
+ // If we're not using the AsyncRequestQueue, then submit it to the network queue.
+ try {
+ 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.
+ */
+ 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.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/cronet/CronetHttpStack.java b/src/main/java/com/android/volley/cronet/CronetHttpStack.java
new file mode 100644
index 0000000..f3baace
--- /dev/null
+++ b/src/main/java/com/android/volley/cronet/CronetHttpStack.java
@@ -0,0 +1,631 @@
+/*
+ * Copyright (C) 2020 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.cronet;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Base64;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.Request;
+import com.android.volley.RequestTask;
+import com.android.volley.VolleyLog;
+import com.android.volley.toolbox.AsyncHttpStack;
+import com.android.volley.toolbox.ByteArrayPool;
+import com.android.volley.toolbox.HttpHeaderParser;
+import com.android.volley.toolbox.HttpResponse;
+import com.android.volley.toolbox.PoolingByteArrayOutputStream;
+import com.android.volley.toolbox.UrlRewriter;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.WritableByteChannel;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import org.chromium.net.CronetEngine;
+import org.chromium.net.CronetException;
+import org.chromium.net.UploadDataProvider;
+import org.chromium.net.UploadDataProviders;
+import org.chromium.net.UrlRequest;
+import org.chromium.net.UrlRequest.Callback;
+import org.chromium.net.UrlResponseInfo;
+
+/**
+ * A {@link AsyncHttpStack} that's based on Cronet's fully asynchronous API for network requests.
+ */
+public class CronetHttpStack extends AsyncHttpStack {
+
+ private final CronetEngine mCronetEngine;
+ private final ByteArrayPool mPool;
+ private final UrlRewriter mUrlRewriter;
+ private final RequestListener mRequestListener;
+
+ // cURL logging support
+ private final boolean mCurlLoggingEnabled;
+ private final CurlCommandLogger mCurlCommandLogger;
+ private final boolean mLogAuthTokensInCurlCommands;
+
+ private CronetHttpStack(
+ CronetEngine cronetEngine,
+ ByteArrayPool pool,
+ UrlRewriter urlRewriter,
+ RequestListener requestListener,
+ boolean curlLoggingEnabled,
+ CurlCommandLogger curlCommandLogger,
+ boolean logAuthTokensInCurlCommands) {
+ mCronetEngine = cronetEngine;
+ mPool = pool;
+ mUrlRewriter = urlRewriter;
+ mRequestListener = requestListener;
+ mCurlLoggingEnabled = curlLoggingEnabled;
+ mCurlCommandLogger = curlCommandLogger;
+ mLogAuthTokensInCurlCommands = logAuthTokensInCurlCommands;
+
+ mRequestListener.initialize(this);
+ }
+
+ @Override
+ public void executeRequest(
+ final Request<?> request,
+ final Map<String, String> additionalHeaders,
+ final OnRequestComplete callback) {
+ if (getBlockingExecutor() == null || getNonBlockingExecutor() == null) {
+ throw new IllegalStateException("Must set blocking and non-blocking executors");
+ }
+ final Callback urlCallback =
+ new Callback() {
+ PoolingByteArrayOutputStream bytesReceived = null;
+ WritableByteChannel receiveChannel = null;
+
+ @Override
+ public void onRedirectReceived(
+ UrlRequest urlRequest,
+ UrlResponseInfo urlResponseInfo,
+ String newLocationUrl) {
+ urlRequest.followRedirect();
+ }
+
+ @Override
+ public void onResponseStarted(
+ UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) {
+ bytesReceived =
+ new PoolingByteArrayOutputStream(
+ mPool, getContentLength(urlResponseInfo));
+ receiveChannel = Channels.newChannel(bytesReceived);
+ urlRequest.read(ByteBuffer.allocateDirect(1024));
+ }
+
+ @Override
+ public void onReadCompleted(
+ UrlRequest urlRequest,
+ UrlResponseInfo urlResponseInfo,
+ ByteBuffer byteBuffer) {
+ byteBuffer.flip();
+ try {
+ receiveChannel.write(byteBuffer);
+ byteBuffer.clear();
+ urlRequest.read(byteBuffer);
+ } catch (IOException e) {
+ urlRequest.cancel();
+ callback.onError(e);
+ }
+ }
+
+ @Override
+ public void onSucceeded(
+ UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) {
+ List<Header> headers = getHeaders(urlResponseInfo.getAllHeadersAsList());
+ HttpResponse response =
+ new HttpResponse(
+ urlResponseInfo.getHttpStatusCode(),
+ headers,
+ bytesReceived.toByteArray());
+ callback.onSuccess(response);
+ }
+
+ @Override
+ public void onFailed(
+ UrlRequest urlRequest,
+ UrlResponseInfo urlResponseInfo,
+ CronetException e) {
+ callback.onError(e);
+ }
+ };
+
+ String url = request.getUrl();
+ String rewritten = mUrlRewriter.rewriteUrl(url);
+ if (rewritten == null) {
+ callback.onError(new IOException("URL blocked by rewriter: " + url));
+ return;
+ }
+ url = rewritten;
+
+ // We can call allowDirectExecutor here and run directly on the network thread, since all
+ // the callbacks are non-blocking.
+ final UrlRequest.Builder builder =
+ mCronetEngine
+ .newUrlRequestBuilder(url, urlCallback, getNonBlockingExecutor())
+ .allowDirectExecutor()
+ .disableCache()
+ .setPriority(getPriority(request));
+ // request.getHeaders() may be blocking, so submit it to the blocking executor.
+ getBlockingExecutor()
+ .execute(
+ new SetUpRequestTask<>(request, url, builder, additionalHeaders, callback));
+ }
+
+ private class SetUpRequestTask<T> extends RequestTask<T> {
+ UrlRequest.Builder builder;
+ String url;
+ Map<String, String> additionalHeaders;
+ OnRequestComplete callback;
+ Request<T> request;
+
+ SetUpRequestTask(
+ Request<T> request,
+ String url,
+ UrlRequest.Builder builder,
+ Map<String, String> additionalHeaders,
+ OnRequestComplete callback) {
+ super(request);
+ // Note that this URL may be different from Request#getUrl() due to the UrlRewriter.
+ this.url = url;
+ this.builder = builder;
+ this.additionalHeaders = additionalHeaders;
+ this.callback = callback;
+ this.request = request;
+ }
+
+ @Override
+ public void run() {
+ try {
+ mRequestListener.onRequestPrepared(request, builder);
+ CurlLoggedRequestParameters requestParameters = new CurlLoggedRequestParameters();
+ setHttpMethod(requestParameters, request);
+ setRequestHeaders(requestParameters, request, additionalHeaders);
+ requestParameters.applyToRequest(builder, getNonBlockingExecutor());
+ UrlRequest urlRequest = builder.build();
+ if (mCurlLoggingEnabled) {
+ mCurlCommandLogger.logCurlCommand(generateCurlCommand(url, requestParameters));
+ }
+ urlRequest.start();
+ } catch (AuthFailureError authFailureError) {
+ callback.onAuthError(authFailureError);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public static List<Header> getHeaders(List<Map.Entry<String, String>> headersList) {
+ List<Header> headers = new ArrayList<>();
+ for (Map.Entry<String, String> header : headersList) {
+ headers.add(new Header(header.getKey(), header.getValue()));
+ }
+ return headers;
+ }
+
+ /** Sets the connection parameters for the UrlRequest */
+ private void setHttpMethod(CurlLoggedRequestParameters requestParameters, Request<?> request)
+ throws AuthFailureError {
+ switch (request.getMethod()) {
+ case Request.Method.DEPRECATED_GET_OR_POST:
+ // This is the deprecated way that needs to be handled for backwards compatibility.
+ // If the request's post body is null, then the assumption is that the request is
+ // GET. Otherwise, it is assumed that the request is a POST.
+ byte[] postBody = request.getPostBody();
+ if (postBody != null) {
+ requestParameters.setHttpMethod("POST");
+ addBodyIfExists(requestParameters, request.getPostBodyContentType(), postBody);
+ } else {
+ requestParameters.setHttpMethod("GET");
+ }
+ break;
+ case Request.Method.GET:
+ // Not necessary to set the request method because connection defaults to GET but
+ // being explicit here.
+ requestParameters.setHttpMethod("GET");
+ break;
+ case Request.Method.DELETE:
+ requestParameters.setHttpMethod("DELETE");
+ break;
+ case Request.Method.POST:
+ requestParameters.setHttpMethod("POST");
+ addBodyIfExists(requestParameters, request.getBodyContentType(), request.getBody());
+ break;
+ case Request.Method.PUT:
+ requestParameters.setHttpMethod("PUT");
+ addBodyIfExists(requestParameters, request.getBodyContentType(), request.getBody());
+ break;
+ case Request.Method.HEAD:
+ requestParameters.setHttpMethod("HEAD");
+ break;
+ case Request.Method.OPTIONS:
+ requestParameters.setHttpMethod("OPTIONS");
+ break;
+ case Request.Method.TRACE:
+ requestParameters.setHttpMethod("TRACE");
+ break;
+ case Request.Method.PATCH:
+ requestParameters.setHttpMethod("PATCH");
+ addBodyIfExists(requestParameters, request.getBodyContentType(), request.getBody());
+ break;
+ default:
+ throw new IllegalStateException("Unknown method type.");
+ }
+ }
+
+ /**
+ * Sets the request headers for the UrlRequest.
+ *
+ * @param requestParameters parameters that we are adding the request headers to
+ * @param request to get the headers from
+ * @param additionalHeaders for the UrlRequest
+ * @throws AuthFailureError is thrown if Request#getHeaders throws ones
+ */
+ private void setRequestHeaders(
+ CurlLoggedRequestParameters requestParameters,
+ Request<?> request,
+ Map<String, String> additionalHeaders)
+ throws AuthFailureError {
+ requestParameters.putAllHeaders(additionalHeaders);
+ // Request.getHeaders() takes precedence over the given additional (cache) headers).
+ requestParameters.putAllHeaders(request.getHeaders());
+ }
+
+ /** Sets the UploadDataProvider of the UrlRequest.Builder */
+ private void addBodyIfExists(
+ CurlLoggedRequestParameters requestParameters,
+ String contentType,
+ @Nullable byte[] body) {
+ requestParameters.setBody(contentType, body);
+ }
+
+ /** Helper method that maps Volley's request priority to Cronet's */
+ private int getPriority(Request<?> request) {
+ switch (request.getPriority()) {
+ case LOW:
+ return UrlRequest.Builder.REQUEST_PRIORITY_LOW;
+ case HIGH:
+ case IMMEDIATE:
+ return UrlRequest.Builder.REQUEST_PRIORITY_HIGHEST;
+ case NORMAL:
+ default:
+ return UrlRequest.Builder.REQUEST_PRIORITY_MEDIUM;
+ }
+ }
+
+ private int getContentLength(UrlResponseInfo urlResponseInfo) {
+ List<String> content = urlResponseInfo.getAllHeaders().get("Content-Length");
+ if (content == null) {
+ return 1024;
+ } else {
+ return Integer.parseInt(content.get(0));
+ }
+ }
+
+ private String generateCurlCommand(String url, CurlLoggedRequestParameters requestParameters) {
+ StringBuilder builder = new StringBuilder("curl ");
+
+ // HTTP method
+ builder.append("-X ").append(requestParameters.getHttpMethod()).append(" ");
+
+ // Request headers
+ for (Map.Entry<String, String> header : requestParameters.getHeaders().entrySet()) {
+ builder.append("--header \"").append(header.getKey()).append(": ");
+ if (!mLogAuthTokensInCurlCommands
+ && ("Authorization".equals(header.getKey())
+ || "Cookie".equals(header.getKey()))) {
+ builder.append("[REDACTED]");
+ } else {
+ builder.append(header.getValue());
+ }
+ builder.append("\" ");
+ }
+
+ // URL
+ builder.append("\"").append(url).append("\"");
+
+ // Request body (if any)
+ if (requestParameters.getBody() != null) {
+ if (requestParameters.getBody().length >= 1024) {
+ builder.append(" [REQUEST BODY TOO LARGE TO INCLUDE]");
+ } else if (isBinaryContentForLogging(requestParameters)) {
+ String base64 = Base64.encodeToString(requestParameters.getBody(), Base64.NO_WRAP);
+ builder.insert(0, "echo '" + base64 + "' | base64 -d > /tmp/$$.bin; ")
+ .append(" --data-binary @/tmp/$$.bin");
+ } else {
+ // Just assume the request body is UTF-8 since this is for debugging.
+ try {
+ builder.append(" --data-ascii \"")
+ .append(new String(requestParameters.getBody(), "UTF-8"))
+ .append("\"");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Could not encode to UTF-8", e);
+ }
+ }
+ }
+
+ return builder.toString();
+ }
+
+ /** Rough heuristic to determine whether the request body is binary, for logging purposes. */
+ private boolean isBinaryContentForLogging(CurlLoggedRequestParameters requestParameters) {
+ // Check to see if the content is gzip compressed - this means it should be treated as
+ // binary content regardless of the content type.
+ String contentEncoding = requestParameters.getHeaders().get("Content-Encoding");
+ if (contentEncoding != null) {
+ String[] encodings = TextUtils.split(contentEncoding, ",");
+ for (String encoding : encodings) {
+ if ("gzip".equals(encoding.trim())) {
+ return true;
+ }
+ }
+ }
+
+ // If the content type is a known text type, treat it as text content.
+ String contentType = requestParameters.getHeaders().get("Content-Type");
+ if (contentType != null) {
+ return !contentType.startsWith("text/")
+ && !contentType.startsWith("application/xml")
+ && !contentType.startsWith("application/json");
+ }
+
+ // Otherwise, assume it is binary content.
+ return true;
+ }
+
+ /**
+ * Builder is used to build an instance of {@link CronetHttpStack} from values configured by the
+ * setters.
+ */
+ public static class Builder {
+ private static final int DEFAULT_POOL_SIZE = 4096;
+ private CronetEngine mCronetEngine;
+ private final Context context;
+ private ByteArrayPool mPool;
+ private UrlRewriter mUrlRewriter;
+ private RequestListener mRequestListener;
+ private boolean mCurlLoggingEnabled;
+ private CurlCommandLogger mCurlCommandLogger;
+ private boolean mLogAuthTokensInCurlCommands;
+
+ public Builder(Context context) {
+ this.context = context;
+ }
+
+ /** Sets the CronetEngine to be used. Defaults to a vanialla CronetEngine. */
+ public Builder setCronetEngine(CronetEngine engine) {
+ mCronetEngine = engine;
+ return this;
+ }
+
+ /** Sets the ByteArrayPool to be used. Defaults to a new pool with 4096 bytes. */
+ public Builder setPool(ByteArrayPool pool) {
+ mPool = pool;
+ return this;
+ }
+
+ /** Sets the UrlRewriter to be used. Default is to return the original string. */
+ public Builder setUrlRewriter(UrlRewriter urlRewriter) {
+ mUrlRewriter = urlRewriter;
+ return this;
+ }
+
+ /** Set the optional RequestListener to be used. */
+ public Builder setRequestListener(RequestListener requestListener) {
+ mRequestListener = requestListener;
+ return this;
+ }
+
+ /**
+ * Sets whether cURL logging should be enabled for debugging purposes.
+ *
+ * <p>When enabled, for each request dispatched to the network, a roughly-equivalent cURL
+ * command will be logged to logcat.
+ *
+ * <p>The command may be missing some headers that are added by Cronet automatically, and
+ * the full request body may not be included if it is too large. To inspect the full
+ * requests and responses, see {@code CronetEngine#startNetLogToFile}.
+ *
+ * <p>WARNING: This is only intended for debugging purposes and should never be enabled on
+ * production devices.
+ *
+ * @see #setCurlCommandLogger(CurlCommandLogger)
+ * @see #setLogAuthTokensInCurlCommands(boolean)
+ */
+ public Builder setCurlLoggingEnabled(boolean curlLoggingEnabled) {
+ mCurlLoggingEnabled = curlLoggingEnabled;
+ return this;
+ }
+
+ /**
+ * Sets the function used to log cURL commands.
+ *
+ * <p>Allows customization of the logging performed when cURL logging is enabled.
+ *
+ * <p>By default, when cURL logging is enabled, cURL commands are logged using {@link
+ * VolleyLog#v}, e.g. at the verbose log level with the same log tag used by the rest of
+ * Volley. This function may optionally be invoked to provide a custom logger.
+ *
+ * @see #setCurlLoggingEnabled(boolean)
+ */
+ public Builder setCurlCommandLogger(CurlCommandLogger curlCommandLogger) {
+ mCurlCommandLogger = curlCommandLogger;
+ return this;
+ }
+
+ /**
+ * Sets whether to log known auth tokens in cURL commands, or redact them.
+ *
+ * <p>By default, headers which may contain auth tokens (e.g. Authorization or Cookie) will
+ * have their values redacted. Passing true to this method will disable this redaction and
+ * log the values of these headers.
+ *
+ * <p>This heuristic is not perfect; tokens that are logged in unknown headers, or in the
+ * request body itself, will not be redacted as they cannot be detected generically.
+ *
+ * @see #setCurlLoggingEnabled(boolean)
+ */
+ public Builder setLogAuthTokensInCurlCommands(boolean logAuthTokensInCurlCommands) {
+ mLogAuthTokensInCurlCommands = logAuthTokensInCurlCommands;
+ return this;
+ }
+
+ public CronetHttpStack build() {
+ if (mCronetEngine == null) {
+ mCronetEngine = new CronetEngine.Builder(context).build();
+ }
+ if (mUrlRewriter == null) {
+ mUrlRewriter =
+ new UrlRewriter() {
+ @Override
+ public String rewriteUrl(String originalUrl) {
+ return originalUrl;
+ }
+ };
+ }
+ if (mRequestListener == null) {
+ mRequestListener = new RequestListener() {};
+ }
+ if (mPool == null) {
+ mPool = new ByteArrayPool(DEFAULT_POOL_SIZE);
+ }
+ if (mCurlCommandLogger == null) {
+ mCurlCommandLogger =
+ new CurlCommandLogger() {
+ @Override
+ public void logCurlCommand(String curlCommand) {
+ VolleyLog.v(curlCommand);
+ }
+ };
+ }
+ return new CronetHttpStack(
+ mCronetEngine,
+ mPool,
+ mUrlRewriter,
+ mRequestListener,
+ mCurlLoggingEnabled,
+ mCurlCommandLogger,
+ mLogAuthTokensInCurlCommands);
+ }
+ }
+
+ /** Callback interface allowing clients to intercept different parts of the request flow. */
+ public abstract static class RequestListener {
+ private CronetHttpStack mStack;
+
+ void initialize(CronetHttpStack stack) {
+ mStack = stack;
+ }
+
+ /**
+ * Called when a request is prepared and about to be sent over the network.
+ *
+ * <p>Clients may use this callback to customize UrlRequests before they are dispatched,
+ * e.g. to enable socket tagging or request finished listeners.
+ */
+ public void onRequestPrepared(Request<?> request, UrlRequest.Builder requestBuilder) {}
+
+ /** @see AsyncHttpStack#getNonBlockingExecutor() */
+ protected Executor getNonBlockingExecutor() {
+ return mStack.getNonBlockingExecutor();
+ }
+
+ /** @see AsyncHttpStack#getBlockingExecutor() */
+ protected Executor getBlockingExecutor() {
+ return mStack.getBlockingExecutor();
+ }
+ }
+
+ /**
+ * Interface for logging cURL commands for requests.
+ *
+ * @see Builder#setCurlCommandLogger(CurlCommandLogger)
+ */
+ public interface CurlCommandLogger {
+ /** Log the given cURL command. */
+ void logCurlCommand(String curlCommand);
+ }
+
+ /**
+ * Internal container class for request parameters that impact logged cURL commands.
+ *
+ * <p>When cURL logging is enabled, an equivalent cURL command to a given request must be
+ * generated and logged. However, the Cronet UrlRequest object is write-only. So, we write any
+ * relevant parameters into this read-write container so they can be referenced when generating
+ * the cURL command (if needed) and then merged into the UrlRequest.
+ */
+ private static class CurlLoggedRequestParameters {
+ private final TreeMap<String, String> mHeaders =
+ new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ private String mHttpMethod;
+ @Nullable private byte[] mBody;
+
+ /**
+ * Return the headers to be used for the request.
+ *
+ * <p>The returned map is case-insensitive.
+ */
+ TreeMap<String, String> getHeaders() {
+ return mHeaders;
+ }
+
+ /** Apply all the headers in the given map to the request. */
+ void putAllHeaders(Map<String, String> headers) {
+ mHeaders.putAll(headers);
+ }
+
+ String getHttpMethod() {
+ return mHttpMethod;
+ }
+
+ void setHttpMethod(String httpMethod) {
+ mHttpMethod = httpMethod;
+ }
+
+ @Nullable
+ byte[] getBody() {
+ return mBody;
+ }
+
+ void setBody(String contentType, @Nullable byte[] body) {
+ mBody = body;
+ if (body != null && !mHeaders.containsKey(HttpHeaderParser.HEADER_CONTENT_TYPE)) {
+ // Set the content-type unless it was already set (by Request#getHeaders).
+ mHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, contentType);
+ }
+ }
+
+ void applyToRequest(UrlRequest.Builder builder, ExecutorService nonBlockingExecutor) {
+ for (Map.Entry<String, String> header : mHeaders.entrySet()) {
+ builder.addHeader(header.getKey(), header.getValue());
+ }
+ builder.setHttpMethod(mHttpMethod);
+ if (mBody != null) {
+ UploadDataProvider dataProvider = UploadDataProviders.create(mBody);
+ builder.setUploadDataProvider(dataProvider, nonBlockingExecutor);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java b/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java
new file mode 100644
index 0000000..bafab8c
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2020 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 androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.VolleyLog;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Asynchronous extension of the {@link BaseHttpStack} class. */
+public abstract class AsyncHttpStack extends BaseHttpStack {
+ private ExecutorService mBlockingExecutor;
+ private ExecutorService mNonBlockingExecutor;
+
+ public interface OnRequestComplete {
+ /** Invoked when the stack successfully completes a request. */
+ void onSuccess(HttpResponse httpResponse);
+
+ /** Invoked when the stack throws an {@link AuthFailureError} during a request. */
+ void onAuthError(AuthFailureError authFailureError);
+
+ /** Invoked when the stack throws an {@link IOException} during a request. */
+ void onError(IOException ioException);
+ }
+
+ /**
+ * Makes an HTTP request with the given parameters, and calls the {@link OnRequestComplete}
+ * callback, with either the {@link HttpResponse} or error that was thrown.
+ *
+ * @param request to perform
+ * @param additionalHeaders to be sent together with {@link Request#getHeaders()}
+ * @param callback to be called after retrieving the {@link HttpResponse} or throwing an error.
+ */
+ public abstract void executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders, OnRequestComplete callback);
+
+ /**
+ * This method sets the non blocking executor to be used by the stack for non-blocking tasks.
+ * This method must be called before executing any requests.
+ */
+ @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+ public void setNonBlockingExecutor(ExecutorService executor) {
+ mNonBlockingExecutor = executor;
+ }
+
+ /**
+ * This method sets the blocking executor to be used by the stack for potentially blocking
+ * tasks. This method must be called before executing any requests.
+ */
+ @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+ public void setBlockingExecutor(ExecutorService executor) {
+ mBlockingExecutor = executor;
+ }
+
+ /** Gets blocking executor to perform any potentially blocking tasks. */
+ protected ExecutorService getBlockingExecutor() {
+ return mBlockingExecutor;
+ }
+
+ /** Gets non-blocking executor to perform any non-blocking tasks. */
+ protected ExecutorService getNonBlockingExecutor() {
+ return mNonBlockingExecutor;
+ }
+
+ /**
+ * Performs an HTTP request with the given parameters.
+ *
+ * @param request the request to perform
+ * @param additionalHeaders additional headers to be sent together with {@link
+ * Request#getHeaders()}
+ * @return the {@link HttpResponse}
+ * @throws IOException if an I/O error occurs during the request
+ * @throws AuthFailureError if an authentication failure occurs during the request
+ */
+ @Override
+ public final HttpResponse executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference<Response> entry = new AtomicReference<>();
+ executeRequest(
+ request,
+ additionalHeaders,
+ new OnRequestComplete() {
+ @Override
+ public void onSuccess(HttpResponse httpResponse) {
+ Response response =
+ new Response(
+ httpResponse,
+ /* ioException= */ null,
+ /* authFailureError= */ null);
+ entry.set(response);
+ latch.countDown();
+ }
+
+ @Override
+ public void onAuthError(AuthFailureError authFailureError) {
+ Response response =
+ new Response(
+ /* httpResponse= */ null,
+ /* ioException= */ null,
+ authFailureError);
+ entry.set(response);
+ latch.countDown();
+ }
+
+ @Override
+ public void onError(IOException ioException) {
+ Response response =
+ new Response(
+ /* httpResponse= */ null,
+ ioException,
+ /* authFailureError= */ null);
+ entry.set(response);
+ latch.countDown();
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ VolleyLog.e(e, "while waiting for CountDownLatch");
+ Thread.currentThread().interrupt();
+ throw new InterruptedIOException(e.toString());
+ }
+ Response response = entry.get();
+ if (response.httpResponse != null) {
+ return response.httpResponse;
+ } else if (response.ioException != null) {
+ throw response.ioException;
+ } else {
+ throw response.authFailureError;
+ }
+ }
+
+ private static class Response {
+ HttpResponse httpResponse;
+ IOException ioException;
+ AuthFailureError authFailureError;
+
+ private Response(
+ @Nullable HttpResponse httpResponse,
+ @Nullable IOException ioException,
+ @Nullable AuthFailureError authFailureError) {
+ this.httpResponse = httpResponse;
+ this.ioException = ioException;
+ this.authFailureError = authFailureError;
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java b/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java
new file mode 100644
index 0000000..55892a0
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2020 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 static com.android.volley.toolbox.NetworkUtility.logSlowRequests;
+
+import android.os.SystemClock;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import com.android.volley.AsyncNetwork;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.NetworkResponse;
+import com.android.volley.Request;
+import com.android.volley.RequestTask;
+import com.android.volley.VolleyError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+
+/** A network performing Volley requests over an {@link HttpStack}. */
+public class BasicAsyncNetwork extends AsyncNetwork {
+
+ private final AsyncHttpStack mAsyncStack;
+ private final ByteArrayPool mPool;
+
+ /**
+ * @param httpStack HTTP stack to be used
+ * @param pool a buffer pool that improves GC performance in copy operations
+ */
+ private BasicAsyncNetwork(AsyncHttpStack httpStack, ByteArrayPool pool) {
+ mAsyncStack = httpStack;
+ mPool = pool;
+ }
+
+ @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+ @Override
+ public void setBlockingExecutor(ExecutorService executor) {
+ super.setBlockingExecutor(executor);
+ mAsyncStack.setBlockingExecutor(executor);
+ }
+
+ @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+ @Override
+ public void setNonBlockingExecutor(ExecutorService executor) {
+ super.setNonBlockingExecutor(executor);
+ mAsyncStack.setNonBlockingExecutor(executor);
+ }
+
+ /* Method to be called after a successful network request */
+ private void onRequestSucceeded(
+ final Request<?> request,
+ final long requestStartMs,
+ final HttpResponse httpResponse,
+ final OnRequestComplete callback) {
+ final int statusCode = httpResponse.getStatusCode();
+ final List<Header> responseHeaders = httpResponse.getHeaders();
+ // Handle cache validation.
+ if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
+ long requestDuration = SystemClock.elapsedRealtime() - requestStartMs;
+ callback.onSuccess(
+ NetworkUtility.getNotModifiedNetworkResponse(
+ request, requestDuration, responseHeaders));
+ return;
+ }
+
+ byte[] responseContents = httpResponse.getContentBytes();
+ if (responseContents == null && httpResponse.getContent() == null) {
+ // Add 0 byte response as a way of honestly representing a
+ // no-content request.
+ responseContents = new byte[0];
+ }
+
+ if (responseContents != null) {
+ onResponseRead(
+ requestStartMs,
+ statusCode,
+ httpResponse,
+ request,
+ callback,
+ responseHeaders,
+ responseContents);
+ return;
+ }
+
+ // The underlying AsyncHttpStack does not support asynchronous reading of the response into
+ // a byte array, so we need to submit a blocking task to copy the response from the
+ // InputStream instead.
+ final InputStream inputStream = httpResponse.getContent();
+ getBlockingExecutor()
+ .execute(
+ new ResponseParsingTask<>(
+ inputStream,
+ httpResponse,
+ request,
+ callback,
+ requestStartMs,
+ responseHeaders,
+ statusCode));
+ }
+
+ /* Method to be called after a failed network request */
+ private void onRequestFailed(
+ Request<?> request,
+ OnRequestComplete callback,
+ IOException exception,
+ long requestStartMs,
+ @Nullable HttpResponse httpResponse,
+ @Nullable byte[] responseContents) {
+ try {
+ NetworkUtility.handleException(
+ request, exception, requestStartMs, httpResponse, responseContents);
+ } catch (VolleyError volleyError) {
+ callback.onError(volleyError);
+ return;
+ }
+ performRequest(request, callback);
+ }
+
+ @Override
+ public void performRequest(final Request<?> request, final OnRequestComplete callback) {
+ if (getBlockingExecutor() == null) {
+ throw new IllegalStateException(
+ "mBlockingExecuter must be set before making a request");
+ }
+ final long requestStartMs = SystemClock.elapsedRealtime();
+ // Gather headers.
+ final Map<String, String> additionalRequestHeaders =
+ HttpHeaderParser.getCacheHeaders(request.getCacheEntry());
+ mAsyncStack.executeRequest(
+ request,
+ additionalRequestHeaders,
+ new AsyncHttpStack.OnRequestComplete() {
+ @Override
+ public void onSuccess(HttpResponse httpResponse) {
+ onRequestSucceeded(request, requestStartMs, httpResponse, callback);
+ }
+
+ @Override
+ public void onAuthError(AuthFailureError authFailureError) {
+ callback.onError(authFailureError);
+ }
+
+ @Override
+ public void onError(IOException ioException) {
+ onRequestFailed(
+ request,
+ callback,
+ ioException,
+ requestStartMs,
+ /* httpResponse= */ null,
+ /* responseContents= */ null);
+ }
+ });
+ }
+
+ /* Helper method that determines what to do after byte[] is received */
+ private void onResponseRead(
+ long requestStartMs,
+ int statusCode,
+ HttpResponse httpResponse,
+ Request<?> request,
+ OnRequestComplete callback,
+ List<Header> responseHeaders,
+ byte[] responseContents) {
+ // if the request is slow, log it.
+ long requestLifetime = SystemClock.elapsedRealtime() - requestStartMs;
+ logSlowRequests(requestLifetime, request, responseContents, statusCode);
+
+ if (statusCode < 200 || statusCode > 299) {
+ onRequestFailed(
+ request,
+ callback,
+ new IOException(),
+ requestStartMs,
+ httpResponse,
+ responseContents);
+ return;
+ }
+
+ callback.onSuccess(
+ new NetworkResponse(
+ statusCode,
+ responseContents,
+ /* notModified= */ false,
+ SystemClock.elapsedRealtime() - requestStartMs,
+ responseHeaders));
+ }
+
+ private class ResponseParsingTask<T> extends RequestTask<T> {
+ InputStream inputStream;
+ HttpResponse httpResponse;
+ Request<T> request;
+ OnRequestComplete callback;
+ long requestStartMs;
+ List<Header> responseHeaders;
+ int statusCode;
+
+ ResponseParsingTask(
+ InputStream inputStream,
+ HttpResponse httpResponse,
+ Request<T> request,
+ OnRequestComplete callback,
+ long requestStartMs,
+ List<Header> responseHeaders,
+ int statusCode) {
+ super(request);
+ this.inputStream = inputStream;
+ this.httpResponse = httpResponse;
+ this.request = request;
+ this.callback = callback;
+ this.requestStartMs = requestStartMs;
+ this.responseHeaders = responseHeaders;
+ this.statusCode = statusCode;
+ }
+
+ @Override
+ public void run() {
+ byte[] finalResponseContents;
+ try {
+ finalResponseContents =
+ NetworkUtility.inputStreamToBytes(
+ inputStream, httpResponse.getContentLength(), mPool);
+ } catch (IOException e) {
+ onRequestFailed(request, callback, e, requestStartMs, httpResponse, null);
+ return;
+ }
+ onResponseRead(
+ requestStartMs,
+ statusCode,
+ httpResponse,
+ request,
+ callback,
+ responseHeaders,
+ finalResponseContents);
+ }
+ }
+
+ /**
+ * Builder is used to build an instance of {@link BasicAsyncNetwork} from values configured by
+ * the setters.
+ */
+ public static class Builder {
+ private static final int DEFAULT_POOL_SIZE = 4096;
+ @NonNull private AsyncHttpStack mAsyncStack;
+ private ByteArrayPool mPool;
+
+ public Builder(@NonNull AsyncHttpStack httpStack) {
+ mAsyncStack = httpStack;
+ mPool = null;
+ }
+
+ /**
+ * Sets the ByteArrayPool to be used. If not set, it will default to a pool with the default
+ * pool size.
+ */
+ public Builder setPool(ByteArrayPool pool) {
+ mPool = pool;
+ return this;
+ }
+
+ /** Builds the {@link com.android.volley.toolbox.BasicAsyncNetwork} */
+ public BasicAsyncNetwork build() {
+ if (mPool == null) {
+ mPool = new ByteArrayPool(DEFAULT_POOL_SIZE);
+ }
+ return new BasicAsyncNetwork(mAsyncStack, mPool);
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
index b527cb9..06427fe 100644
--- a/src/main/java/com/android/volley/toolbox/BasicNetwork.java
+++ b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
@@ -17,41 +17,21 @@
package com.android.volley.toolbox;
import android.os.SystemClock;
-import com.android.volley.AuthFailureError;
-import com.android.volley.Cache;
-import com.android.volley.Cache.Entry;
-import com.android.volley.ClientError;
import com.android.volley.Header;
import com.android.volley.Network;
-import com.android.volley.NetworkError;
import com.android.volley.NetworkResponse;
-import com.android.volley.NoConnectionError;
import com.android.volley.Request;
-import com.android.volley.RetryPolicy;
-import com.android.volley.ServerError;
-import com.android.volley.TimeoutError;
import com.android.volley.VolleyError;
-import com.android.volley.VolleyLog;
import 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.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}. */
public class BasicNetwork implements Network {
- protected static final boolean DEBUG = VolleyLog.DEBUG;
-
- private static final int SLOW_REQUEST_THRESHOLD_MS = 3000;
-
private static final int DEFAULT_POOL_SIZE = 4096;
/**
@@ -119,37 +99,24 @@ public class BasicNetwork implements Network {
try {
// Gather headers.
Map<String, String> additionalRequestHeaders =
- getCacheHeaders(request.getCacheEntry());
+ HttpHeaderParser.getCacheHeaders(request.getCacheEntry());
httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);
int statusCode = httpResponse.getStatusCode();
responseHeaders = httpResponse.getHeaders();
// Handle cache validation.
if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
- Entry entry = request.getCacheEntry();
- if (entry == null) {
- return new NetworkResponse(
- HttpURLConnection.HTTP_NOT_MODIFIED,
- /* data= */ null,
- /* notModified= */ true,
- SystemClock.elapsedRealtime() - requestStart,
- responseHeaders);
- }
- // 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,
- /* notModified= */ true,
- SystemClock.elapsedRealtime() - requestStart,
- combinedHeaders);
+ long requestDuration = SystemClock.elapsedRealtime() - requestStart;
+ return NetworkUtility.getNotModifiedNetworkResponse(
+ request, requestDuration, responseHeaders);
}
// Some responses such as 204s do not have content. We must check.
InputStream inputStream = httpResponse.getContent();
if (inputStream != null) {
responseContents =
- inputStreamToBytes(inputStream, httpResponse.getContentLength());
+ NetworkUtility.inputStreamToBytes(
+ inputStream, httpResponse.getContentLength(), mPool);
} else {
// Add 0 byte response as a way of honestly representing a
// no-content request.
@@ -158,7 +125,8 @@ public class BasicNetwork implements Network {
// if the request is slow, log it.
long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
- logSlowRequests(requestLifetime, request, responseContents, statusCode);
+ NetworkUtility.logSlowRequests(
+ requestLifetime, request, responseContents, statusCode);
if (statusCode < 200 || statusCode > 299) {
throw new IOException();
@@ -169,141 +137,12 @@ public class BasicNetwork implements Network {
/* notModified= */ false,
SystemClock.elapsedRealtime() - requestStart,
responseHeaders);
- } catch (SocketTimeoutException e) {
- attemptRetryOnException("socket", request, new TimeoutError());
- } catch (MalformedURLException e) {
- throw new RuntimeException("Bad URL " + request.getUrl(), e);
- } catch (IOException e) {
- int statusCode;
- if (httpResponse != null) {
- 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,
- /* notModified= */ 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) {
- // Don't retry other client errors.
- throw new ClientError(networkResponse);
- } else if (statusCode >= 500 && statusCode <= 599) {
- if (request.shouldRetryServerErrors()) {
- attemptRetryOnException(
- "server", request, new ServerError(networkResponse));
- } else {
- throw new ServerError(networkResponse);
- }
- } else {
- // 3xx? No reason to retry.
- throw new ServerError(networkResponse);
- }
- } else {
- attemptRetryOnException("network", request, new NetworkError());
- }
- }
- }
- }
-
- /** Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. */
- private void logSlowRequests(
- long requestLifetime, Request<?> request, 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",
- statusCode,
- request.getRetryPolicy().getCurrentRetryCount());
- }
- }
-
- /**
- * Attempts to prepare the request for a retry. If there are no more attempts remaining in the
- * request's retry policy, a timeout exception is thrown.
- *
- * @param request The request to use.
- */
- private static void attemptRetryOnException(
- String logPrefix, Request<?> request, VolleyError exception) throws VolleyError {
- RetryPolicy retryPolicy = request.getRetryPolicy();
- int oldTimeout = request.getTimeoutMs();
-
- try {
- retryPolicy.retry(exception);
- } catch (VolleyError e) {
- request.addMarker(
- String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
- throw e;
- }
- request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
- }
-
- private Map<String, String> getCacheHeaders(Cache.Entry entry) {
- // If there's no cache entry, we're done.
- if (entry == null) {
- return Collections.emptyMap();
- }
-
- Map<String, String> headers = new HashMap<>();
-
- if (entry.etag != null) {
- headers.put("If-None-Match", entry.etag);
- }
-
- if (entry.lastModified > 0) {
- headers.put(
- "If-Modified-Since", HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified));
- }
-
- return headers;
- }
-
- protected void logError(String what, String url, long start) {
- long now = SystemClock.elapsedRealtime();
- VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url);
- }
-
- /** Reads the contents of an InputStream into a byte[]. */
- private byte[] inputStreamToBytes(InputStream in, int contentLength)
- throws IOException, ServerError {
- PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(mPool, contentLength);
- byte[] buffer = null;
- try {
- if (in == null) {
- throw new ServerError();
- }
- buffer = mPool.getBuf(1024);
- int count;
- while ((count = in.read(buffer)) != -1) {
- bytes.write(buffer, 0, count);
- }
- return bytes.toByteArray();
- } finally {
- try {
- // Close the InputStream and release the resources by "consuming the content".
- if (in != null) {
- in.close();
- }
} catch (IOException e) {
- // This can happen if there was an exception above that left the stream in
- // an invalid state.
- VolleyLog.v("Error occurred when closing InputStream");
+ // This will either throw an exception, breaking us from the loop, or will loop
+ // again and retry the request.
+ NetworkUtility.handleException(
+ request, e, requestStart, httpResponse, responseContents);
}
- mPool.returnBuf(buffer);
- bytes.close();
}
}
@@ -321,49 +160,4 @@ public class BasicNetwork implements Network {
}
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/FileSupplier.java b/src/main/java/com/android/volley/toolbox/FileSupplier.java
new file mode 100644
index 0000000..70898a6
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/FileSupplier.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import java.io.File;
+
+/** Represents a supplier for {@link File}s. */
+public interface FileSupplier {
+ File get();
+}
diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
index 1b410af..0b29e80 100644
--- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
+++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
@@ -17,6 +17,8 @@
package com.android.volley.toolbox;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
import com.android.volley.Cache;
import com.android.volley.Header;
import com.android.volley.NetworkResponse;
@@ -24,17 +26,22 @@ import com.android.volley.VolleyLog;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Date;
+import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.TimeZone;
import java.util.TreeMap;
+import java.util.TreeSet;
/** Utility methods for parsing HTTP headers. */
public class HttpHeaderParser {
- static final String HEADER_CONTENT_TYPE = "Content-Type";
+ @RestrictTo({Scope.LIBRARY_GROUP})
+ public static final String HEADER_CONTENT_TYPE = "Content-Type";
private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1";
@@ -226,4 +233,69 @@ public class HttpHeaderParser {
}
return allHeaders;
}
+
+ /**
+ * 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.
+ */
+ static List<Header> combineHeaders(List<Header> responseHeaders, Cache.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;
+ }
+
+ static Map<String, String> getCacheHeaders(Cache.Entry entry) {
+ // If there's no cache entry, we're done.
+ if (entry == null) {
+ return Collections.emptyMap();
+ }
+
+ Map<String, String> headers = new HashMap<>();
+
+ if (entry.etag != null) {
+ headers.put("If-None-Match", entry.etag);
+ }
+
+ if (entry.lastModified > 0) {
+ headers.put(
+ "If-Modified-Since", HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified));
+ }
+
+ return headers;
+ }
}
diff --git a/src/main/java/com/android/volley/toolbox/HttpResponse.java b/src/main/java/com/android/volley/toolbox/HttpResponse.java
index 9a9294f..595f926 100644
--- a/src/main/java/com/android/volley/toolbox/HttpResponse.java
+++ b/src/main/java/com/android/volley/toolbox/HttpResponse.java
@@ -15,7 +15,9 @@
*/
package com.android.volley.toolbox;
+import androidx.annotation.Nullable;
import com.android.volley.Header;
+import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
@@ -26,7 +28,8 @@ public final class HttpResponse {
private final int mStatusCode;
private final List<Header> mHeaders;
private final int mContentLength;
- private final InputStream mContent;
+ @Nullable private final InputStream mContent;
+ @Nullable private final byte[] mContentBytes;
/**
* Construct a new HttpResponse for an empty response body.
@@ -53,6 +56,23 @@ public final class HttpResponse {
mHeaders = headers;
mContentLength = contentLength;
mContent = content;
+ mContentBytes = null;
+ }
+
+ /**
+ * Construct a new HttpResponse.
+ *
+ * @param statusCode the HTTP status code of the response
+ * @param headers the response headers
+ * @param contentBytes a byte[] of the response content. This is an optimization for HTTP stacks
+ * that natively support returning a byte[].
+ */
+ public HttpResponse(int statusCode, List<Header> headers, byte[] contentBytes) {
+ mStatusCode = statusCode;
+ mHeaders = headers;
+ mContentLength = contentBytes.length;
+ mContentBytes = contentBytes;
+ mContent = null;
}
/** Returns the HTTP status code of the response. */
@@ -71,10 +91,28 @@ public final class HttpResponse {
}
/**
+ * If a byte[] was already provided by an HTTP stack that natively supports returning one, this
+ * method will return that byte[] as an optimization over copying the bytes from an input
+ * stream. It may return null, even if the response has content, as long as mContent is
+ * provided.
+ */
+ @Nullable
+ public final byte[] getContentBytes() {
+ return mContentBytes;
+ }
+
+ /**
* Returns an {@link InputStream} of the response content. May be null to indicate that the
* response has no content.
*/
+ @Nullable
public final InputStream getContent() {
- return mContent;
+ if (mContent != null) {
+ return mContent;
+ } else if (mContentBytes != null) {
+ return new ByteArrayInputStream(mContentBytes);
+ } else {
+ return null;
+ }
}
}
diff --git a/src/main/java/com/android/volley/toolbox/HurlStack.java b/src/main/java/com/android/volley/toolbox/HurlStack.java
index 9c38023..35c6a72 100644
--- a/src/main/java/com/android/volley/toolbox/HurlStack.java
+++ b/src/main/java/com/android/volley/toolbox/HurlStack.java
@@ -41,13 +41,7 @@ public class HurlStack extends BaseHttpStack {
private static final int HTTP_CONTINUE = 100;
/** An interface for transforming URLs before use. */
- public interface UrlRewriter {
- /**
- * Returns a URL to use instead of the provided one, or null to indicate this URL should not
- * be used at all.
- */
- String rewriteUrl(String originalUrl);
- }
+ public interface UrlRewriter extends com.android.volley.toolbox.UrlRewriter {}
private final UrlRewriter mUrlRewriter;
private final SSLSocketFactory mSslSocketFactory;
diff --git a/src/main/java/com/android/volley/toolbox/NetworkUtility.java b/src/main/java/com/android/volley/toolbox/NetworkUtility.java
new file mode 100644
index 0000000..44d5904
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/NetworkUtility.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import android.os.SystemClock;
+import androidx.annotation.Nullable;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Cache;
+import com.android.volley.ClientError;
+import com.android.volley.Header;
+import com.android.volley.NetworkError;
+import com.android.volley.NetworkResponse;
+import com.android.volley.NoConnectionError;
+import com.android.volley.Request;
+import com.android.volley.RetryPolicy;
+import com.android.volley.ServerError;
+import com.android.volley.TimeoutError;
+import com.android.volley.VolleyError;
+import com.android.volley.VolleyLog;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.util.List;
+
+/**
+ * Utility class for methods that are shared between {@link BasicNetwork} and {@link
+ * BasicAsyncNetwork}
+ */
+public final class NetworkUtility {
+ private static final int SLOW_REQUEST_THRESHOLD_MS = 3000;
+
+ private NetworkUtility() {}
+
+ /** Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. */
+ static void logSlowRequests(
+ long requestLifetime, Request<?> request, byte[] responseContents, int statusCode) {
+ if (VolleyLog.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",
+ statusCode,
+ request.getRetryPolicy().getCurrentRetryCount());
+ }
+ }
+
+ static NetworkResponse getNotModifiedNetworkResponse(
+ Request<?> request, long requestDuration, List<Header> responseHeaders) {
+ Cache.Entry entry = request.getCacheEntry();
+ if (entry == null) {
+ return new NetworkResponse(
+ HttpURLConnection.HTTP_NOT_MODIFIED,
+ /* data= */ null,
+ /* notModified= */ true,
+ requestDuration,
+ responseHeaders);
+ }
+ // Combine cached and response headers so the response will be complete.
+ List<Header> combinedHeaders = HttpHeaderParser.combineHeaders(responseHeaders, entry);
+ return new NetworkResponse(
+ HttpURLConnection.HTTP_NOT_MODIFIED,
+ entry.data,
+ /* notModified= */ true,
+ requestDuration,
+ combinedHeaders);
+ }
+
+ /** Reads the contents of an InputStream into a byte[]. */
+ static byte[] inputStreamToBytes(InputStream in, int contentLength, ByteArrayPool pool)
+ throws IOException {
+ PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(pool, contentLength);
+ byte[] buffer = null;
+ try {
+ buffer = pool.getBuf(1024);
+ int count;
+ while ((count = in.read(buffer)) != -1) {
+ bytes.write(buffer, 0, count);
+ }
+ return bytes.toByteArray();
+ } finally {
+ try {
+ // Close the InputStream and release the resources by "consuming the content".
+ if (in != null) {
+ in.close();
+ }
+ } catch (IOException e) {
+ // This can happen if there was an exception above that left the stream in
+ // an invalid state.
+ VolleyLog.v("Error occurred when closing InputStream");
+ }
+ pool.returnBuf(buffer);
+ bytes.close();
+ }
+ }
+
+ /**
+ * Attempts to prepare the request for a retry. If there are no more attempts remaining in the
+ * request's retry policy, a timeout exception is thrown.
+ *
+ * @param request The request to use.
+ */
+ private static void attemptRetryOnException(
+ final String logPrefix, final Request<?> request, final VolleyError exception)
+ throws VolleyError {
+ final RetryPolicy retryPolicy = request.getRetryPolicy();
+ final int oldTimeout = request.getTimeoutMs();
+ try {
+ retryPolicy.retry(exception);
+ } catch (VolleyError e) {
+ request.addMarker(
+ String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
+ throw e;
+ }
+ request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
+ }
+
+ /**
+ * Based on the exception thrown, decides whether to attempt to retry, or to throw the error.
+ * Also handles logging.
+ */
+ static void handleException(
+ Request<?> request,
+ IOException exception,
+ long requestStartMs,
+ @Nullable HttpResponse httpResponse,
+ @Nullable byte[] responseContents)
+ throws VolleyError {
+ if (exception instanceof SocketTimeoutException) {
+ attemptRetryOnException("socket", request, new TimeoutError());
+ } else if (exception instanceof MalformedURLException) {
+ throw new RuntimeException("Bad URL " + request.getUrl(), exception);
+ } else {
+ int statusCode;
+ if (httpResponse != null) {
+ statusCode = httpResponse.getStatusCode();
+ } else {
+ if (request.shouldRetryConnectionErrors()) {
+ attemptRetryOnException("connection", request, new NoConnectionError());
+ return;
+ } else {
+ throw new NoConnectionError(exception);
+ }
+ }
+ VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
+ NetworkResponse networkResponse;
+ if (responseContents != null) {
+ List<Header> responseHeaders;
+ responseHeaders = httpResponse.getHeaders();
+ networkResponse =
+ new NetworkResponse(
+ statusCode,
+ responseContents,
+ /* notModified= */ false,
+ SystemClock.elapsedRealtime() - requestStartMs,
+ responseHeaders);
+ if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED
+ || statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
+ attemptRetryOnException("auth", request, new AuthFailureError(networkResponse));
+ } else if (statusCode >= 400 && statusCode <= 499) {
+ // Don't retry other client errors.
+ throw new ClientError(networkResponse);
+ } else if (statusCode >= 500 && statusCode <= 599) {
+ if (request.shouldRetryServerErrors()) {
+ attemptRetryOnException(
+ "server", request, new ServerError(networkResponse));
+ } else {
+ throw new ServerError(networkResponse);
+ }
+ } else {
+ // 3xx? No reason to retry.
+ throw new ServerError(networkResponse);
+ }
+ } else {
+ attemptRetryOnException("network", request, new NetworkError());
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/NoAsyncCache.java b/src/main/java/com/android/volley/toolbox/NoAsyncCache.java
new file mode 100644
index 0000000..aa4aeea
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/NoAsyncCache.java
@@ -0,0 +1,37 @@
+package com.android.volley.toolbox;
+
+import com.android.volley.AsyncCache;
+import com.android.volley.Cache;
+
+/** An AsyncCache that doesn't cache anything. */
+public class NoAsyncCache extends AsyncCache {
+ @Override
+ public void get(String key, OnGetCompleteCallback callback) {
+ callback.onGetComplete(null);
+ }
+
+ @Override
+ public void put(String key, Cache.Entry entry, OnWriteCompleteCallback callback) {
+ callback.onWriteComplete();
+ }
+
+ @Override
+ public void clear(OnWriteCompleteCallback callback) {
+ callback.onWriteComplete();
+ }
+
+ @Override
+ public void initialize(OnWriteCompleteCallback callback) {
+ callback.onWriteComplete();
+ }
+
+ @Override
+ public void invalidate(String key, boolean fullExpire, OnWriteCompleteCallback callback) {
+ callback.onWriteComplete();
+ }
+
+ @Override
+ public void remove(String key, OnWriteCompleteCallback callback) {
+ callback.onWriteComplete();
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/UrlRewriter.java b/src/main/java/com/android/volley/toolbox/UrlRewriter.java
new file mode 100644
index 0000000..8bbb770
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/UrlRewriter.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 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 androidx.annotation.Nullable;
+
+/** An interface for transforming URLs before use. */
+public interface UrlRewriter {
+ /**
+ * Returns a URL to use instead of the provided one, or null to indicate this URL should not be
+ * used at all.
+ */
+ @Nullable
+ String rewriteUrl(String originalUrl);
+}
diff --git a/src/test/java/com/android/volley/AsyncRequestQueueTest.java b/src/test/java/com/android/volley/AsyncRequestQueueTest.java
new file mode 100644
index 0000000..54ff0a1
--- /dev/null
+++ b/src/test/java/com/android/volley/AsyncRequestQueueTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.android.volley.mock.ShadowSystemClock;
+import com.android.volley.toolbox.NoAsyncCache;
+import com.android.volley.toolbox.StringRequest;
+import com.android.volley.utils.ImmediateResponseDelivery;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledExecutorService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Unit tests for AsyncRequestQueue, with all dependencies mocked out */
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowSystemClock.class})
+public class AsyncRequestQueueTest {
+
+ @Mock private AsyncNetwork mMockNetwork;
+ @Mock private ScheduledExecutorService mMockScheduledExecutor;
+ private AsyncRequestQueue queue;
+
+ @Before
+ public void setUp() throws Exception {
+ ResponseDelivery mDelivery = new ImmediateResponseDelivery();
+ initMocks(this);
+ queue =
+ new AsyncRequestQueue.Builder(mMockNetwork)
+ .setAsyncCache(new NoAsyncCache())
+ .setResponseDelivery(mDelivery)
+ .setExecutorFactory(
+ new AsyncRequestQueue.ExecutorFactory() {
+ @Override
+ public ExecutorService createNonBlockingExecutor(
+ BlockingQueue<Runnable> taskQueue) {
+ return MoreExecutors.newDirectExecutorService();
+ }
+
+ @Override
+ public ExecutorService createBlockingExecutor(
+ BlockingQueue<Runnable> taskQueue) {
+ return MoreExecutors.newDirectExecutorService();
+ }
+
+ @Override
+ public ScheduledExecutorService
+ createNonBlockingScheduledExecutor() {
+ return mMockScheduledExecutor;
+ }
+ })
+ .build();
+ }
+
+ @Test
+ public void cancelAll_onlyCorrectTag() throws Exception {
+ queue.start();
+ Object tagA = new Object();
+ Object tagB = new Object();
+ StringRequest req1 = mock(StringRequest.class);
+ when(req1.getTag()).thenReturn(tagA);
+ StringRequest req2 = mock(StringRequest.class);
+ when(req2.getTag()).thenReturn(tagB);
+ StringRequest req3 = mock(StringRequest.class);
+ when(req3.getTag()).thenReturn(tagA);
+ StringRequest req4 = mock(StringRequest.class);
+ when(req4.getTag()).thenReturn(tagA);
+
+ queue.add(req1); // A
+ queue.add(req2); // B
+ queue.add(req3); // A
+ queue.cancelAll(tagA);
+ queue.add(req4); // A
+
+ verify(req1).cancel(); // A cancelled
+ verify(req3).cancel(); // A cancelled
+ verify(req2, never()).cancel(); // B not cancelled
+ verify(req4, never()).cancel(); // A added after cancel not cancelled
+ queue.stop();
+ }
+
+ @Test
+ public void add_notifiesListener() throws Exception {
+ RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
+ queue.start();
+ queue.addRequestEventListener(listener);
+ StringRequest req = mock(StringRequest.class);
+
+ queue.add(req);
+
+ verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_QUEUED);
+ verifyNoMoreInteractions(listener);
+ queue.stop();
+ }
+
+ @Test
+ public void finish_notifiesListener() throws Exception {
+ RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
+ queue.start();
+ queue.addRequestEventListener(listener);
+ StringRequest req = mock(StringRequest.class);
+
+ queue.finish(req);
+
+ verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_FINISHED);
+ verifyNoMoreInteractions(listener);
+ queue.stop();
+ }
+
+ @Test
+ public void sendRequestEvent_notifiesListener() throws Exception {
+ StringRequest req = mock(StringRequest.class);
+ RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
+ queue.start();
+ queue.addRequestEventListener(listener);
+
+ queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED);
+
+ verify(listener)
+ .onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED);
+ verifyNoMoreInteractions(listener);
+ queue.stop();
+ }
+
+ @Test
+ public void removeRequestEventListener_removesListener() throws Exception {
+ StringRequest req = mock(StringRequest.class);
+ RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
+ queue.start();
+ queue.addRequestEventListener(listener);
+ queue.removeRequestEventListener(listener);
+
+ queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED);
+
+ verifyNoMoreInteractions(listener);
+ queue.stop();
+ }
+}
diff --git a/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java b/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java
new file mode 100644
index 0000000..cedb6ff
--- /dev/null
+++ b/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2020 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.cronet;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.volley.Header;
+import com.android.volley.cronet.CronetHttpStack.CurlCommandLogger;
+import com.android.volley.mock.TestRequest;
+import com.android.volley.toolbox.AsyncHttpStack.OnRequestComplete;
+import com.android.volley.toolbox.UrlRewriter;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import org.chromium.net.CronetEngine;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class CronetHttpStackTest {
+ @Mock private CurlCommandLogger mMockCurlCommandLogger;
+ @Mock private OnRequestComplete mMockOnRequestComplete;
+ @Mock private UrlRewriter mMockUrlRewriter;
+
+ // A fake would be ideal here, but Cronet doesn't (yet) provide one, and at the moment we aren't
+ // exercising the full response flow.
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private CronetEngine mMockCronetEngine;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void curlLogging_disabled() {
+ CronetHttpStack stack =
+ createStack(
+ new Consumer<CronetHttpStack.Builder>() {
+ @Override
+ public void accept(CronetHttpStack.Builder builder) {
+ // Default parameters should not enable cURL logging.
+ }
+ });
+
+ stack.executeRequest(
+ new TestRequest.Get(), ImmutableMap.<String, String>of(), mMockOnRequestComplete);
+
+ verify(mMockCurlCommandLogger, never()).logCurlCommand(anyString());
+ }
+
+ @Test
+ public void curlLogging_simpleTextRequest() {
+ CronetHttpStack stack =
+ createStack(
+ new Consumer<CronetHttpStack.Builder>() {
+ @Override
+ public void accept(CronetHttpStack.Builder builder) {
+ builder.setCurlLoggingEnabled(true);
+ }
+ });
+
+ stack.executeRequest(
+ new TestRequest.Get(), ImmutableMap.<String, String>of(), mMockOnRequestComplete);
+
+ ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
+ verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
+ assertEquals("curl -X GET \"http://foo.com\"", curlCommandCaptor.getValue());
+ }
+
+ @Test
+ public void curlLogging_rewrittenUrl() {
+ CronetHttpStack stack =
+ createStack(
+ new Consumer<CronetHttpStack.Builder>() {
+ @Override
+ public void accept(CronetHttpStack.Builder builder) {
+ builder.setCurlLoggingEnabled(true)
+ .setUrlRewriter(mMockUrlRewriter);
+ }
+ });
+ when(mMockUrlRewriter.rewriteUrl("http://foo.com")).thenReturn("http://bar.com");
+
+ stack.executeRequest(
+ new TestRequest.Get(), ImmutableMap.<String, String>of(), mMockOnRequestComplete);
+
+ ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
+ verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
+ assertEquals("curl -X GET \"http://bar.com\"", curlCommandCaptor.getValue());
+ }
+
+ @Test
+ public void curlLogging_headers_withoutTokens() {
+ CronetHttpStack stack =
+ createStack(
+ new Consumer<CronetHttpStack.Builder>() {
+ @Override
+ public void accept(CronetHttpStack.Builder builder) {
+ builder.setCurlLoggingEnabled(true);
+ }
+ });
+
+ stack.executeRequest(
+ new TestRequest.Delete() {
+ @Override
+ public Map<String, String> getHeaders() {
+ return ImmutableMap.of(
+ "SomeHeader", "SomeValue",
+ "Authorization", "SecretToken");
+ }
+ },
+ ImmutableMap.of("SomeOtherHeader", "SomeValue"),
+ mMockOnRequestComplete);
+
+ ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
+ verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
+ // NOTE: Header order is stable because the implementation uses a TreeMap.
+ assertEquals(
+ "curl -X DELETE --header \"Authorization: [REDACTED]\" "
+ + "--header \"SomeHeader: SomeValue\" "
+ + "--header \"SomeOtherHeader: SomeValue\" \"http://foo.com\"",
+ curlCommandCaptor.getValue());
+ }
+
+ @Test
+ public void curlLogging_headers_withTokens() {
+ CronetHttpStack stack =
+ createStack(
+ new Consumer<CronetHttpStack.Builder>() {
+ @Override
+ public void accept(CronetHttpStack.Builder builder) {
+ builder.setCurlLoggingEnabled(true)
+ .setLogAuthTokensInCurlCommands(true);
+ }
+ });
+
+ stack.executeRequest(
+ new TestRequest.Delete() {
+ @Override
+ public Map<String, String> getHeaders() {
+ return ImmutableMap.of(
+ "SomeHeader", "SomeValue",
+ "Authorization", "SecretToken");
+ }
+ },
+ ImmutableMap.of("SomeOtherHeader", "SomeValue"),
+ mMockOnRequestComplete);
+
+ ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
+ verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
+ // NOTE: Header order is stable because the implementation uses a TreeMap.
+ assertEquals(
+ "curl -X DELETE --header \"Authorization: SecretToken\" "
+ + "--header \"SomeHeader: SomeValue\" "
+ + "--header \"SomeOtherHeader: SomeValue\" \"http://foo.com\"",
+ curlCommandCaptor.getValue());
+ }
+
+ @Test
+ public void curlLogging_textRequest() {
+ CronetHttpStack stack =
+ createStack(
+ new Consumer<CronetHttpStack.Builder>() {
+ @Override
+ public void accept(CronetHttpStack.Builder builder) {
+ builder.setCurlLoggingEnabled(true);
+ }
+ });
+
+ stack.executeRequest(
+ new TestRequest.PostWithBody() {
+ @Override
+ public byte[] getBody() {
+ try {
+ return "hello".getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public String getBodyContentType() {
+ return "text/plain; charset=UTF-8";
+ }
+ },
+ ImmutableMap.<String, String>of(),
+ mMockOnRequestComplete);
+
+ ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
+ verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
+ assertEquals(
+ "curl -X POST "
+ + "--header \"Content-Type: text/plain; charset=UTF-8\" \"http://foo.com\" "
+ + "--data-ascii \"hello\"",
+ curlCommandCaptor.getValue());
+ }
+
+ @Test
+ public void curlLogging_gzipTextRequest() {
+ CronetHttpStack stack =
+ createStack(
+ new Consumer<CronetHttpStack.Builder>() {
+ @Override
+ public void accept(CronetHttpStack.Builder builder) {
+ builder.setCurlLoggingEnabled(true);
+ }
+ });
+
+ stack.executeRequest(
+ new TestRequest.PostWithBody() {
+ @Override
+ public byte[] getBody() {
+ return new byte[] {1, 2, 3, 4, 5};
+ }
+
+ @Override
+ public String getBodyContentType() {
+ return "text/plain";
+ }
+
+ @Override
+ public Map<String, String> getHeaders() {
+ return ImmutableMap.of("Content-Encoding", "gzip, identity");
+ }
+ },
+ ImmutableMap.<String, String>of(),
+ mMockOnRequestComplete);
+
+ ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
+ verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
+ assertEquals(
+ "echo 'AQIDBAU=' | base64 -d > /tmp/$$.bin; curl -X POST "
+ + "--header \"Content-Encoding: gzip, identity\" "
+ + "--header \"Content-Type: text/plain\" \"http://foo.com\" "
+ + "--data-binary @/tmp/$$.bin",
+ curlCommandCaptor.getValue());
+ }
+
+ @Test
+ public void curlLogging_binaryRequest() {
+ CronetHttpStack stack =
+ createStack(
+ new Consumer<CronetHttpStack.Builder>() {
+ @Override
+ public void accept(CronetHttpStack.Builder builder) {
+ builder.setCurlLoggingEnabled(true);
+ }
+ });
+
+ stack.executeRequest(
+ new TestRequest.PostWithBody() {
+ @Override
+ public byte[] getBody() {
+ return new byte[] {1, 2, 3, 4, 5};
+ }
+
+ @Override
+ public String getBodyContentType() {
+ return "application/octet-stream";
+ }
+ },
+ ImmutableMap.<String, String>of(),
+ mMockOnRequestComplete);
+
+ ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
+ verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
+ assertEquals(
+ "echo 'AQIDBAU=' | base64 -d > /tmp/$$.bin; curl -X POST "
+ + "--header \"Content-Type: application/octet-stream\" \"http://foo.com\" "
+ + "--data-binary @/tmp/$$.bin",
+ curlCommandCaptor.getValue());
+ }
+
+ @Test
+ public void curlLogging_largeRequest() {
+ CronetHttpStack stack =
+ createStack(
+ new Consumer<CronetHttpStack.Builder>() {
+ @Override
+ public void accept(CronetHttpStack.Builder builder) {
+ builder.setCurlLoggingEnabled(true);
+ }
+ });
+
+ stack.executeRequest(
+ new TestRequest.PostWithBody() {
+ @Override
+ public byte[] getBody() {
+ return new byte[2048];
+ }
+
+ @Override
+ public String getBodyContentType() {
+ return "application/octet-stream";
+ }
+ },
+ ImmutableMap.<String, String>of(),
+ mMockOnRequestComplete);
+
+ ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
+ verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
+ assertEquals(
+ "curl -X POST "
+ + "--header \"Content-Type: application/octet-stream\" \"http://foo.com\" "
+ + "[REQUEST BODY TOO LARGE TO INCLUDE]",
+ curlCommandCaptor.getValue());
+ }
+
+ @Test
+ public void getHeadersEmptyTest() {
+ List<Map.Entry<String, String>> list = new ArrayList<>();
+ List<Header> actual = CronetHttpStack.getHeaders(list);
+ List<Header> expected = new ArrayList<>();
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void getHeadersNonEmptyTest() {
+ Map<String, String> headers = new HashMap<>();
+ for (int i = 1; i < 5; i++) {
+ headers.put("key" + i, "value" + i);
+ }
+ List<Map.Entry<String, String>> list = new ArrayList<>(headers.entrySet());
+ List<Header> actual = CronetHttpStack.getHeaders(list);
+ List<Header> expected = new ArrayList<>();
+ for (int i = 1; i < 5; i++) {
+ expected.add(new Header("key" + i, "value" + i));
+ }
+ assertHeaderListsEqual(expected, actual);
+ }
+
+ private void assertHeaderListsEqual(List<Header> expected, List<Header> actual) {
+ assertEquals(expected.size(), actual.size());
+ for (int i = 0; i < expected.size(); i++) {
+ assertEquals(expected.get(i).getName(), actual.get(i).getName());
+ assertEquals(expected.get(i).getValue(), actual.get(i).getValue());
+ }
+ }
+
+ private CronetHttpStack createStack(Consumer<CronetHttpStack.Builder> stackEditor) {
+ CronetHttpStack.Builder builder =
+ new CronetHttpStack.Builder(RuntimeEnvironment.application)
+ .setCronetEngine(mMockCronetEngine)
+ .setCurlCommandLogger(mMockCurlCommandLogger);
+ stackEditor.accept(builder);
+ CronetHttpStack stack = builder.build();
+ stack.setBlockingExecutor(MoreExecutors.newDirectExecutorService());
+ stack.setNonBlockingExecutor(MoreExecutors.newDirectExecutorService());
+ return stack;
+ }
+}
diff --git a/src/test/java/com/android/volley/mock/MockAsyncStack.java b/src/test/java/com/android/volley/mock/MockAsyncStack.java
new file mode 100644
index 0000000..5ea8343
--- /dev/null
+++ b/src/test/java/com/android/volley/mock/MockAsyncStack.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2020 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 com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.toolbox.AsyncHttpStack;
+import com.android.volley.toolbox.HttpResponse;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class MockAsyncStack extends AsyncHttpStack {
+
+ private HttpResponse mResponseToReturn;
+
+ private IOException mExceptionToThrow;
+
+ private String mLastUrl;
+
+ private Map<String, String> mLastHeaders;
+
+ private byte[] mLastPostBody;
+
+ public String getLastUrl() {
+ return mLastUrl;
+ }
+
+ public Map<String, String> getLastHeaders() {
+ return mLastHeaders;
+ }
+
+ public byte[] getLastPostBody() {
+ return mLastPostBody;
+ }
+
+ public void setResponseToReturn(HttpResponse response) {
+ mResponseToReturn = response;
+ }
+
+ public void setExceptionToThrow(IOException exception) {
+ mExceptionToThrow = exception;
+ }
+
+ @Override
+ public void executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders, OnRequestComplete callback) {
+ if (mExceptionToThrow != null) {
+ callback.onError(mExceptionToThrow);
+ return;
+ }
+ mLastUrl = request.getUrl();
+ mLastHeaders = new HashMap<>();
+ try {
+ if (request.getHeaders() != null) {
+ mLastHeaders.putAll(request.getHeaders());
+ }
+ } catch (AuthFailureError authFailureError) {
+ callback.onAuthError(authFailureError);
+ return;
+ }
+ if (additionalHeaders != null) {
+ mLastHeaders.putAll(additionalHeaders);
+ }
+ try {
+ mLastPostBody = request.getBody();
+ } catch (AuthFailureError e) {
+ mLastPostBody = null;
+ }
+ callback.onSuccess(mResponseToReturn);
+ }
+}
diff --git a/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java b/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java
new file mode 100644
index 0000000..91d4062
--- /dev/null
+++ b/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java
@@ -0,0 +1,508 @@
+/*
+ * Copyright (C) 2020 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 static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.*;
+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;
+
+import com.android.volley.AsyncNetwork;
+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.NoConnectionError;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.RetryPolicy;
+import com.android.volley.ServerError;
+import com.android.volley.TimeoutError;
+import com.android.volley.VolleyError;
+import com.android.volley.mock.MockAsyncStack;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class BasicAsyncNetworkTest {
+
+ @Mock private RetryPolicy mMockRetryPolicy;
+ @Mock private AsyncNetwork.OnRequestComplete mockCallback;
+ private ExecutorService executor = MoreExecutors.newDirectExecutorService();
+
+ @Before
+ public void setUp() throws Exception {
+ initMocks(this);
+ }
+
+ @Test
+ public void headersAndPostParams() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ HttpResponse fakeResponse =
+ new HttpResponse(
+ 200,
+ Collections.<Header>emptyList(),
+ "foobar".getBytes(StandardCharsets.UTF_8));
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ Entry entry = new Entry();
+ entry.etag = "foobar";
+ entry.lastModified = 1503102002000L;
+ request.setCacheEntry(entry);
+ perform(request, httpNetwork).get();
+ assertEquals("foo", mockAsyncStack.getLastHeaders().get("requestheader"));
+ assertEquals("foobar", mockAsyncStack.getLastHeaders().get("If-None-Match"));
+ assertEquals(
+ "Sat, 19 Aug 2017 00:20:02 GMT",
+ mockAsyncStack.getLastHeaders().get("If-Modified-Since"));
+ assertEquals(
+ "requestpost=foo&",
+ new String(mockAsyncStack.getLastPostBody(), StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void headersAndPostParamsStream() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ ByteArrayInputStream stream = new ByteArrayInputStream("foobar".getBytes("UTF-8"));
+ HttpResponse fakeResponse =
+ new HttpResponse(200, Collections.<Header>emptyList(), 6, stream);
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ Entry entry = new Entry();
+ entry.etag = "foobar";
+ entry.lastModified = 1503102002000L;
+ request.setCacheEntry(entry);
+ perform(request, httpNetwork).get();
+ assertEquals("foo", mockAsyncStack.getLastHeaders().get("requestheader"));
+ assertEquals("foobar", mockAsyncStack.getLastHeaders().get("If-None-Match"));
+ assertEquals(
+ "Sat, 19 Aug 2017 00:20:02 GMT",
+ mockAsyncStack.getLastHeaders().get("If-Modified-Since"));
+ assertEquals(
+ "requestpost=foo&",
+ new String(mockAsyncStack.getLastPostBody(), StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void notModified() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ 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);
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ 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);
+ httpNetwork.performRequest(request, mockCallback);
+ NetworkResponse response = perform(request, httpNetwork).get();
+ 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[0])));
+ }
+
+ @Test
+ public void notModified_legacyCache() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ 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);
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ 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 = perform(request, httpNetwork).get();
+ 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[0])));
+ }
+
+ @Test
+ public void socketTimeout() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ mockAsyncStack.setExceptionToThrow(new SocketTimeoutException());
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ httpNetwork.performRequest(request, mockCallback);
+ verify(mockCallback, times(1)).onError(any(VolleyError.class));
+ verify(mockCallback, never()).onSuccess(any(NetworkResponse.class));
+ // should retry socket timeouts
+ verify(mMockRetryPolicy).retry(any(TimeoutError.class));
+ reset(mMockRetryPolicy, mockCallback);
+ }
+
+ @Test
+ public void noConnectionDefault() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ mockAsyncStack.setExceptionToThrow(new IOException());
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ httpNetwork.performRequest(request, mockCallback);
+ verify(mockCallback, times(1)).onError(any(VolleyError.class));
+ verify(mockCallback, never()).onSuccess(any(NetworkResponse.class));
+ // should not retry when there is no connection
+ verify(mMockRetryPolicy, never()).retry(any(VolleyError.class));
+ reset(mMockRetryPolicy, mockCallback);
+ }
+
+ @Test
+ public void noConnectionRetry() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ mockAsyncStack.setExceptionToThrow(new IOException());
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ request.setShouldRetryConnectionErrors(true);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ httpNetwork.performRequest(request, mockCallback);
+ verify(mockCallback, times(1)).onError(any(VolleyError.class));
+ verify(mockCallback, never()).onSuccess(any(NetworkResponse.class));
+ // should retry when there is no connection
+ verify(mMockRetryPolicy).retry(any(NoConnectionError.class));
+ reset(mMockRetryPolicy, mockCallback);
+ }
+
+ @Test
+ public void noConnectionNoRetry() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ mockAsyncStack.setExceptionToThrow(new IOException());
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ request.setShouldRetryConnectionErrors(false);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ httpNetwork.performRequest(request, mockCallback);
+ verify(mockCallback, times(1)).onError(any(VolleyError.class));
+ verify(mockCallback, never()).onSuccess(any(NetworkResponse.class));
+ // should not retry when there is no connection
+ verify(mMockRetryPolicy, never()).retry(any(VolleyError.class));
+ reset(mMockRetryPolicy, mockCallback);
+ }
+
+ @Test
+ public void unauthorized() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ HttpResponse fakeResponse = new HttpResponse(401, Collections.<Header>emptyList());
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ httpNetwork.performRequest(request, mockCallback);
+ verify(mockCallback, times(1)).onError(any(VolleyError.class));
+ verify(mockCallback, never()).onSuccess(any(NetworkResponse.class));
+ // should retry in case it's an auth failure.
+ verify(mMockRetryPolicy).retry(any(AuthFailureError.class));
+ reset(mMockRetryPolicy, mockCallback);
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void malformedUrlRequest() throws VolleyError, ExecutionException, InterruptedException {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ mockAsyncStack.setExceptionToThrow(new MalformedURLException());
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ perform(request, httpNetwork).get();
+ }
+
+ @Test
+ public void forbidden() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ HttpResponse fakeResponse = new HttpResponse(403, Collections.<Header>emptyList());
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ httpNetwork.performRequest(request, mockCallback);
+ verify(mockCallback, times(1)).onError(any(VolleyError.class));
+ verify(mockCallback, never()).onSuccess(any(NetworkResponse.class));
+ // should retry in case it's an auth failure.
+ verify(mMockRetryPolicy).retry(any(AuthFailureError.class));
+ reset(mMockRetryPolicy, mockCallback);
+ }
+
+ @Test
+ public void redirect() throws Exception {
+ for (int i = 300; i <= 399; i++) {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ httpNetwork.performRequest(request, mockCallback);
+ if (i != 304) {
+ verify(mockCallback, times(1)).onError(any(VolleyError.class));
+ verify(mockCallback, never()).onSuccess(any(NetworkResponse.class));
+ } else {
+ verify(mockCallback, never()).onError(any(VolleyError.class));
+ verify(mockCallback, times(1)).onSuccess(any(NetworkResponse.class));
+ }
+ // should not retry 300 responses.
+ verify(mMockRetryPolicy, never()).retry(any(VolleyError.class));
+ reset(mMockRetryPolicy, mockCallback);
+ }
+ }
+
+ @Test
+ public void otherClientError() throws Exception {
+ for (int i = 400; i <= 499; i++) {
+ if (i == 401 || i == 403) {
+ // covered above.
+ continue;
+ }
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ httpNetwork.performRequest(request, mockCallback);
+ verify(mockCallback, times(1)).onError(any(VolleyError.class));
+ verify(mockCallback, never()).onSuccess(any(NetworkResponse.class));
+ // should not retry other 400 errors.
+ verify(mMockRetryPolicy, never()).retry(any(VolleyError.class));
+ reset(mMockRetryPolicy, mockCallback);
+ }
+ }
+
+ @Test
+ public void serverError_enableRetries() throws Exception {
+ for (int i = 500; i <= 599; i++) {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork =
+ new BasicAsyncNetwork.Builder(mockAsyncStack)
+ .setPool(new ByteArrayPool(4096))
+ .build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ request.setShouldRetryServerErrors(true);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ httpNetwork.performRequest(request, mockCallback);
+ verify(mockCallback, times(1)).onError(any(VolleyError.class));
+ verify(mockCallback, never()).onSuccess(any(NetworkResponse.class));
+ // should retry all 500 errors
+ verify(mMockRetryPolicy).retry(any(ServerError.class));
+ reset(mMockRetryPolicy, mockCallback);
+ }
+ }
+
+ @Test
+ public void serverError_disableRetries() throws Exception {
+ for (int i = 500; i <= 599; i++) {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ httpNetwork.performRequest(request, mockCallback);
+ verify(mockCallback, times(1)).onError(any(VolleyError.class));
+ verify(mockCallback, never()).onSuccess(any(NetworkResponse.class));
+ // should not retry any 500 error w/ HTTP 500 retries turned off (the default).
+ verify(mMockRetryPolicy, never()).retry(any(VolleyError.class));
+ reset(mMockRetryPolicy, mockCallback);
+ }
+ }
+
+ @Test
+ public void notModifiedShortCircuit() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ 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);
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ httpNetwork.performRequest(request, mockCallback);
+ verify(mockCallback, times(1)).onSuccess(any(NetworkResponse.class));
+ verify(mockCallback, never()).onError(any(VolleyError.class));
+ reset(mMockRetryPolicy, mockCallback);
+ }
+
+ @Test
+ public void performRequestSuccess() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ HttpResponse fakeResponse =
+ new HttpResponse(
+ 200,
+ Collections.<Header>emptyList(),
+ "foobar".getBytes(StandardCharsets.UTF_8));
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ httpNetwork.setBlockingExecutor(executor);
+ Request<String> request = buildRequest();
+ Entry entry = new Entry();
+ entry.etag = "foobar";
+ entry.lastModified = 1503102002000L;
+ request.setCacheEntry(entry);
+ httpNetwork.performRequest(request, mockCallback);
+ verify(mockCallback, times(1)).onSuccess(any(NetworkResponse.class));
+ verify(mockCallback, never()).onError(any(VolleyError.class));
+ reset(mMockRetryPolicy, mockCallback);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void performRequestNeverSetExecutorTest() throws Exception {
+ MockAsyncStack mockAsyncStack = new MockAsyncStack();
+ HttpResponse fakeResponse = new HttpResponse(200, Collections.<Header>emptyList());
+ mockAsyncStack.setResponseToReturn(fakeResponse);
+ BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build();
+ Request<String> request = buildRequest();
+ perform(request, httpNetwork).get();
+ }
+
+ /** Helper functions */
+ private CompletableFuture<NetworkResponse> perform(Request<?> request, AsyncNetwork network)
+ throws VolleyError {
+ final CompletableFuture<NetworkResponse> future = new CompletableFuture<>();
+ network.performRequest(
+ request,
+ new AsyncNetwork.OnRequestComplete() {
+ @Override
+ public void onSuccess(NetworkResponse networkResponse) {
+ future.complete(networkResponse);
+ }
+
+ @Override
+ public void onError(VolleyError volleyError) {
+ future.complete(null);
+ }
+ });
+ return future;
+ }
+
+ private static Request<String> buildRequest() {
+ return new Request<String>(Request.Method.GET, "http://foo", null) {
+
+ @Override
+ protected Response<String> parseNetworkResponse(NetworkResponse response) {
+ return null;
+ }
+
+ @Override
+ protected void deliverResponse(String response) {}
+
+ @Override
+ public Map<String, String> getHeaders() {
+ Map<String, String> result = new HashMap<String, String>();
+ result.put("requestheader", "foo");
+ return result;
+ }
+
+ @Override
+ public Map<String, String> getParams() {
+ Map<String, String> result = new HashMap<String, String>();
+ result.put("requestpost", "foo");
+ return result;
+ }
+ };
+ }
+}
diff --git a/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java b/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
index fec0694..3630379 100644
--- a/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
+++ b/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
@@ -30,6 +30,7 @@ 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.NoConnectionError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.RetryPolicy;
@@ -176,7 +177,7 @@ public class BasicNetworkTest {
}
@Test
- public void noConnection() throws Exception {
+ public void noConnectionDefault() throws Exception {
MockHttpStack mockHttpStack = new MockHttpStack();
mockHttpStack.setExceptionToThrow(new IOException());
BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
@@ -193,6 +194,43 @@ public class BasicNetworkTest {
}
@Test
+ public void noConnectionRetry() throws Exception {
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ mockHttpStack.setExceptionToThrow(new IOException());
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ request.setShouldRetryConnectionErrors(true);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ try {
+ httpNetwork.performRequest(request);
+ } catch (VolleyError e) {
+ // expected
+ }
+ // should retry when there is no connection
+ verify(mMockRetryPolicy).retry(any(NoConnectionError.class));
+ reset(mMockRetryPolicy);
+ }
+
+ @Test
+ public void noConnectionNoRetry() throws Exception {
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ mockHttpStack.setExceptionToThrow(new IOException());
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ request.setShouldRetryConnectionErrors(false);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ try {
+ httpNetwork.performRequest(request);
+ } catch (VolleyError e) {
+ // expected
+ }
+ // should not retry when there is no connection
+ verify(mMockRetryPolicy, never()).retry(any(VolleyError.class));
+ }
+
+ @Test
public void unauthorized() throws Exception {
MockHttpStack mockHttpStack = new MockHttpStack();
HttpResponse fakeResponse = new HttpResponse(401, Collections.<Header>emptyList());
diff --git a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
index ccf68fa..db6e491 100644
--- a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
+++ b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
@@ -59,7 +59,7 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
-@Config(manifest = "src/main/AndroidManifest.xml", sdk = 16)
+@Config(sdk = 16)
public class DiskBasedCacheTest {
private static final int MAX_SIZE = 1024 * 1024;
diff --git a/src/test/java/com/android/volley/utils/CacheTestUtils.java b/src/test/java/com/android/volley/utils/CacheTestUtils.java
index 49ab996..5980712 100644
--- a/src/test/java/com/android/volley/utils/CacheTestUtils.java
+++ b/src/test/java/com/android/volley/utils/CacheTestUtils.java
@@ -16,6 +16,11 @@
package com.android.volley.utils;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
import com.android.volley.Cache;
import java.util.Random;
@@ -51,4 +56,34 @@ public class CacheTestUtils {
public static Cache.Entry makeRandomCacheEntry(byte[] data) {
return makeRandomCacheEntry(data, false, false);
}
+
+ public static void assertThatEntriesAreEqual(Cache.Entry actual, Cache.Entry expected) {
+ assertNotNull(actual);
+ assertThat(actual.data, is(equalTo(expected.data)));
+ assertThat(actual.etag, is(equalTo(expected.etag)));
+ assertThat(actual.lastModified, is(equalTo(expected.lastModified)));
+ assertThat(actual.responseHeaders, is(equalTo(expected.responseHeaders)));
+ assertThat(actual.serverDate, is(equalTo(expected.serverDate)));
+ assertThat(actual.softTtl, is(equalTo(expected.softTtl)));
+ assertThat(actual.ttl, is(equalTo(expected.ttl)));
+ }
+
+ public static Cache.Entry randomData(int length) {
+ Cache.Entry entry = new Cache.Entry();
+ byte[] data = new byte[length];
+ new Random(42).nextBytes(data); // explicit seed for reproducible results
+ entry.data = data;
+ return entry;
+ }
+
+ public static int getEntrySizeOnDisk(String key) {
+ // Header size is:
+ // 4 bytes for magic int
+ // 8 + len(key) bytes for key (long length)
+ // 8 bytes for etag (long length + 0 characters)
+ // 32 bytes for serverDate, lastModified, ttl, and softTtl longs
+ // 4 bytes for length of header list int
+ // == 56 + len(key) bytes total.
+ return 56 + key.length();
+ }
}