aboutsummaryrefslogtreecommitdiff
path: root/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'core/src')
-rw-r--r--core/src/main/AndroidManifest.xml2
-rw-r--r--core/src/main/java/com/android/volley/AsyncCache.java94
-rw-r--r--core/src/main/java/com/android/volley/AsyncNetwork.java145
-rw-r--r--core/src/main/java/com/android/volley/AsyncRequestQueue.java684
-rw-r--r--core/src/main/java/com/android/volley/AuthFailureError.java56
-rw-r--r--core/src/main/java/com/android/volley/Cache.java121
-rw-r--r--core/src/main/java/com/android/volley/CacheDispatcher.java212
-rw-r--r--core/src/main/java/com/android/volley/ClientError.java34
-rw-r--r--core/src/main/java/com/android/volley/DefaultRetryPolicy.java95
-rw-r--r--core/src/main/java/com/android/volley/ExecutorDelivery.java121
-rw-r--r--core/src/main/java/com/android/volley/Header.java59
-rw-r--r--core/src/main/java/com/android/volley/Network.java29
-rw-r--r--core/src/main/java/com/android/volley/NetworkDispatcher.java177
-rw-r--r--core/src/main/java/com/android/volley/NetworkError.java33
-rw-r--r--core/src/main/java/com/android/volley/NetworkResponse.java198
-rw-r--r--core/src/main/java/com/android/volley/NoConnectionError.java29
-rw-r--r--core/src/main/java/com/android/volley/ParseError.java31
-rw-r--r--core/src/main/java/com/android/volley/Request.java723
-rw-r--r--core/src/main/java/com/android/volley/RequestQueue.java342
-rw-r--r--core/src/main/java/com/android/volley/RequestTask.java20
-rw-r--r--core/src/main/java/com/android/volley/Response.java84
-rw-r--r--core/src/main/java/com/android/volley/ResponseDelivery.java31
-rw-r--r--core/src/main/java/com/android/volley/RetryPolicy.java56
-rw-r--r--core/src/main/java/com/android/volley/ServerError.java29
-rw-r--r--core/src/main/java/com/android/volley/TimeoutError.java21
-rw-r--r--core/src/main/java/com/android/volley/VolleyError.java55
-rw-r--r--core/src/main/java/com/android/volley/VolleyLog.java182
-rw-r--r--core/src/main/java/com/android/volley/WaitingRequestManager.java176
-rw-r--r--core/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java78
-rw-r--r--core/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java123
-rw-r--r--core/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java175
-rw-r--r--core/src/main/java/com/android/volley/toolbox/Authenticator.java32
-rw-r--r--core/src/main/java/com/android/volley/toolbox/BaseHttpStack.java92
-rw-r--r--core/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java320
-rw-r--r--core/src/main/java/com/android/volley/toolbox/BasicNetwork.java167
-rw-r--r--core/src/main/java/com/android/volley/toolbox/ByteArrayPool.java130
-rw-r--r--core/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java66
-rw-r--r--core/src/main/java/com/android/volley/toolbox/DiskBasedCache.java677
-rw-r--r--core/src/main/java/com/android/volley/toolbox/FileSupplier.java24
-rw-r--r--core/src/main/java/com/android/volley/toolbox/HttpClientStack.java201
-rw-r--r--core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java301
-rw-r--r--core/src/main/java/com/android/volley/toolbox/HttpResponse.java118
-rw-r--r--core/src/main/java/com/android/volley/toolbox/HttpStack.java47
-rw-r--r--core/src/main/java/com/android/volley/toolbox/HurlStack.java321
-rw-r--r--core/src/main/java/com/android/volley/toolbox/ImageLoader.java541
-rw-r--r--core/src/main/java/com/android/volley/toolbox/ImageRequest.java283
-rw-r--r--core/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java86
-rw-r--r--core/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java106
-rw-r--r--core/src/main/java/com/android/volley/toolbox/JsonRequest.java137
-rw-r--r--core/src/main/java/com/android/volley/toolbox/NetworkImageView.java332
-rw-r--r--core/src/main/java/com/android/volley/toolbox/NetworkUtility.java206
-rw-r--r--core/src/main/java/com/android/volley/toolbox/NoAsyncCache.java42
-rw-r--r--core/src/main/java/com/android/volley/toolbox/NoCache.java42
-rw-r--r--core/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java92
-rw-r--r--core/src/main/java/com/android/volley/toolbox/RequestFuture.java159
-rw-r--r--core/src/main/java/com/android/volley/toolbox/StringRequest.java100
-rw-r--r--core/src/main/java/com/android/volley/toolbox/Threads.java13
-rw-r--r--core/src/main/java/com/android/volley/toolbox/UrlRewriter.java29
-rw-r--r--core/src/main/java/com/android/volley/toolbox/Volley.java123
-rw-r--r--core/src/test/java/com/android/volley/AsyncRequestQueueTest.java200
-rw-r--r--core/src/test/java/com/android/volley/CacheDispatcherTest.java276
-rw-r--r--core/src/test/java/com/android/volley/NetworkDispatcherTest.java146
-rw-r--r--core/src/test/java/com/android/volley/NetworkResponseTest.java61
-rw-r--r--core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java197
-rw-r--r--core/src/test/java/com/android/volley/RequestQueueTest.java129
-rw-r--r--core/src/test/java/com/android/volley/RequestTest.java232
-rw-r--r--core/src/test/java/com/android/volley/ResponseDeliveryTest.java71
-rw-r--r--core/src/test/java/com/android/volley/mock/MockAsyncStack.java86
-rw-r--r--core/src/test/java/com/android/volley/mock/MockHttpStack.java80
-rw-r--r--core/src/test/java/com/android/volley/mock/MockRequest.java99
-rw-r--r--core/src/test/java/com/android/volley/mock/ShadowSystemClock.java27
-rw-r--r--core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java128
-rw-r--r--core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java111
-rw-r--r--core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java104
-rw-r--r--core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java508
-rw-r--r--core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java384
-rw-r--r--core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java78
-rw-r--r--core/src/test/java/com/android/volley/toolbox/CacheTest.java39
-rw-r--r--core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java646
-rw-r--r--core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java156
-rw-r--r--core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java317
-rw-r--r--core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java192
-rw-r--r--core/src/test/java/com/android/volley/toolbox/HurlStackTest.java337
-rw-r--r--core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java121
-rw-r--r--core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java194
-rw-r--r--core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java119
-rw-r--r--core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java73
-rw-r--r--core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java101
-rw-r--r--core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java81
-rw-r--r--core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java35
-rw-r--r--core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java51
-rw-r--r--core/src/test/java/com/android/volley/toolbox/RequestTest.java77
-rw-r--r--core/src/test/java/com/android/volley/toolbox/ResponseTest.java55
-rw-r--r--core/src/test/java/com/android/volley/toolbox/StringRequestTest.java42
-rw-r--r--core/src/test/java/com/android/volley/utils/CacheTestUtils.java89
-rw-r--r--core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java37
-rw-r--r--core/src/test/resources/org.robolectric.Config.properties1
97 files changed, 14682 insertions, 0 deletions
diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ba3a2a7
--- /dev/null
+++ b/core/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest package="com.android.volley" />
diff --git a/core/src/main/java/com/android/volley/AsyncCache.java b/core/src/main/java/com/android/volley/AsyncCache.java
new file mode 100644
index 0000000..8b2dbcc
--- /dev/null
+++ b/core/src/main/java/com/android/volley/AsyncCache.java
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
+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/core/src/main/java/com/android/volley/AsyncNetwork.java b/core/src/main/java/com/android/volley/AsyncNetwork.java
new file mode 100644
index 0000000..47f35ea
--- /dev/null
+++ b/core/src/main/java/com/android/volley/AsyncNetwork.java
@@ -0,0 +1,145 @@
+/*
+ * 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.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
+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/core/src/main/java/com/android/volley/AsyncRequestQueue.java b/core/src/main/java/com/android/volley/AsyncRequestQueue.java
new file mode 100644
index 0000000..7bf8c21
--- /dev/null
+++ b/core/src/main/java/com/android/volley/AsyncRequestQueue.java
@@ -0,0 +1,684 @@
+/*
+ * 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.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+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).
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
+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);
+
+ /**
+ * Requests which have been queued before cache initialization has completed.
+ *
+ * <p>These requests are kicked off once cache initialization finishes. We avoid enqueuing them
+ * sooner as the cache may not yet be ready.
+ */
+ private final List<Request<?>> mRequestsAwaitingCacheInitialization = new ArrayList<>();
+
+ private volatile boolean mIsCacheInitialized = false;
+ private final Object mCacheInitializationLock = new Object[0];
+
+ /**
+ * 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);
+
+ // Kick off cache initialization, which must complete before any requests can be processed.
+ if (mAsyncCache != null) {
+ mNonBlockingExecutor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ mAsyncCache.initialize(
+ new AsyncCache.OnWriteCompleteCallback() {
+ @Override
+ public void onWriteComplete() {
+ onCacheInitializationComplete();
+ }
+ });
+ }
+ });
+ } else {
+ mBlockingExecutor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ getCache().initialize();
+ mNonBlockingExecutor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ onCacheInitializationComplete();
+ }
+ });
+ }
+ });
+ }
+ }
+
+ /** 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 cache hasn't been initialized yet, add the request to a temporary queue to be
+ // flushed once initialization completes.
+ if (!mIsCacheInitialized) {
+ synchronized (mCacheInitializationLock) {
+ if (!mIsCacheInitialized) {
+ mRequestsAwaitingCacheInitialization.add(request);
+ return;
+ }
+ }
+ }
+
+ // 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);
+ }
+ }
+
+ private void onCacheInitializationComplete() {
+ List<Request<?>> requestsToDispatch;
+ synchronized (mCacheInitializationLock) {
+ requestsToDispatch = new ArrayList<>(mRequestsAwaitingCacheInitialization);
+ mRequestsAwaitingCacheInitialization.clear();
+ mIsCacheInitialized = true;
+ }
+
+ // Kick off any requests that were queued while waiting for cache initialization.
+ for (Request<?> request : requestsToDispatch) {
+ beginRequest(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;
+ }
+
+ // Use a single instant to evaluate cache expiration. Otherwise, a cache entry with
+ // identical soft and hard TTL times may appear to be valid when checking isExpired but
+ // invalid upon checking refreshNeeded(), triggering a soft TTL refresh which should be
+ // impossible.
+ long currentTimeMillis = System.currentTimeMillis();
+
+ // If it is completely expired, just send it to the network.
+ if (entry.isExpired(currentTimeMillis)) {
+ 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, currentTimeMillis));
+ }
+
+ private class CacheParseTask<T> extends RequestTask<T> {
+ Cache.Entry entry;
+ long startTimeMillis;
+
+ CacheParseTask(Request<T> request, Cache.Entry entry, long startTimeMillis) {
+ super(request);
+ this.entry = entry;
+ this.startTimeMillis = startTimeMillis;
+ }
+
+ @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(startTimeMillis)) {
+ // 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);
+ }
+
+ /**
+ * Factory to create/provide the executors which Volley will use.
+ *
+ * <p>This class may be used by advanced applications to provide custom executors according to
+ * their needs.
+ *
+ * <p>For applications which rely on setting request priority via {@link Request#getPriority}, a
+ * task queue is provided which will prioritize requests of higher priority should the thread
+ * pool itself be exhausted. If a shared pool is provided which does not make use of the given
+ * queue, then lower-priority requests may have tasks executed before higher-priority requests
+ * when enough tasks are in flight to fully saturate the shared pool.
+ */
+ public abstract static class ExecutorFactory {
+ public abstract ExecutorService createNonBlockingExecutor(
+ BlockingQueue<Runnable> taskQueue);
+
+ public abstract ExecutorService createBlockingExecutor(BlockingQueue<Runnable> taskQueue);
+
+ public 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/core/src/main/java/com/android/volley/AuthFailureError.java b/core/src/main/java/com/android/volley/AuthFailureError.java
new file mode 100644
index 0000000..fc6417e
--- /dev/null
+++ b/core/src/main/java/com/android/volley/AuthFailureError.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+import android.content.Intent;
+
+/** Error indicating that there was an authentication failure when performing a Request. */
+@SuppressWarnings("serial")
+public class AuthFailureError extends VolleyError {
+ /** An intent that can be used to resolve this exception. (Brings up the password dialog.) */
+ private Intent mResolutionIntent;
+
+ public AuthFailureError() {}
+
+ public AuthFailureError(Intent intent) {
+ mResolutionIntent = intent;
+ }
+
+ public AuthFailureError(NetworkResponse response) {
+ super(response);
+ }
+
+ public AuthFailureError(String message) {
+ super(message);
+ }
+
+ public AuthFailureError(String message, Exception reason) {
+ super(message, reason);
+ }
+
+ public Intent getResolutionIntent() {
+ return mResolutionIntent;
+ }
+
+ @Override
+ public String getMessage() {
+ if (mResolutionIntent != null) {
+ return "User needs to (re)enter credentials.";
+ }
+ return super.getMessage();
+ }
+}
diff --git a/core/src/main/java/com/android/volley/Cache.java b/core/src/main/java/com/android/volley/Cache.java
new file mode 100644
index 0000000..7348d0f
--- /dev/null
+++ b/core/src/main/java/com/android/volley/Cache.java
@@ -0,0 +1,121 @@
+/*
+ * 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 androidx.annotation.Nullable;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/** An interface for a cache keyed by a String with a byte array as data. */
+public interface Cache {
+ /**
+ * Retrieves an entry from the cache.
+ *
+ * @param key Cache key
+ * @return An {@link Entry} or null in the event of a cache miss
+ */
+ @Nullable
+ Entry get(String key);
+
+ /**
+ * Adds or replaces an entry to the cache.
+ *
+ * @param key Cache key
+ * @param entry Data to store and metadata for cache coherency, TTL, etc.
+ */
+ void put(String key, Entry entry);
+
+ /**
+ * Performs any potentially long-running actions needed to initialize the cache; will be called
+ * from a worker thread.
+ */
+ void initialize();
+
+ /**
+ * Invalidates an entry in the cache.
+ *
+ * @param key Cache key
+ * @param fullExpire True to fully expire the entry, false to soft expire
+ */
+ void invalidate(String key, boolean fullExpire);
+
+ /**
+ * Removes an entry from the cache.
+ *
+ * @param key Cache key
+ */
+ void remove(String key);
+
+ /** Empties the cache. */
+ void clear();
+
+ /** Data and metadata for an entry returned by the cache. */
+ class Entry {
+ /** The data returned from cache. */
+ public byte[] data;
+
+ /** ETag for cache coherency. */
+ public String etag;
+
+ /** Date of this response as reported by the server. */
+ public long serverDate;
+
+ /** The last modified date for the requested object. */
+ public long lastModified;
+
+ /** TTL for this record. */
+ public long ttl;
+
+ /** Soft TTL for this record. */
+ public long softTtl;
+
+ /**
+ * Response headers as received from server; must be non-null. Should not be mutated
+ * directly.
+ *
+ * <p>Note that if the server returns two headers with the same (case-insensitive) name,
+ * this map will only contain the one of them. {@link #allResponseHeaders} may contain all
+ * headers if the {@link Cache} implementation supports it.
+ */
+ public Map<String, String> responseHeaders = Collections.emptyMap();
+
+ /**
+ * All response headers. May be null depending on the {@link Cache} implementation. Should
+ * not be mutated directly.
+ */
+ public List<Header> allResponseHeaders;
+
+ /** True if the entry is expired. */
+ public boolean isExpired() {
+ return isExpired(System.currentTimeMillis());
+ }
+
+ boolean isExpired(long currentTimeMillis) {
+ return this.ttl < currentTimeMillis;
+ }
+
+ /** True if a refresh is needed from the original data source. */
+ public boolean refreshNeeded() {
+ return refreshNeeded(System.currentTimeMillis());
+ }
+
+ boolean refreshNeeded(long currentTimeMillis) {
+ return this.softTtl < currentTimeMillis;
+ }
+ }
+}
diff --git a/core/src/main/java/com/android/volley/CacheDispatcher.java b/core/src/main/java/com/android/volley/CacheDispatcher.java
new file mode 100644
index 0000000..4443143
--- /dev/null
+++ b/core/src/main/java/com/android/volley/CacheDispatcher.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+import android.os.Process;
+import androidx.annotation.VisibleForTesting;
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Provides a thread for performing cache triage on a queue of requests.
+ *
+ * <p>Requests added to the specified cache queue are resolved from cache. Any deliverable response
+ * is posted back to the caller via a {@link ResponseDelivery}. Cache misses and responses that
+ * require refresh are enqueued on the specified network queue for processing by a {@link
+ * NetworkDispatcher}.
+ */
+public class CacheDispatcher extends Thread {
+
+ private static final boolean DEBUG = VolleyLog.DEBUG;
+
+ /** The queue of requests coming in for triage. */
+ private final BlockingQueue<Request<?>> mCacheQueue;
+
+ /** The queue of requests going out to the network. */
+ private final BlockingQueue<Request<?>> mNetworkQueue;
+
+ /** The cache to read from. */
+ private final Cache mCache;
+
+ /** For posting responses. */
+ private final ResponseDelivery mDelivery;
+
+ /** Used for telling us to die. */
+ private volatile boolean mQuit = false;
+
+ /** Manage list of waiting requests and de-duplicate requests with same cache key. */
+ private final WaitingRequestManager mWaitingRequestManager;
+
+ /**
+ * Creates a new cache triage dispatcher thread. You must call {@link #start()} in order to
+ * begin processing.
+ *
+ * @param cacheQueue Queue of incoming requests for triage
+ * @param networkQueue Queue to post requests that require network to
+ * @param cache Cache interface to use for resolution
+ * @param delivery Delivery interface to use for posting responses
+ */
+ public CacheDispatcher(
+ BlockingQueue<Request<?>> cacheQueue,
+ BlockingQueue<Request<?>> networkQueue,
+ Cache cache,
+ ResponseDelivery delivery) {
+ mCacheQueue = cacheQueue;
+ mNetworkQueue = networkQueue;
+ mCache = cache;
+ mDelivery = delivery;
+ mWaitingRequestManager = new WaitingRequestManager(this, networkQueue, delivery);
+ }
+
+ /**
+ * Forces this dispatcher to quit immediately. If any requests are still in the queue, they are
+ * not guaranteed to be processed.
+ */
+ public void quit() {
+ mQuit = true;
+ interrupt();
+ }
+
+ @Override
+ public void run() {
+ if (DEBUG) VolleyLog.v("start new dispatcher");
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ // Make a blocking call to initialize the cache.
+ mCache.initialize();
+
+ while (true) {
+ try {
+ processRequest();
+ } catch (InterruptedException e) {
+ // We may have been interrupted because it was time to quit.
+ if (mQuit) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ VolleyLog.e(
+ "Ignoring spurious interrupt of CacheDispatcher thread; "
+ + "use quit() to terminate it");
+ }
+ }
+ }
+
+ // Extracted to its own method to ensure locals have a constrained liveness scope by the GC.
+ // This is needed to avoid keeping previous request references alive for an indeterminate amount
+ // of time. Update consumer-proguard-rules.pro when modifying this. See also
+ // https://github.com/google/volley/issues/114
+ private void processRequest() throws InterruptedException {
+ // Get a request from the cache triage queue, blocking until
+ // at least one is available.
+ final Request<?> request = mCacheQueue.take();
+ processRequest(request);
+ }
+
+ @VisibleForTesting
+ void processRequest(final Request<?> request) throws InterruptedException {
+ request.addMarker("cache-queue-take");
+ request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED);
+
+ try {
+ // If the request has been canceled, don't bother dispatching it.
+ if (request.isCanceled()) {
+ request.finish("cache-discard-canceled");
+ return;
+ }
+
+ // Attempt to retrieve this item from cache.
+ Cache.Entry entry = mCache.get(request.getCacheKey());
+ if (entry == null) {
+ request.addMarker("cache-miss");
+ // Cache miss; send off to the network dispatcher.
+ if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+ mNetworkQueue.put(request);
+ }
+ return;
+ }
+
+ // Use a single instant to evaluate cache expiration. Otherwise, a cache entry with
+ // identical soft and hard TTL times may appear to be valid when checking isExpired but
+ // invalid upon checking refreshNeeded(), triggering a soft TTL refresh which should be
+ // impossible.
+ long currentTimeMillis = System.currentTimeMillis();
+
+ // If it is completely expired, just send it to the network.
+ if (entry.isExpired(currentTimeMillis)) {
+ request.addMarker("cache-hit-expired");
+ request.setCacheEntry(entry);
+ if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+ mNetworkQueue.put(request);
+ }
+ return;
+ }
+
+ // We have a cache hit; parse its data for delivery back to the request.
+ request.addMarker("cache-hit");
+ Response<?> response =
+ request.parseNetworkResponse(
+ new NetworkResponse(entry.data, entry.responseHeaders));
+ request.addMarker("cache-hit-parsed");
+
+ if (!response.isSuccess()) {
+ request.addMarker("cache-parsing-failed");
+ mCache.invalidate(request.getCacheKey(), true);
+ request.setCacheEntry(null);
+ if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+ mNetworkQueue.put(request);
+ }
+ return;
+ }
+ if (!entry.refreshNeeded(currentTimeMillis)) {
+ // Completely unexpired cache hit. Just deliver the response.
+ mDelivery.postResponse(request, response);
+ } else {
+ // Soft-expired cache hit. We can deliver the cached response,
+ // but we need to also send the request to the network for
+ // refreshing.
+ request.addMarker("cache-hit-refresh-needed");
+ request.setCacheEntry(entry);
+ // Mark the response as intermediate.
+ response.intermediate = true;
+
+ if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+ // Post the intermediate response back to the user and have
+ // the delivery then forward the request along to the network.
+ mDelivery.postResponse(
+ request,
+ response,
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mNetworkQueue.put(request);
+ } catch (InterruptedException e) {
+ // Restore the interrupted status
+ Thread.currentThread().interrupt();
+ }
+ }
+ });
+ } else {
+ // request has been added to list of waiting requests
+ // to receive the network response from the first request once it returns.
+ mDelivery.postResponse(request, response);
+ }
+ }
+ } finally {
+ request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED);
+ }
+ }
+}
diff --git a/core/src/main/java/com/android/volley/ClientError.java b/core/src/main/java/com/android/volley/ClientError.java
new file mode 100644
index 0000000..521b76f
--- /dev/null
+++ b/core/src/main/java/com/android/volley/ClientError.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+/**
+ * Indicates that the server responded with an error response indicating that the client has erred.
+ *
+ * <p>For backwards compatibility, extends ServerError which used to be thrown for all server
+ * errors, including 4xx error codes indicating a client error.
+ */
+@SuppressWarnings("serial")
+public class ClientError extends ServerError {
+ public ClientError(NetworkResponse networkResponse) {
+ super(networkResponse);
+ }
+
+ public ClientError() {
+ super();
+ }
+}
diff --git a/core/src/main/java/com/android/volley/DefaultRetryPolicy.java b/core/src/main/java/com/android/volley/DefaultRetryPolicy.java
new file mode 100644
index 0000000..4be6b50
--- /dev/null
+++ b/core/src/main/java/com/android/volley/DefaultRetryPolicy.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+/** Default retry policy for requests. */
+public class DefaultRetryPolicy implements RetryPolicy {
+ /** The current timeout in milliseconds. */
+ private int mCurrentTimeoutMs;
+
+ /** The current retry count. */
+ private int mCurrentRetryCount;
+
+ /** The maximum number of attempts. */
+ private final int mMaxNumRetries;
+
+ /** The backoff multiplier for the policy. */
+ private final float mBackoffMultiplier;
+
+ /** The default socket timeout in milliseconds */
+ public static final int DEFAULT_TIMEOUT_MS = 2500;
+
+ /** The default number of retries */
+ public static final int DEFAULT_MAX_RETRIES = 1;
+
+ /** The default backoff multiplier */
+ public static final float DEFAULT_BACKOFF_MULT = 1f;
+
+ /** Constructs a new retry policy using the default timeouts. */
+ public DefaultRetryPolicy() {
+ this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
+ }
+
+ /**
+ * Constructs a new retry policy.
+ *
+ * @param initialTimeoutMs The initial timeout for the policy.
+ * @param maxNumRetries The maximum number of retries.
+ * @param backoffMultiplier Backoff multiplier for the policy.
+ */
+ public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
+ mCurrentTimeoutMs = initialTimeoutMs;
+ mMaxNumRetries = maxNumRetries;
+ mBackoffMultiplier = backoffMultiplier;
+ }
+
+ /** Returns the current timeout. */
+ @Override
+ public int getCurrentTimeout() {
+ return mCurrentTimeoutMs;
+ }
+
+ /** Returns the current retry count. */
+ @Override
+ public int getCurrentRetryCount() {
+ return mCurrentRetryCount;
+ }
+
+ /** Returns the backoff multiplier for the policy. */
+ public float getBackoffMultiplier() {
+ return mBackoffMultiplier;
+ }
+
+ /**
+ * Prepares for the next retry by applying a backoff to the timeout.
+ *
+ * @param error The error code of the last attempt.
+ */
+ @Override
+ public void retry(VolleyError error) throws VolleyError {
+ mCurrentRetryCount++;
+ mCurrentTimeoutMs += (int) (mCurrentTimeoutMs * mBackoffMultiplier);
+ if (!hasAttemptRemaining()) {
+ throw error;
+ }
+ }
+
+ /** Returns true if this policy has attempts remaining, false otherwise. */
+ protected boolean hasAttemptRemaining() {
+ return mCurrentRetryCount <= mMaxNumRetries;
+ }
+}
diff --git a/core/src/main/java/com/android/volley/ExecutorDelivery.java b/core/src/main/java/com/android/volley/ExecutorDelivery.java
new file mode 100644
index 0000000..fd992f9
--- /dev/null
+++ b/core/src/main/java/com/android/volley/ExecutorDelivery.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+import android.os.Handler;
+import java.util.concurrent.Executor;
+
+/** Delivers responses and errors. */
+public class ExecutorDelivery implements ResponseDelivery {
+ /** Used for posting responses, typically to the main thread. */
+ private final Executor mResponsePoster;
+
+ /**
+ * Creates a new response delivery interface.
+ *
+ * @param handler {@link Handler} to post responses on
+ */
+ public ExecutorDelivery(final Handler handler) {
+ // Make an Executor that just wraps the handler.
+ mResponsePoster =
+ new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ handler.post(command);
+ }
+ };
+ }
+
+ /**
+ * Creates a new response delivery interface, mockable version for testing.
+ *
+ * @param executor For running delivery tasks
+ */
+ public ExecutorDelivery(Executor executor) {
+ mResponsePoster = executor;
+ }
+
+ @Override
+ public void postResponse(Request<?> request, Response<?> response) {
+ postResponse(request, response, null);
+ }
+
+ @Override
+ public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
+ request.markDelivered();
+ request.addMarker("post-response");
+ mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
+ }
+
+ @Override
+ public void postError(Request<?> request, VolleyError error) {
+ request.addMarker("post-error");
+ Response<?> response = Response.error(error);
+ mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));
+ }
+
+ /** A Runnable used for delivering network responses to a listener on the main thread. */
+ @SuppressWarnings("rawtypes")
+ private static class ResponseDeliveryRunnable implements Runnable {
+ private final Request mRequest;
+ private final Response mResponse;
+ private final Runnable mRunnable;
+
+ public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
+ mRequest = request;
+ mResponse = response;
+ mRunnable = runnable;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void run() {
+ // NOTE: If cancel() is called off the thread that we're currently running in (by
+ // default, the main thread), we cannot guarantee that deliverResponse()/deliverError()
+ // won't be called, since it may be canceled after we check isCanceled() but before we
+ // deliver the response. Apps concerned about this guarantee must either call cancel()
+ // from the same thread or implement their own guarantee about not invoking their
+ // listener after cancel() has been called.
+
+ // If this request has canceled, finish it and don't deliver.
+ if (mRequest.isCanceled()) {
+ mRequest.finish("canceled-at-delivery");
+ return;
+ }
+
+ // Deliver a normal response or error, depending.
+ if (mResponse.isSuccess()) {
+ mRequest.deliverResponse(mResponse.result);
+ } else {
+ mRequest.deliverError(mResponse.error);
+ }
+
+ // If this is an intermediate response, add a marker, otherwise we're done
+ // and the request can be finished.
+ if (mResponse.intermediate) {
+ mRequest.addMarker("intermediate-response");
+ } else {
+ mRequest.finish("done");
+ }
+
+ // If we have been provided a post-delivery runnable, run it.
+ if (mRunnable != null) {
+ mRunnable.run();
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/com/android/volley/Header.java b/core/src/main/java/com/android/volley/Header.java
new file mode 100644
index 0000000..cd9c6ec
--- /dev/null
+++ b/core/src/main/java/com/android/volley/Header.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley;
+
+import android.text.TextUtils;
+
+/** An HTTP header. */
+public final class Header {
+ private final String mName;
+ private final String mValue;
+
+ public Header(String name, String value) {
+ mName = name;
+ mValue = value;
+ }
+
+ public final String getName() {
+ return mName;
+ }
+
+ public final String getValue() {
+ return mValue;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Header header = (Header) o;
+
+ return TextUtils.equals(mName, header.mName) && TextUtils.equals(mValue, header.mValue);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mName.hashCode();
+ result = 31 * result + mValue.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Header[name=" + mName + ",value=" + mValue + "]";
+ }
+}
diff --git a/core/src/main/java/com/android/volley/Network.java b/core/src/main/java/com/android/volley/Network.java
new file mode 100644
index 0000000..16d5858
--- /dev/null
+++ b/core/src/main/java/com/android/volley/Network.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+/** An interface for performing requests. */
+public interface Network {
+ /**
+ * Performs the specified request.
+ *
+ * @param request Request to process
+ * @return A {@link NetworkResponse} with data and caching metadata; will never be null
+ * @throws VolleyError on errors
+ */
+ NetworkResponse performRequest(Request<?> request) throws VolleyError;
+}
diff --git a/core/src/main/java/com/android/volley/NetworkDispatcher.java b/core/src/main/java/com/android/volley/NetworkDispatcher.java
new file mode 100644
index 0000000..06057c3
--- /dev/null
+++ b/core/src/main/java/com/android/volley/NetworkDispatcher.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+import android.annotation.TargetApi;
+import android.net.TrafficStats;
+import android.os.Build;
+import android.os.Process;
+import android.os.SystemClock;
+import androidx.annotation.VisibleForTesting;
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Provides a thread for performing network dispatch from a queue of requests.
+ *
+ * <p>Requests added to the specified queue are processed from the network via a specified {@link
+ * Network} interface. Responses are committed to cache, if eligible, using a specified {@link
+ * Cache} interface. Valid responses and errors are posted back to the caller via a {@link
+ * ResponseDelivery}.
+ */
+public class NetworkDispatcher extends Thread {
+
+ /** The queue of requests to service. */
+ private final BlockingQueue<Request<?>> mQueue;
+ /** The network interface for processing requests. */
+ private final Network mNetwork;
+ /** The cache to write to. */
+ private final Cache mCache;
+ /** For posting responses and errors. */
+ private final ResponseDelivery mDelivery;
+ /** Used for telling us to die. */
+ private volatile boolean mQuit = false;
+
+ /**
+ * Creates a new network dispatcher thread. You must call {@link #start()} in order to begin
+ * processing.
+ *
+ * @param queue Queue of incoming requests for triage
+ * @param network Network interface to use for performing requests
+ * @param cache Cache interface to use for writing responses to cache
+ * @param delivery Delivery interface to use for posting responses
+ */
+ public NetworkDispatcher(
+ BlockingQueue<Request<?>> queue,
+ Network network,
+ Cache cache,
+ ResponseDelivery delivery) {
+ mQueue = queue;
+ mNetwork = network;
+ mCache = cache;
+ mDelivery = delivery;
+ }
+
+ /**
+ * Forces this dispatcher to quit immediately. If any requests are still in the queue, they are
+ * not guaranteed to be processed.
+ */
+ public void quit() {
+ mQuit = true;
+ interrupt();
+ }
+
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void addTrafficStatsTag(Request<?> request) {
+ // Tag the request (if API >= 14)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ TrafficStats.setThreadStatsTag(request.getTrafficStatsTag());
+ }
+ }
+
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ while (true) {
+ try {
+ processRequest();
+ } catch (InterruptedException e) {
+ // We may have been interrupted because it was time to quit.
+ if (mQuit) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ VolleyLog.e(
+ "Ignoring spurious interrupt of NetworkDispatcher thread; "
+ + "use quit() to terminate it");
+ }
+ }
+ }
+
+ // Extracted to its own method to ensure locals have a constrained liveness scope by the GC.
+ // This is needed to avoid keeping previous request references alive for an indeterminate amount
+ // of time. Update consumer-proguard-rules.pro when modifying this. See also
+ // https://github.com/google/volley/issues/114
+ private void processRequest() throws InterruptedException {
+ // Take a request from the queue.
+ Request<?> request = mQueue.take();
+ processRequest(request);
+ }
+
+ @VisibleForTesting
+ void processRequest(Request<?> request) {
+ long startTimeMs = SystemClock.elapsedRealtime();
+ request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED);
+ try {
+ request.addMarker("network-queue-take");
+
+ // If the request was cancelled already, do not perform the
+ // network request.
+ if (request.isCanceled()) {
+ request.finish("network-discard-cancelled");
+ request.notifyListenerResponseNotUsable();
+ return;
+ }
+
+ addTrafficStatsTag(request);
+
+ // Perform the network request.
+ NetworkResponse networkResponse = mNetwork.performRequest(request);
+ request.addMarker("network-http-complete");
+
+ // If the server returned 304 AND we delivered a response already,
+ // we're done -- don't deliver a second identical response.
+ if (networkResponse.notModified && request.hasHadResponseDelivered()) {
+ request.finish("not-modified");
+ request.notifyListenerResponseNotUsable();
+ return;
+ }
+
+ // Parse the response here on the worker thread.
+ Response<?> response = request.parseNetworkResponse(networkResponse);
+ request.addMarker("network-parse-complete");
+
+ // Write to cache if applicable.
+ // TODO: Only update cache metadata instead of entire record for 304s.
+ if (request.shouldCache() && response.cacheEntry != null) {
+ mCache.put(request.getCacheKey(), response.cacheEntry);
+ request.addMarker("network-cache-written");
+ }
+
+ // Post the response back.
+ request.markDelivered();
+ mDelivery.postResponse(request, response);
+ request.notifyListenerResponseReceived(response);
+ } catch (VolleyError volleyError) {
+ volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
+ parseAndDeliverNetworkError(request, volleyError);
+ request.notifyListenerResponseNotUsable();
+ } catch (Exception e) {
+ VolleyLog.e(e, "Unhandled exception %s", e.toString());
+ VolleyError volleyError = new VolleyError(e);
+ volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
+ mDelivery.postError(request, volleyError);
+ request.notifyListenerResponseNotUsable();
+ } finally {
+ request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED);
+ }
+ }
+
+ private void parseAndDeliverNetworkError(Request<?> request, VolleyError error) {
+ error = request.parseNetworkError(error);
+ mDelivery.postError(request, error);
+ }
+}
diff --git a/core/src/main/java/com/android/volley/NetworkError.java b/core/src/main/java/com/android/volley/NetworkError.java
new file mode 100644
index 0000000..6b2b19f
--- /dev/null
+++ b/core/src/main/java/com/android/volley/NetworkError.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+/** Indicates that there was a network error when performing a Volley request. */
+@SuppressWarnings("serial")
+public class NetworkError extends VolleyError {
+ public NetworkError() {
+ super();
+ }
+
+ public NetworkError(Throwable cause) {
+ super(cause);
+ }
+
+ public NetworkError(NetworkResponse networkResponse) {
+ super(networkResponse);
+ }
+}
diff --git a/core/src/main/java/com/android/volley/NetworkResponse.java b/core/src/main/java/com/android/volley/NetworkResponse.java
new file mode 100644
index 0000000..cfbc371
--- /dev/null
+++ b/core/src/main/java/com/android/volley/NetworkResponse.java
@@ -0,0 +1,198 @@
+/*
+ * 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 androidx.annotation.Nullable;
+import java.net.HttpURLConnection;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/** Data and headers returned from {@link Network#performRequest(Request)}. */
+public class NetworkResponse {
+
+ /**
+ * Creates a new network response.
+ *
+ * @param statusCode the HTTP status code
+ * @param data Response body
+ * @param headers Headers returned with this response, or null for none
+ * @param notModified True if the server returned a 304 and the data was already in cache
+ * @param networkTimeMs Round-trip network time to receive network response
+ * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor
+ * cannot handle server responses containing multiple headers with the same name. This
+ * constructor may be removed in a future release of Volley.
+ */
+ @Deprecated
+ public NetworkResponse(
+ int statusCode,
+ byte[] data,
+ @Nullable Map<String, String> headers,
+ boolean notModified,
+ long networkTimeMs) {
+ this(statusCode, data, headers, toAllHeaderList(headers), notModified, networkTimeMs);
+ }
+
+ /**
+ * Creates a new network response.
+ *
+ * @param statusCode the HTTP status code
+ * @param data Response body
+ * @param notModified True if the server returned a 304 and the data was already in cache
+ * @param networkTimeMs Round-trip network time to receive network response
+ * @param allHeaders All headers returned with this response, or null for none
+ */
+ public NetworkResponse(
+ int statusCode,
+ byte[] data,
+ boolean notModified,
+ long networkTimeMs,
+ @Nullable List<Header> allHeaders) {
+ this(statusCode, data, toHeaderMap(allHeaders), allHeaders, notModified, networkTimeMs);
+ }
+
+ /**
+ * Creates a new network response.
+ *
+ * @param statusCode the HTTP status code
+ * @param data Response body
+ * @param headers Headers returned with this response, or null for none
+ * @param notModified True if the server returned a 304 and the data was already in cache
+ * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor
+ * cannot handle server responses containing multiple headers with the same name. This
+ * constructor may be removed in a future release of Volley.
+ */
+ @Deprecated
+ public NetworkResponse(
+ int statusCode,
+ byte[] data,
+ @Nullable Map<String, String> headers,
+ boolean notModified) {
+ this(statusCode, data, headers, notModified, /* networkTimeMs= */ 0);
+ }
+
+ /**
+ * Creates a new network response for an OK response with no headers.
+ *
+ * @param data Response body
+ */
+ public NetworkResponse(byte[] data) {
+ this(
+ HttpURLConnection.HTTP_OK,
+ data,
+ /* notModified= */ false,
+ /* networkTimeMs= */ 0,
+ Collections.<Header>emptyList());
+ }
+
+ /**
+ * Creates a new network response for an OK response.
+ *
+ * @param data Response body
+ * @param headers Headers returned with this response, or null for none
+ * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor
+ * cannot handle server responses containing multiple headers with the same name. This
+ * constructor may be removed in a future release of Volley.
+ */
+ @Deprecated
+ public NetworkResponse(byte[] data, @Nullable Map<String, String> headers) {
+ this(
+ HttpURLConnection.HTTP_OK,
+ data,
+ headers,
+ /* notModified= */ false,
+ /* networkTimeMs= */ 0);
+ }
+
+ private NetworkResponse(
+ int statusCode,
+ byte[] data,
+ @Nullable Map<String, String> headers,
+ @Nullable List<Header> allHeaders,
+ boolean notModified,
+ long networkTimeMs) {
+ this.statusCode = statusCode;
+ this.data = data;
+ this.headers = headers;
+ if (allHeaders == null) {
+ this.allHeaders = null;
+ } else {
+ this.allHeaders = Collections.unmodifiableList(allHeaders);
+ }
+ this.notModified = notModified;
+ this.networkTimeMs = networkTimeMs;
+ }
+
+ /** The HTTP status code. */
+ public final int statusCode;
+
+ /** Raw data from this response. */
+ public final byte[] data;
+
+ /**
+ * Response headers.
+ *
+ * <p>This map is case-insensitive. It should not be mutated directly.
+ *
+ * <p>Note that if the server returns two headers with the same (case-insensitive) name, this
+ * map will only contain the last one. Use {@link #allHeaders} to inspect all headers returned
+ * by the server.
+ */
+ @Nullable public final Map<String, String> headers;
+
+ /** All response headers. Must not be mutated directly. */
+ @Nullable public final List<Header> allHeaders;
+
+ /** True if the server returned a 304 (Not Modified). */
+ public final boolean notModified;
+
+ /** Network roundtrip time in milliseconds. */
+ public final long networkTimeMs;
+
+ @Nullable
+ private static Map<String, String> toHeaderMap(@Nullable List<Header> allHeaders) {
+ if (allHeaders == null) {
+ return null;
+ }
+ if (allHeaders.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ // Later elements in the list take precedence.
+ for (Header header : allHeaders) {
+ headers.put(header.getName(), header.getValue());
+ }
+ return headers;
+ }
+
+ @Nullable
+ private static List<Header> toAllHeaderList(@Nullable Map<String, String> headers) {
+ if (headers == null) {
+ return null;
+ }
+ if (headers.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List<Header> allHeaders = new ArrayList<>(headers.size());
+ for (Map.Entry<String, String> header : headers.entrySet()) {
+ allHeaders.add(new Header(header.getKey(), header.getValue()));
+ }
+ return allHeaders;
+ }
+}
diff --git a/core/src/main/java/com/android/volley/NoConnectionError.java b/core/src/main/java/com/android/volley/NoConnectionError.java
new file mode 100644
index 0000000..185eb35
--- /dev/null
+++ b/core/src/main/java/com/android/volley/NoConnectionError.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+/** Error indicating that no connection could be established when performing a Volley request. */
+@SuppressWarnings("serial")
+public class NoConnectionError extends NetworkError {
+ public NoConnectionError() {
+ super();
+ }
+
+ public NoConnectionError(Throwable reason) {
+ super(reason);
+ }
+}
diff --git a/core/src/main/java/com/android/volley/ParseError.java b/core/src/main/java/com/android/volley/ParseError.java
new file mode 100644
index 0000000..04a9d58
--- /dev/null
+++ b/core/src/main/java/com/android/volley/ParseError.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+/** Indicates that the server's response could not be parsed. */
+@SuppressWarnings("serial")
+public class ParseError extends VolleyError {
+ public ParseError() {}
+
+ public ParseError(NetworkResponse networkResponse) {
+ super(networkResponse);
+ }
+
+ public ParseError(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/core/src/main/java/com/android/volley/Request.java b/core/src/main/java/com/android/volley/Request.java
new file mode 100644
index 0000000..df0d18f
--- /dev/null
+++ b/core/src/main/java/com/android/volley/Request.java
@@ -0,0 +1,723 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+import android.net.TrafficStats;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import androidx.annotation.CallSuper;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import com.android.volley.VolleyLog.MarkerLog;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Base class for all network requests.
+ *
+ * @param <T> The type of parsed response this request expects.
+ */
+public abstract class Request<T> implements Comparable<Request<T>> {
+
+ /** Default encoding for POST or PUT parameters. See {@link #getParamsEncoding()}. */
+ private static final String DEFAULT_PARAMS_ENCODING = "UTF-8";
+
+ /** Supported request methods. */
+ public interface Method {
+ int DEPRECATED_GET_OR_POST = -1;
+ int GET = 0;
+ int POST = 1;
+ int PUT = 2;
+ int DELETE = 3;
+ int HEAD = 4;
+ int OPTIONS = 5;
+ int TRACE = 6;
+ int PATCH = 7;
+ }
+
+ /** Callback to notify when the network request returns. */
+ /* package */ interface NetworkRequestCompleteListener {
+
+ /** Callback when a network response has been received. */
+ void onResponseReceived(Request<?> request, Response<?> response);
+
+ /** Callback when request returns from network without valid response. */
+ void onNoUsableResponseReceived(Request<?> request);
+ }
+
+ /** An event log tracing the lifetime of this request; for debugging. */
+ private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null;
+
+ /**
+ * Request method of this request. Currently supports GET, POST, PUT, DELETE, HEAD, OPTIONS,
+ * TRACE, and PATCH.
+ */
+ private final int mMethod;
+
+ /** URL of this request. */
+ private final String mUrl;
+
+ /** Default tag for {@link TrafficStats}. */
+ private final int mDefaultTrafficStatsTag;
+
+ /** Lock to guard state which can be mutated after a request is added to the queue. */
+ private final Object mLock = new Object();
+
+ /** Listener interface for errors. */
+ @Nullable
+ @GuardedBy("mLock")
+ private Response.ErrorListener mErrorListener;
+
+ /** Sequence number of this request, used to enforce FIFO ordering. */
+ private Integer mSequence;
+
+ /** The request queue this request is associated with. */
+ private RequestQueue mRequestQueue;
+
+ /** Whether or not responses to this request should be cached. */
+ // TODO(#190): Turn this off by default for anything other than GET requests.
+ private boolean mShouldCache = true;
+
+ /** Whether or not this request has been canceled. */
+ @GuardedBy("mLock")
+ private boolean mCanceled = false;
+
+ /** Whether or not a response has been delivered for this request yet. */
+ @GuardedBy("mLock")
+ private boolean mResponseDelivered = false;
+
+ /** 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;
+
+ /**
+ * When a request can be retrieved from cache but must be refreshed from the network, the cache
+ * entry will be stored here so that in the event of a "Not Modified" response, we can be sure
+ * it hasn't been evicted from cache.
+ */
+ @Nullable private Cache.Entry mCacheEntry = null;
+
+ /** An opaque token tagging this request; used for bulk cancellation. */
+ private Object mTag;
+
+ /** Listener that will be notified when a response has been delivered. */
+ @GuardedBy("mLock")
+ private NetworkRequestCompleteListener mRequestCompleteListener;
+
+ /**
+ * Creates a new request with the given URL and error listener. Note that the normal response
+ * listener is not provided here as delivery of responses is provided by subclasses, who have a
+ * better idea of how to deliver an already-parsed response.
+ *
+ * @deprecated Use {@link #Request(int, String, com.android.volley.Response.ErrorListener)}.
+ */
+ @Deprecated
+ public Request(String url, Response.ErrorListener errorListener) {
+ this(Method.DEPRECATED_GET_OR_POST, url, errorListener);
+ }
+
+ /**
+ * Creates a new request with the given method (one of the values from {@link Method}), URL, and
+ * error listener. Note that the normal response listener is not provided here as delivery of
+ * responses is provided by subclasses, who have a better idea of how to deliver an
+ * already-parsed response.
+ *
+ * @param method the HTTP method to use
+ * @param url URL to fetch the response from
+ * @param errorListener Error listener, or null to ignore errors.
+ */
+ public Request(int method, String url, @Nullable Response.ErrorListener errorListener) {
+ mMethod = method;
+ mUrl = url;
+ mErrorListener = errorListener;
+ setRetryPolicy(new DefaultRetryPolicy());
+
+ mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
+ }
+
+ /** Return the method for this request. Can be one of the values in {@link Method}. */
+ public int getMethod() {
+ return mMethod;
+ }
+
+ /**
+ * Set a tag on this request. Can be used to cancel all requests with this tag by {@link
+ * RequestQueue#cancelAll(Object)}.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public Request<?> setTag(Object tag) {
+ mTag = tag;
+ return this;
+ }
+
+ /**
+ * Returns this request's tag.
+ *
+ * @see Request#setTag(Object)
+ */
+ public Object getTag() {
+ return mTag;
+ }
+
+ /** @return this request's {@link com.android.volley.Response.ErrorListener}. */
+ @Nullable
+ public Response.ErrorListener getErrorListener() {
+ synchronized (mLock) {
+ return mErrorListener;
+ }
+ }
+
+ /** @return A tag for use with {@link TrafficStats#setThreadStatsTag(int)} */
+ public int getTrafficStatsTag() {
+ return mDefaultTrafficStatsTag;
+ }
+
+ /** @return The hashcode of the URL's host component, or 0 if there is none. */
+ private static int findDefaultTrafficStatsTag(String url) {
+ if (!TextUtils.isEmpty(url)) {
+ Uri uri = Uri.parse(url);
+ if (uri != null) {
+ String host = uri.getHost();
+ if (host != null) {
+ return host.hashCode();
+ }
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Sets the retry policy for this request.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public Request<?> setRetryPolicy(RetryPolicy retryPolicy) {
+ mRetryPolicy = retryPolicy;
+ return this;
+ }
+
+ /** Adds an event to this request's event log; for debugging. */
+ public void addMarker(String tag) {
+ if (MarkerLog.ENABLED) {
+ mEventLog.add(tag, Thread.currentThread().getId());
+ }
+ }
+
+ /**
+ * Notifies the request queue that this request has finished (successfully or with error).
+ *
+ * <p>Also dumps all events from this request's event log; for debugging.
+ */
+ void finish(final String tag) {
+ if (mRequestQueue != null) {
+ mRequestQueue.finish(this);
+ }
+ if (MarkerLog.ENABLED) {
+ final long threadId = Thread.currentThread().getId();
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ // If we finish marking off of the main thread, we need to
+ // actually do it on the main thread to ensure correct ordering.
+ Handler mainThread = new Handler(Looper.getMainLooper());
+ mainThread.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ mEventLog.add(tag, threadId);
+ mEventLog.finish(Request.this.toString());
+ }
+ });
+ return;
+ }
+
+ mEventLog.add(tag, threadId);
+ mEventLog.finish(this.toString());
+ }
+ }
+
+ void sendEvent(@RequestQueue.RequestEvent int event) {
+ if (mRequestQueue != null) {
+ mRequestQueue.sendRequestEvent(this, event);
+ }
+ }
+
+ /**
+ * Associates this request with the given queue. The request queue will be notified when this
+ * request has finished.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public Request<?> setRequestQueue(RequestQueue requestQueue) {
+ mRequestQueue = requestQueue;
+ return this;
+ }
+
+ /**
+ * Sets the sequence number of this request. Used by {@link RequestQueue}.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public final Request<?> setSequence(int sequence) {
+ mSequence = sequence;
+ return this;
+ }
+
+ /** Returns the sequence number of this request. */
+ public final int getSequence() {
+ if (mSequence == null) {
+ throw new IllegalStateException("getSequence called before setSequence");
+ }
+ return mSequence;
+ }
+
+ /** Returns the URL of this request. */
+ public String getUrl() {
+ return mUrl;
+ }
+
+ /** Returns the cache key for this request. By default, this is the URL. */
+ public String getCacheKey() {
+ String url = getUrl();
+ // If this is a GET request, just use the URL as the key.
+ // For callers using DEPRECATED_GET_OR_POST, we assume the method is GET, which matches
+ // legacy behavior where all methods had the same cache key. We can't determine which method
+ // will be used because doing so requires calling getPostBody() which is expensive and may
+ // throw AuthFailureError.
+ // TODO(#190): Remove support for non-GET methods.
+ int method = getMethod();
+ if (method == Method.GET || method == Method.DEPRECATED_GET_OR_POST) {
+ return url;
+ }
+ return Integer.toString(method) + '-' + url;
+ }
+
+ /**
+ * Annotates this request with an entry retrieved for it from cache. Used for cache coherency
+ * support.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public Request<?> setCacheEntry(Cache.Entry entry) {
+ mCacheEntry = entry;
+ return this;
+ }
+
+ /** Returns the annotated cache entry, or null if there isn't one. */
+ @Nullable
+ public Cache.Entry getCacheEntry() {
+ return mCacheEntry;
+ }
+
+ /**
+ * Mark this request as canceled.
+ *
+ * <p>No callback will be delivered as long as either:
+ *
+ * <ul>
+ * <li>This method is called on the same thread as the {@link ResponseDelivery} is running on.
+ * By default, this is the main thread.
+ * <li>The request subclass being used overrides cancel() and ensures that it does not invoke
+ * the listener in {@link #deliverResponse} after cancel() has been called in a
+ * thread-safe manner.
+ * </ul>
+ *
+ * <p>There are no guarantees if both of these conditions aren't met.
+ */
+ @CallSuper
+ public void cancel() {
+ synchronized (mLock) {
+ mCanceled = true;
+ mErrorListener = null;
+ }
+ }
+
+ /** Returns true if this request has been canceled. */
+ public boolean isCanceled() {
+ synchronized (mLock) {
+ return mCanceled;
+ }
+ }
+
+ /**
+ * Returns a list of extra HTTP headers to go along with this request. Can throw {@link
+ * AuthFailureError} as authentication may be required to provide these values.
+ *
+ * @throws AuthFailureError In the event of auth failure
+ */
+ public Map<String, String> getHeaders() throws AuthFailureError {
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Returns a Map of POST parameters to be used for this request, or null if a simple GET should
+ * be used. Can throw {@link AuthFailureError} as authentication may be required to provide
+ * these values.
+ *
+ * <p>Note that only one of getPostParams() and getPostBody() can return a non-null value.
+ *
+ * @throws AuthFailureError In the event of auth failure
+ * @deprecated Use {@link #getParams()} instead.
+ */
+ @Deprecated
+ @Nullable
+ protected Map<String, String> getPostParams() throws AuthFailureError {
+ return getParams();
+ }
+
+ /**
+ * Returns which encoding should be used when converting POST parameters returned by {@link
+ * #getPostParams()} into a raw POST body.
+ *
+ * <p>This controls both encodings:
+ *
+ * <ol>
+ * <li>The string encoding used when converting parameter names and values into bytes prior to
+ * URL encoding them.
+ * <li>The string encoding used when converting the URL encoded parameters into a raw byte
+ * array.
+ * </ol>
+ *
+ * @deprecated Use {@link #getParamsEncoding()} instead.
+ */
+ @Deprecated
+ protected String getPostParamsEncoding() {
+ return getParamsEncoding();
+ }
+
+ /** @deprecated Use {@link #getBodyContentType()} instead. */
+ @Deprecated
+ public String getPostBodyContentType() {
+ return getBodyContentType();
+ }
+
+ /**
+ * Returns the raw POST body to be sent.
+ *
+ * @throws AuthFailureError In the event of auth failure
+ * @deprecated Use {@link #getBody()} instead.
+ */
+ @Deprecated
+ public byte[] getPostBody() throws AuthFailureError {
+ // Note: For compatibility with legacy clients of volley, this implementation must remain
+ // here instead of simply calling the getBody() function because this function must
+ // call getPostParams() and getPostParamsEncoding() since legacy clients would have
+ // overridden these two member functions for POST requests.
+ Map<String, String> postParams = getPostParams();
+ if (postParams != null && postParams.size() > 0) {
+ return encodeParameters(postParams, getPostParamsEncoding());
+ }
+ return null;
+ }
+
+ /**
+ * Returns a Map of parameters to be used for a POST or PUT request. Can throw {@link
+ * AuthFailureError} as authentication may be required to provide these values.
+ *
+ * <p>Note that you can directly override {@link #getBody()} for custom data.
+ *
+ * @throws AuthFailureError in the event of auth failure
+ */
+ @Nullable
+ protected Map<String, String> getParams() throws AuthFailureError {
+ return null;
+ }
+
+ /**
+ * Returns which encoding should be used when converting POST or PUT parameters returned by
+ * {@link #getParams()} into a raw POST or PUT body.
+ *
+ * <p>This controls both encodings:
+ *
+ * <ol>
+ * <li>The string encoding used when converting parameter names and values into bytes prior to
+ * URL encoding them.
+ * <li>The string encoding used when converting the URL encoded parameters into a raw byte
+ * array.
+ * </ol>
+ */
+ protected String getParamsEncoding() {
+ return DEFAULT_PARAMS_ENCODING;
+ }
+
+ /** Returns the content type of the POST or PUT body. */
+ public String getBodyContentType() {
+ return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
+ }
+
+ /**
+ * Returns the raw POST or PUT body to be sent.
+ *
+ * <p>By default, the body consists of the request parameters in
+ * application/x-www-form-urlencoded format. When overriding this method, consider overriding
+ * {@link #getBodyContentType()} as well to match the new body format.
+ *
+ * @throws AuthFailureError in the event of auth failure
+ */
+ public byte[] getBody() throws AuthFailureError {
+ Map<String, String> params = getParams();
+ if (params != null && params.size() > 0) {
+ return encodeParameters(params, getParamsEncoding());
+ }
+ return null;
+ }
+
+ /** Converts <code>params</code> into an application/x-www-form-urlencoded encoded string. */
+ private byte[] encodeParameters(Map<String, String> params, String paramsEncoding) {
+ StringBuilder encodedParams = new StringBuilder();
+ try {
+ for (Map.Entry<String, String> entry : params.entrySet()) {
+ if (entry.getKey() == null || entry.getValue() == null) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Request#getParams() or Request#getPostParams() returned a map "
+ + "containing a null key or value: (%s, %s). All keys "
+ + "and values must be non-null.",
+ entry.getKey(), entry.getValue()));
+ }
+ encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding));
+ encodedParams.append('=');
+ encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding));
+ encodedParams.append('&');
+ }
+ return encodedParams.toString().getBytes(paramsEncoding);
+ } catch (UnsupportedEncodingException uee) {
+ throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee);
+ }
+ }
+
+ /**
+ * Set whether or not responses to this request should be cached.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public final Request<?> setShouldCache(boolean shouldCache) {
+ mShouldCache = shouldCache;
+ return this;
+ }
+
+ /** Returns true if responses to this request should be cached. */
+ public final boolean shouldCache() {
+ return mShouldCache;
+ }
+
+ /**
+ * Sets whether or not the request should be retried in the event of an HTTP 5xx (server) error.
+ *
+ * @return This Request object to allow for chaining.
+ */
+ public final Request<?> setShouldRetryServerErrors(boolean shouldRetryServerErrors) {
+ mShouldRetryServerErrors = shouldRetryServerErrors;
+ return this;
+ }
+
+ /**
+ * Returns true if this request should be retried in the event of an HTTP 5xx (server) error.
+ */
+ public final boolean shouldRetryServerErrors() {
+ return mShouldRetryServerErrors;
+ }
+
+ /**
+ * 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.
+ */
+ public enum Priority {
+ LOW,
+ NORMAL,
+ HIGH,
+ IMMEDIATE
+ }
+
+ /** Returns the {@link Priority} of this request; {@link Priority#NORMAL} by default. */
+ public Priority getPriority() {
+ return Priority.NORMAL;
+ }
+
+ /**
+ * Returns the socket timeout in milliseconds per retry attempt. (This value can be changed per
+ * retry attempt if a backoff is specified via backoffTimeout()). If there are no retry attempts
+ * remaining, this will cause delivery of a {@link TimeoutError} error.
+ */
+ public final int getTimeoutMs() {
+ return getRetryPolicy().getCurrentTimeout();
+ }
+
+ /** Returns the retry policy that should be used for this request. */
+ public RetryPolicy getRetryPolicy() {
+ return mRetryPolicy;
+ }
+
+ /**
+ * Mark this request as having a response delivered on it. This can be used later in the
+ * request's lifetime for suppressing identical responses.
+ */
+ public void markDelivered() {
+ synchronized (mLock) {
+ mResponseDelivered = true;
+ }
+ }
+
+ /** Returns true if this request has had a response delivered for it. */
+ public boolean hasHadResponseDelivered() {
+ synchronized (mLock) {
+ return mResponseDelivered;
+ }
+ }
+
+ /**
+ * Subclasses must implement this to parse the raw network response and return an appropriate
+ * response type. This method will be called from a worker thread. The response will not be
+ * delivered if you return null.
+ *
+ * @param response Response from the network
+ * @return The parsed response, or null in the case of an error
+ */
+ protected abstract Response<T> parseNetworkResponse(NetworkResponse response);
+
+ /**
+ * Subclasses can override this method to parse 'networkError' and return a more specific error.
+ *
+ * <p>The default implementation just returns the passed 'networkError'.
+ *
+ * @param volleyError the error retrieved from the network
+ * @return an NetworkError augmented with additional information
+ */
+ protected VolleyError parseNetworkError(VolleyError volleyError) {
+ return volleyError;
+ }
+
+ /**
+ * Subclasses must implement this to perform delivery of the parsed response to their listeners.
+ * The given response is guaranteed to be non-null; responses that fail to parse are not
+ * delivered.
+ *
+ * @param response The parsed response returned by {@link
+ * #parseNetworkResponse(NetworkResponse)}
+ */
+ protected abstract void deliverResponse(T response);
+
+ /**
+ * Delivers error message to the ErrorListener that the Request was initialized with.
+ *
+ * @param error Error details
+ */
+ public void deliverError(VolleyError error) {
+ Response.ErrorListener listener;
+ synchronized (mLock) {
+ listener = mErrorListener;
+ }
+ if (listener != null) {
+ listener.onErrorResponse(error);
+ }
+ }
+
+ /**
+ * {@link NetworkRequestCompleteListener} that will receive callbacks when the request returns
+ * from the network.
+ */
+ /* package */ void setNetworkRequestCompleteListener(
+ NetworkRequestCompleteListener requestCompleteListener) {
+ synchronized (mLock) {
+ mRequestCompleteListener = requestCompleteListener;
+ }
+ }
+
+ /**
+ * Notify NetworkRequestCompleteListener that a valid response has been received which can be
+ * used for other, waiting requests.
+ *
+ * @param response received from the network
+ */
+ /* package */ void notifyListenerResponseReceived(Response<?> response) {
+ NetworkRequestCompleteListener listener;
+ synchronized (mLock) {
+ listener = mRequestCompleteListener;
+ }
+ if (listener != null) {
+ listener.onResponseReceived(this, response);
+ }
+ }
+
+ /**
+ * Notify NetworkRequestCompleteListener that the network request did not result in a response
+ * which can be used for other, waiting requests.
+ */
+ /* package */ void notifyListenerResponseNotUsable() {
+ NetworkRequestCompleteListener listener;
+ synchronized (mLock) {
+ listener = mRequestCompleteListener;
+ }
+ if (listener != null) {
+ listener.onNoUsableResponseReceived(this);
+ }
+ }
+
+ /**
+ * Our comparator sorts from high to low priority, and secondarily by sequence number to provide
+ * FIFO ordering.
+ */
+ @Override
+ public int compareTo(Request<T> other) {
+ Priority left = this.getPriority();
+ Priority right = other.getPriority();
+
+ // High-priority requests are "lesser" so they are sorted to the front.
+ // Equal priorities are sorted by sequence number to provide FIFO ordering.
+ return left == right ? this.mSequence - other.mSequence : right.ordinal() - left.ordinal();
+ }
+
+ @Override
+ public String toString() {
+ String trafficStatsTag = "0x" + Integer.toHexString(getTrafficStatsTag());
+ return (isCanceled() ? "[X] " : "[ ] ")
+ + getUrl()
+ + " "
+ + trafficStatsTag
+ + " "
+ + getPriority()
+ + " "
+ + mSequence;
+ }
+}
diff --git a/core/src/main/java/com/android/volley/RequestQueue.java b/core/src/main/java/com/android/volley/RequestQueue.java
new file mode 100644
index 0000000..6db0b1c
--- /dev/null
+++ b/core/src/main/java/com/android/volley/RequestQueue.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+import android.os.Handler;
+import android.os.Looper;
+import androidx.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A request dispatch queue with a thread pool of dispatchers.
+ *
+ * <p>Calling {@link #add(Request)} will enqueue the given Request for dispatch, resolving from
+ * either cache or network on a worker thread, and then delivering a parsed response on the main
+ * thread.
+ */
+public class RequestQueue {
+
+ /** Callback interface for completed requests. */
+ // TODO: This should not be a generic class, because the request type can't be determined at
+ // compile time, so all calls to onRequestFinished are unsafe. However, changing this would be
+ // an API-breaking change. See also: https://github.com/google/volley/pull/109
+ @Deprecated // Use RequestEventListener instead.
+ public interface RequestFinishedListener<T> {
+ /** Called when a request has finished processing. */
+ void onRequestFinished(Request<T> request);
+ }
+
+ /** Request event types the listeners {@link RequestEventListener} will be notified about. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ RequestEvent.REQUEST_QUEUED,
+ RequestEvent.REQUEST_CACHE_LOOKUP_STARTED,
+ RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED,
+ RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED,
+ RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED,
+ RequestEvent.REQUEST_FINISHED
+ })
+ public @interface RequestEvent {
+ /** The request was added to the queue. */
+ public static final int REQUEST_QUEUED = 0;
+ /** Cache lookup started for the request. */
+ public static final int REQUEST_CACHE_LOOKUP_STARTED = 1;
+ /**
+ * Cache lookup finished for the request and cached response is delivered or request is
+ * queued for network dispatching.
+ */
+ public static final int REQUEST_CACHE_LOOKUP_FINISHED = 2;
+ /** Network dispatch started for the request. */
+ public static final int REQUEST_NETWORK_DISPATCH_STARTED = 3;
+ /** The network dispatch finished for the request and response (if any) is delivered. */
+ public static final int REQUEST_NETWORK_DISPATCH_FINISHED = 4;
+ /**
+ * All the work associated with the request is finished and request is removed from all the
+ * queues.
+ */
+ public static final int REQUEST_FINISHED = 5;
+ }
+
+ /** Callback interface for request life cycle events. */
+ public interface RequestEventListener {
+ /**
+ * Called on every request lifecycle event. Can be called from different threads. The call
+ * is blocking request processing, so any processing should be kept at minimum or moved to
+ * another thread.
+ */
+ void onRequestEvent(Request<?> request, @RequestEvent int event);
+ }
+
+ /** Used for generating monotonically-increasing sequence numbers for requests. */
+ private final AtomicInteger mSequenceGenerator = new AtomicInteger();
+
+ /**
+ * The set of all requests currently being processed by this RequestQueue. A Request will be in
+ * this set if it is waiting in any queue or currently being processed by any dispatcher.
+ */
+ private final Set<Request<?>> mCurrentRequests = new HashSet<>();
+
+ /** The cache triage queue. */
+ private final PriorityBlockingQueue<Request<?>> mCacheQueue = new PriorityBlockingQueue<>();
+
+ /** The queue of requests that are actually going out to the network. */
+ private final PriorityBlockingQueue<Request<?>> mNetworkQueue = new PriorityBlockingQueue<>();
+
+ /** Number of network request dispatcher threads to start. */
+ private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
+
+ /** Cache interface for retrieving and storing responses. */
+ private final Cache mCache;
+
+ /** Network interface for performing requests. */
+ private final Network mNetwork;
+
+ /** Response delivery mechanism. */
+ private final ResponseDelivery mDelivery;
+
+ /** The network dispatchers. */
+ private final NetworkDispatcher[] mDispatchers;
+
+ /** The cache dispatcher. */
+ private CacheDispatcher mCacheDispatcher;
+
+ private final List<RequestFinishedListener> mFinishedListeners = new ArrayList<>();
+
+ /** Collection of listeners for request life cycle events. */
+ private final List<RequestEventListener> mEventListeners = new ArrayList<>();
+
+ /**
+ * Creates the worker pool. Processing will not begin until {@link #start()} is called.
+ *
+ * @param cache A Cache to use for persisting responses to disk
+ * @param network A Network interface for performing HTTP requests
+ * @param threadPoolSize Number of network dispatcher threads to create
+ * @param delivery A ResponseDelivery interface for posting responses and errors
+ */
+ public RequestQueue(
+ Cache cache, Network network, int threadPoolSize, ResponseDelivery delivery) {
+ mCache = cache;
+ mNetwork = network;
+ mDispatchers = new NetworkDispatcher[threadPoolSize];
+ mDelivery = delivery;
+ }
+
+ /**
+ * Creates the worker pool. Processing will not begin until {@link #start()} is called.
+ *
+ * @param cache A Cache to use for persisting responses to disk
+ * @param network A Network interface for performing HTTP requests
+ * @param threadPoolSize Number of network dispatcher threads to create
+ */
+ public RequestQueue(Cache cache, Network network, int threadPoolSize) {
+ this(
+ cache,
+ network,
+ threadPoolSize,
+ new ExecutorDelivery(new Handler(Looper.getMainLooper())));
+ }
+
+ /**
+ * Creates the worker pool. Processing will not begin until {@link #start()} is called.
+ *
+ * @param cache A Cache to use for persisting responses to disk
+ * @param network A Network interface for performing HTTP requests
+ */
+ public RequestQueue(Cache cache, Network network) {
+ this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
+ }
+
+ /** Starts the dispatchers in this queue. */
+ public void start() {
+ stop(); // Make sure any currently running dispatchers are stopped.
+ // Create the cache dispatcher and start it.
+ mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
+ mCacheDispatcher.start();
+
+ // Create network dispatchers (and corresponding threads) up to the pool size.
+ for (int i = 0; i < mDispatchers.length; i++) {
+ NetworkDispatcher networkDispatcher =
+ new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);
+ mDispatchers[i] = networkDispatcher;
+ networkDispatcher.start();
+ }
+ }
+
+ /** Stops the cache and network dispatchers. */
+ public void stop() {
+ if (mCacheDispatcher != null) {
+ mCacheDispatcher.quit();
+ }
+ for (final NetworkDispatcher mDispatcher : mDispatchers) {
+ if (mDispatcher != null) {
+ mDispatcher.quit();
+ }
+ }
+ }
+
+ /** Gets a sequence number. */
+ public int getSequenceNumber() {
+ return mSequenceGenerator.incrementAndGet();
+ }
+
+ /** Gets the {@link Cache} instance being used. */
+ public Cache getCache() {
+ return mCache;
+ }
+
+ /**
+ * A simple predicate or filter interface for Requests, for use by {@link
+ * RequestQueue#cancelAll(RequestFilter)}.
+ */
+ public interface RequestFilter {
+ boolean apply(Request<?> request);
+ }
+
+ /**
+ * Cancels all requests in this queue for which the given filter applies.
+ *
+ * @param filter The filtering function to use
+ */
+ public void cancelAll(RequestFilter filter) {
+ synchronized (mCurrentRequests) {
+ for (Request<?> request : mCurrentRequests) {
+ if (filter.apply(request)) {
+ request.cancel();
+ }
+ }
+ }
+ }
+
+ /**
+ * Cancels all requests in this queue with the given tag. Tag must be non-null and equality is
+ * by identity.
+ */
+ public void cancelAll(final Object tag) {
+ if (tag == null) {
+ throw new IllegalArgumentException("Cannot cancelAll with a null tag");
+ }
+ cancelAll(
+ new RequestFilter() {
+ @Override
+ public boolean apply(Request<?> request) {
+ return request.getTag() == tag;
+ }
+ });
+ }
+
+ /**
+ * Adds a Request to the dispatch queue.
+ *
+ * @param request The request to service
+ * @return The passed-in request
+ */
+ public <T> Request<T> add(Request<T> request) {
+ // Tag the request as belonging to this queue and add it to the set of current requests.
+ request.setRequestQueue(this);
+ synchronized (mCurrentRequests) {
+ mCurrentRequests.add(request);
+ }
+
+ // Process requests in the order they are added.
+ request.setSequence(getSequenceNumber());
+ request.addMarker("add-to-queue");
+ 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()) {
+ sendRequestOverNetwork(request);
+ } else {
+ mCacheQueue.add(request);
+ }
+ }
+
+ /**
+ * Called from {@link Request#finish(String)}, indicating that processing of the given request
+ * has finished.
+ */
+ @SuppressWarnings("unchecked") // see above note on RequestFinishedListener
+ <T> void finish(Request<T> request) {
+ // Remove from the set of requests currently being processed.
+ synchronized (mCurrentRequests) {
+ mCurrentRequests.remove(request);
+ }
+ synchronized (mFinishedListeners) {
+ for (RequestFinishedListener<T> listener : mFinishedListeners) {
+ listener.onRequestFinished(request);
+ }
+ }
+ sendRequestEvent(request, RequestEvent.REQUEST_FINISHED);
+ }
+
+ /** Sends a request life cycle event to the listeners. */
+ void sendRequestEvent(Request<?> request, @RequestEvent int event) {
+ synchronized (mEventListeners) {
+ for (RequestEventListener listener : mEventListeners) {
+ listener.onRequestEvent(request, event);
+ }
+ }
+ }
+
+ /** Add a listener for request life cycle events. */
+ public void addRequestEventListener(RequestEventListener listener) {
+ synchronized (mEventListeners) {
+ mEventListeners.add(listener);
+ }
+ }
+
+ /** Remove a listener for request life cycle events. */
+ public void removeRequestEventListener(RequestEventListener listener) {
+ synchronized (mEventListeners) {
+ mEventListeners.remove(listener);
+ }
+ }
+
+ @Deprecated // Use RequestEventListener instead.
+ public <T> void addRequestFinishedListener(RequestFinishedListener<T> listener) {
+ synchronized (mFinishedListeners) {
+ mFinishedListeners.add(listener);
+ }
+ }
+
+ /** Remove a RequestFinishedListener. Has no effect if listener was not previously added. */
+ @Deprecated // Use RequestEventListener instead.
+ public <T> void removeRequestFinishedListener(RequestFinishedListener<T> listener) {
+ synchronized (mFinishedListeners) {
+ mFinishedListeners.remove(listener);
+ }
+ }
+
+ public ResponseDelivery getResponseDelivery() {
+ return mDelivery;
+ }
+
+ <T> void sendRequestOverNetwork(Request<T> request) {
+ mNetworkQueue.add(request);
+ }
+}
diff --git a/core/src/main/java/com/android/volley/RequestTask.java b/core/src/main/java/com/android/volley/RequestTask.java
new file mode 100644
index 0000000..b429f79
--- /dev/null
+++ b/core/src/main/java/com/android/volley/RequestTask.java
@@ -0,0 +1,20 @@
+package com.android.volley;
+
+/**
+ * Abstract runnable that's a task to be completed by the RequestQueue.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
+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/core/src/main/java/com/android/volley/Response.java b/core/src/main/java/com/android/volley/Response.java
new file mode 100644
index 0000000..622bdc4
--- /dev/null
+++ b/core/src/main/java/com/android/volley/Response.java
@@ -0,0 +1,84 @@
+/*
+ * 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 androidx.annotation.Nullable;
+
+/**
+ * Encapsulates a parsed response for delivery.
+ *
+ * @param <T> Parsed type of this response
+ */
+public class Response<T> {
+
+ /** Callback interface for delivering parsed responses. */
+ public interface Listener<T> {
+ /** Called when a response is received. */
+ void onResponse(T response);
+ }
+
+ /** Callback interface for delivering error responses. */
+ public interface ErrorListener {
+ /**
+ * Callback method that an error has been occurred with the provided error code and optional
+ * user-readable message.
+ */
+ void onErrorResponse(VolleyError error);
+ }
+
+ /** Returns a successful response containing the parsed result. */
+ public static <T> Response<T> success(@Nullable T result, @Nullable Cache.Entry cacheEntry) {
+ return new Response<>(result, cacheEntry);
+ }
+
+ /**
+ * Returns a failed response containing the given error code and an optional localized message
+ * displayed to the user.
+ */
+ public static <T> Response<T> error(VolleyError error) {
+ return new Response<>(error);
+ }
+
+ /** Parsed response, can be null; always null in the case of error. */
+ @Nullable public final T result;
+
+ /** Cache metadata for this response; null if not cached or in the case of error. */
+ @Nullable public final Cache.Entry cacheEntry;
+
+ /** Detailed error information if <code>errorCode != OK</code>. */
+ @Nullable public final VolleyError error;
+
+ /** True if this response was a soft-expired one and a second one MAY be coming. */
+ public boolean intermediate = false;
+
+ /** Returns whether this response is considered successful. */
+ public boolean isSuccess() {
+ return error == null;
+ }
+
+ private Response(@Nullable T result, @Nullable Cache.Entry cacheEntry) {
+ this.result = result;
+ this.cacheEntry = cacheEntry;
+ this.error = null;
+ }
+
+ private Response(VolleyError error) {
+ this.result = null;
+ this.cacheEntry = null;
+ this.error = error;
+ }
+}
diff --git a/core/src/main/java/com/android/volley/ResponseDelivery.java b/core/src/main/java/com/android/volley/ResponseDelivery.java
new file mode 100644
index 0000000..10aa137
--- /dev/null
+++ b/core/src/main/java/com/android/volley/ResponseDelivery.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+public interface ResponseDelivery {
+ /** Parses a response from the network or cache and delivers it. */
+ void postResponse(Request<?> request, Response<?> response);
+
+ /**
+ * Parses a response from the network or cache and delivers it. The provided Runnable will be
+ * executed after delivery.
+ */
+ void postResponse(Request<?> request, Response<?> response, Runnable runnable);
+
+ /** Posts an error for the given request. */
+ void postError(Request<?> request, VolleyError error);
+}
diff --git a/core/src/main/java/com/android/volley/RetryPolicy.java b/core/src/main/java/com/android/volley/RetryPolicy.java
new file mode 100644
index 0000000..3ef26de
--- /dev/null
+++ b/core/src/main/java/com/android/volley/RetryPolicy.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+/**
+ * Retry policy for a request.
+ *
+ * <p>A retry policy can control two parameters:
+ *
+ * <ul>
+ * <li>The number of tries. This can be a simple counter or more complex logic based on the type
+ * of error passed to {@link #retry(VolleyError)}, although {@link #getCurrentRetryCount()}
+ * should always return the current retry count for logging purposes.
+ * <li>The request timeout for each try, via {@link #getCurrentTimeout()}. In the common case that
+ * a request times out before the response has been received from the server, retrying again
+ * with a longer timeout can increase the likelihood of success (at the expense of causing the
+ * user to wait longer, especially if the request still fails).
+ * </ul>
+ *
+ * <p>Note that currently, retries triggered by a retry policy are attempted immediately in sequence
+ * with no delay between them (although the time between tries may increase if the requests are
+ * timing out and {@link #getCurrentTimeout()} is returning increasing values).
+ *
+ * <p>By default, Volley uses {@link DefaultRetryPolicy}.
+ */
+public interface RetryPolicy {
+
+ /** Returns the current timeout (used for logging). */
+ int getCurrentTimeout();
+
+ /** Returns the current retry count (used for logging). */
+ int getCurrentRetryCount();
+
+ /**
+ * Prepares for the next retry by applying a backoff to the timeout.
+ *
+ * @param error The error code of the last attempt.
+ * @throws VolleyError In the event that the retry could not be performed (for example if we ran
+ * out of attempts), the passed in error is thrown.
+ */
+ void retry(VolleyError error) throws VolleyError;
+}
diff --git a/core/src/main/java/com/android/volley/ServerError.java b/core/src/main/java/com/android/volley/ServerError.java
new file mode 100644
index 0000000..84b2eb4
--- /dev/null
+++ b/core/src/main/java/com/android/volley/ServerError.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+/** Indicates that the server responded with an error response. */
+@SuppressWarnings("serial")
+public class ServerError extends VolleyError {
+ public ServerError(NetworkResponse networkResponse) {
+ super(networkResponse);
+ }
+
+ public ServerError() {
+ super();
+ }
+}
diff --git a/core/src/main/java/com/android/volley/TimeoutError.java b/core/src/main/java/com/android/volley/TimeoutError.java
new file mode 100644
index 0000000..227ae08
--- /dev/null
+++ b/core/src/main/java/com/android/volley/TimeoutError.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+/** Indicates that the connection or the socket timed out. */
+@SuppressWarnings("serial")
+public class TimeoutError extends VolleyError {}
diff --git a/core/src/main/java/com/android/volley/VolleyError.java b/core/src/main/java/com/android/volley/VolleyError.java
new file mode 100644
index 0000000..45086da
--- /dev/null
+++ b/core/src/main/java/com/android/volley/VolleyError.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+/** Exception style class encapsulating Volley errors */
+@SuppressWarnings("serial")
+public class VolleyError extends Exception {
+ public final NetworkResponse networkResponse;
+ private long networkTimeMs;
+
+ public VolleyError() {
+ networkResponse = null;
+ }
+
+ public VolleyError(NetworkResponse response) {
+ networkResponse = response;
+ }
+
+ public VolleyError(String exceptionMessage) {
+ super(exceptionMessage);
+ networkResponse = null;
+ }
+
+ public VolleyError(String exceptionMessage, Throwable reason) {
+ super(exceptionMessage, reason);
+ networkResponse = null;
+ }
+
+ public VolleyError(Throwable cause) {
+ super(cause);
+ networkResponse = null;
+ }
+
+ /* package */ void setNetworkTimeMs(long networkTimeMs) {
+ this.networkTimeMs = networkTimeMs;
+ }
+
+ public long getNetworkTimeMs() {
+ return networkTimeMs;
+ }
+}
diff --git a/core/src/main/java/com/android/volley/VolleyLog.java b/core/src/main/java/com/android/volley/VolleyLog.java
new file mode 100644
index 0000000..8477668
--- /dev/null
+++ b/core/src/main/java/com/android/volley/VolleyLog.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley;
+
+import android.os.SystemClock;
+import android.util.Log;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Logging helper class.
+ *
+ * <p>to see Volley logs call:<br>
+ * {@code <android-sdk>/platform-tools/adb shell setprop log.tag.Volley VERBOSE}
+ */
+public class VolleyLog {
+ public static String TAG = "Volley";
+
+ public static boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
+
+ /**
+ * {@link Class#getName()} uses reflection and calling it on a potentially hot code path may
+ * have some cost. To minimize this cost we fetch class name once here and use it later.
+ */
+ private static final String CLASS_NAME = VolleyLog.class.getName();
+
+ /**
+ * Customize the log tag for your application, so that other apps using Volley don't mix their
+ * logs with yours. <br>
+ * Enable the log property for your tag before starting your app: <br>
+ * {@code adb shell setprop log.tag.&lt;tag&gt;}
+ */
+ public static void setTag(String tag) {
+ d("Changing log tag to %s", tag);
+ TAG = tag;
+
+ // Reinitialize the DEBUG "constant"
+ DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
+ }
+
+ public static void v(String format, Object... args) {
+ if (DEBUG) {
+ Log.v(TAG, buildMessage(format, args));
+ }
+ }
+
+ public static void d(String format, Object... args) {
+ Log.d(TAG, buildMessage(format, args));
+ }
+
+ public static void e(String format, Object... args) {
+ Log.e(TAG, buildMessage(format, args));
+ }
+
+ public static void e(Throwable tr, String format, Object... args) {
+ Log.e(TAG, buildMessage(format, args), tr);
+ }
+
+ public static void wtf(String format, Object... args) {
+ Log.wtf(TAG, buildMessage(format, args));
+ }
+
+ public static void wtf(Throwable tr, String format, Object... args) {
+ Log.wtf(TAG, buildMessage(format, args), tr);
+ }
+
+ /**
+ * Formats the caller's provided message and prepends useful info like calling thread ID and
+ * method name.
+ */
+ private static String buildMessage(String format, Object... args) {
+ String msg = (args == null) ? format : String.format(Locale.US, format, args);
+ StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace();
+
+ String caller = "<unknown>";
+ // Walk up the stack looking for the first caller outside of VolleyLog.
+ // It will be at least two frames up, so start there.
+ for (int i = 2; i < trace.length; i++) {
+ String clazz = trace[i].getClassName();
+ if (!clazz.equals(VolleyLog.CLASS_NAME)) {
+ String callingClass = trace[i].getClassName();
+ callingClass = callingClass.substring(callingClass.lastIndexOf('.') + 1);
+ callingClass = callingClass.substring(callingClass.lastIndexOf('$') + 1);
+
+ caller = callingClass + "." + trace[i].getMethodName();
+ break;
+ }
+ }
+ return String.format(Locale.US, "[%d] %s: %s", Thread.currentThread().getId(), caller, msg);
+ }
+
+ /** A simple event log with records containing a name, thread ID, and timestamp. */
+ static class MarkerLog {
+ public static final boolean ENABLED = VolleyLog.DEBUG;
+
+ /** Minimum duration from first marker to last in an marker log to warrant logging. */
+ private static final long MIN_DURATION_FOR_LOGGING_MS = 0;
+
+ private static class Marker {
+ public final String name;
+ public final long thread;
+ public final long time;
+
+ public Marker(String name, long thread, long time) {
+ this.name = name;
+ this.thread = thread;
+ this.time = time;
+ }
+ }
+
+ private final List<Marker> mMarkers = new ArrayList<>();
+ private boolean mFinished = false;
+
+ /** Adds a marker to this log with the specified name. */
+ public synchronized void add(String name, long threadId) {
+ if (mFinished) {
+ throw new IllegalStateException("Marker added to finished log");
+ }
+
+ mMarkers.add(new Marker(name, threadId, SystemClock.elapsedRealtime()));
+ }
+
+ /**
+ * Closes the log, dumping it to logcat if the time difference between the first and last
+ * markers is greater than {@link #MIN_DURATION_FOR_LOGGING_MS}.
+ *
+ * @param header Header string to print above the marker log.
+ */
+ public synchronized void finish(String header) {
+ mFinished = true;
+
+ long duration = getTotalDuration();
+ if (duration <= MIN_DURATION_FOR_LOGGING_MS) {
+ return;
+ }
+
+ long prevTime = mMarkers.get(0).time;
+ d("(%-4d ms) %s", duration, header);
+ for (Marker marker : mMarkers) {
+ long thisTime = marker.time;
+ d("(+%-4d) [%2d] %s", (thisTime - prevTime), marker.thread, marker.name);
+ prevTime = thisTime;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ // Catch requests that have been collected (and hence end-of-lifed)
+ // but had no debugging output printed for them.
+ if (!mFinished) {
+ finish("Request on the loose");
+ e("Marker log finalized without finish() - uncaught exit point for request");
+ }
+ }
+
+ /** Returns the time difference between the first and last events in this log. */
+ private long getTotalDuration() {
+ if (mMarkers.size() == 0) {
+ return 0;
+ }
+
+ long first = mMarkers.get(0).time;
+ long last = mMarkers.get(mMarkers.size() - 1).time;
+ return last - first;
+ }
+ }
+}
diff --git a/core/src/main/java/com/android/volley/WaitingRequestManager.java b/core/src/main/java/com/android/volley/WaitingRequestManager.java
new file mode 100644
index 0000000..682e339
--- /dev/null
+++ b/core/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/core/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java b/core/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java
new file mode 100644
index 0000000..c75c25f
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.Request;
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.http.conn.ConnectTimeoutException;
+
+/**
+ * {@link BaseHttpStack} implementation wrapping a {@link HttpStack}.
+ *
+ * <p>{@link BasicNetwork} uses this if it is provided a {@link HttpStack} at construction time,
+ * allowing it to have one implementation based atop {@link BaseHttpStack}.
+ */
+@SuppressWarnings("deprecation")
+class AdaptedHttpStack extends BaseHttpStack {
+
+ private final HttpStack mHttpStack;
+
+ AdaptedHttpStack(HttpStack httpStack) {
+ mHttpStack = httpStack;
+ }
+
+ @Override
+ public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ org.apache.http.HttpResponse apacheResp;
+ try {
+ apacheResp = mHttpStack.performRequest(request, additionalHeaders);
+ } catch (ConnectTimeoutException e) {
+ // BasicNetwork won't know that this exception should be retried like a timeout, since
+ // it's an Apache-specific error, so wrap it in a standard timeout exception.
+ throw new SocketTimeoutException(e.getMessage());
+ }
+
+ int statusCode = apacheResp.getStatusLine().getStatusCode();
+
+ org.apache.http.Header[] headers = apacheResp.getAllHeaders();
+ List<Header> headerList = new ArrayList<>(headers.length);
+ for (org.apache.http.Header header : headers) {
+ headerList.add(new Header(header.getName(), header.getValue()));
+ }
+
+ if (apacheResp.getEntity() == null) {
+ return new HttpResponse(statusCode, headerList);
+ }
+
+ long contentLength = apacheResp.getEntity().getContentLength();
+ if ((int) contentLength != contentLength) {
+ throw new IOException("Response too large: " + contentLength);
+ }
+
+ return new HttpResponse(
+ statusCode,
+ headerList,
+ (int) apacheResp.getEntity().getContentLength(),
+ apacheResp.getEntity().getContent());
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java b/core/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java
new file mode 100644
index 0000000..f3381ae
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import androidx.annotation.VisibleForTesting;
+import com.android.volley.AuthFailureError;
+
+/**
+ * An Authenticator that uses {@link AccountManager} to get auth tokens of a specified type for a
+ * specified account.
+ */
+// TODO: Update this to account for runtime permissions
+@SuppressLint("MissingPermission")
+public class AndroidAuthenticator implements Authenticator {
+ private final AccountManager mAccountManager;
+ private final Account mAccount;
+ private final String mAuthTokenType;
+ private final boolean mNotifyAuthFailure;
+
+ /**
+ * Creates a new authenticator.
+ *
+ * @param context Context for accessing AccountManager
+ * @param account Account to authenticate as
+ * @param authTokenType Auth token type passed to AccountManager
+ */
+ public AndroidAuthenticator(Context context, Account account, String authTokenType) {
+ this(context, account, authTokenType, /* notifyAuthFailure= */ false);
+ }
+
+ /**
+ * Creates a new authenticator.
+ *
+ * @param context Context for accessing AccountManager
+ * @param account Account to authenticate as
+ * @param authTokenType Auth token type passed to AccountManager
+ * @param notifyAuthFailure Whether to raise a notification upon auth failure
+ */
+ public AndroidAuthenticator(
+ Context context, Account account, String authTokenType, boolean notifyAuthFailure) {
+ this(AccountManager.get(context), account, authTokenType, notifyAuthFailure);
+ }
+
+ @VisibleForTesting
+ AndroidAuthenticator(
+ AccountManager accountManager,
+ Account account,
+ String authTokenType,
+ boolean notifyAuthFailure) {
+ mAccountManager = accountManager;
+ mAccount = account;
+ mAuthTokenType = authTokenType;
+ mNotifyAuthFailure = notifyAuthFailure;
+ }
+
+ /** Returns the Account being used by this authenticator. */
+ public Account getAccount() {
+ return mAccount;
+ }
+
+ /** Returns the Auth Token Type used by this authenticator. */
+ public String getAuthTokenType() {
+ return mAuthTokenType;
+ }
+
+ // TODO: Figure out what to do about notifyAuthFailure
+ @SuppressWarnings("deprecation")
+ @Override
+ public String getAuthToken() throws AuthFailureError {
+ AccountManagerFuture<Bundle> future =
+ mAccountManager.getAuthToken(
+ mAccount,
+ mAuthTokenType,
+ mNotifyAuthFailure,
+ /* callback= */ null,
+ /* handler= */ null);
+ Bundle result;
+ try {
+ result = future.getResult();
+ } catch (Exception e) {
+ throw new AuthFailureError("Error while retrieving auth token", e);
+ }
+ String authToken = null;
+ if (future.isDone() && !future.isCancelled()) {
+ if (result.containsKey(AccountManager.KEY_INTENT)) {
+ Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
+ throw new AuthFailureError(intent);
+ }
+ authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
+ }
+ if (authToken == null) {
+ throw new AuthFailureError("Got null auth token for type: " + mAuthTokenType);
+ }
+
+ return authToken;
+ }
+
+ @Override
+ public void invalidateAuthToken(String authToken) {
+ mAccountManager.invalidateAuthToken(mAccount.type, authToken);
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java b/core/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java
new file mode 100644
index 0000000..4165637
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java
@@ -0,0 +1,175 @@
+/*
+ * 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.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
+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/core/src/main/java/com/android/volley/toolbox/Authenticator.java b/core/src/main/java/com/android/volley/toolbox/Authenticator.java
new file mode 100644
index 0000000..2ba43db
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/Authenticator.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import com.android.volley.AuthFailureError;
+
+/** An interface for interacting with auth tokens. */
+public interface Authenticator {
+ /**
+ * Synchronously retrieves an auth token.
+ *
+ * @throws AuthFailureError If authentication did not succeed
+ */
+ String getAuthToken() throws AuthFailureError;
+
+ /** Invalidates the provided auth token. */
+ void invalidateAuthToken(String authToken);
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/BaseHttpStack.java b/core/src/main/java/com/android/volley/toolbox/BaseHttpStack.java
new file mode 100644
index 0000000..99a9899
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/BaseHttpStack.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.Request;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.StatusLine;
+import org.apache.http.entity.BasicHttpEntity;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.message.BasicStatusLine;
+
+/** An HTTP stack abstraction. */
+@SuppressWarnings("deprecation") // for HttpStack
+public abstract class BaseHttpStack implements HttpStack {
+
+ /**
+ * Performs an HTTP request with the given parameters.
+ *
+ * <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
+ * and the Content-Type header is set to request.getPostBodyContentType().
+ *
+ * @param request the request to perform
+ * @param additionalHeaders additional headers to be sent together with {@link
+ * Request#getHeaders()}
+ * @return the {@link HttpResponse}
+ * @throws SocketTimeoutException if the request times out
+ * @throws IOException if another I/O error occurs during the request
+ * @throws AuthFailureError if an authentication failure occurs during the request
+ */
+ public abstract HttpResponse executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError;
+
+ /**
+ * @deprecated use {@link #executeRequest} instead to avoid a dependency on the deprecated
+ * Apache HTTP library. Nothing in Volley's own source calls this method. However, since
+ * {@link BasicNetwork#mHttpStack} is exposed to subclasses, we provide this implementation
+ * in case legacy client apps are dependent on that field. This method may be removed in a
+ * future release of Volley.
+ */
+ @Deprecated
+ @Override
+ public final org.apache.http.HttpResponse performRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ HttpResponse response = executeRequest(request, additionalHeaders);
+
+ ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
+ StatusLine statusLine =
+ new BasicStatusLine(
+ protocolVersion, response.getStatusCode(), /* reasonPhrase= */ "");
+ BasicHttpResponse apacheResponse = new BasicHttpResponse(statusLine);
+
+ List<org.apache.http.Header> headers = new ArrayList<>();
+ for (Header header : response.getHeaders()) {
+ headers.add(new BasicHeader(header.getName(), header.getValue()));
+ }
+ apacheResponse.setHeaders(headers.toArray(new org.apache.http.Header[0]));
+
+ InputStream responseStream = response.getContent();
+ if (responseStream != null) {
+ BasicHttpEntity entity = new BasicHttpEntity();
+ entity.setContent(responseStream);
+ entity.setContentLength(response.getContentLength());
+ apacheResponse.setEntity(entity);
+ }
+
+ return apacheResponse;
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java b/core/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java
new file mode 100644
index 0000000..cdedaff
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java
@@ -0,0 +1,320 @@
+/*
+ * 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 com.android.volley.toolbox.NetworkUtility.RetryInfo;
+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}.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
+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 {
+ RetryInfo retryInfo =
+ NetworkUtility.shouldRetryException(
+ request, exception, requestStartMs, httpResponse, responseContents);
+ // RetryPolicy#retry may need a background thread, so invoke in the blocking executor.
+ getBlockingExecutor()
+ .execute(new InvokeRetryPolicyTask<>(request, retryInfo, callback));
+ } catch (VolleyError volleyError) {
+ callback.onError(volleyError);
+ }
+ }
+
+ private class InvokeRetryPolicyTask<T> extends RequestTask<T> {
+ final Request<T> request;
+ final RetryInfo retryInfo;
+ final OnRequestComplete callback;
+
+ InvokeRetryPolicyTask(Request<T> request, RetryInfo retryInfo, OnRequestComplete callback) {
+ super(request);
+ this.request = request;
+ this.retryInfo = retryInfo;
+ this.callback = callback;
+ }
+
+ @Override
+ public void run() {
+ try {
+ NetworkUtility.attemptRetryOnException(request, retryInfo);
+ // attemptRetryOnException didn't throw, so proceed with the next attempt.
+ performRequest(request, callback);
+ } catch (VolleyError e) {
+ callback.onError(e);
+ }
+ }
+ }
+
+ @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/core/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/core/src/main/java/com/android/volley/toolbox/BasicNetwork.java
new file mode 100644
index 0000000..552e628
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/BasicNetwork.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import android.os.SystemClock;
+import com.android.volley.Header;
+import com.android.volley.Network;
+import com.android.volley.NetworkResponse;
+import com.android.volley.Request;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.NetworkUtility.RetryInfo;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/** A network performing Volley requests over an {@link HttpStack}. */
+public class BasicNetwork implements Network {
+ private static final int DEFAULT_POOL_SIZE = 4096;
+
+ /**
+ * @deprecated Should never have been exposed in the API. This field may be removed in a future
+ * release of Volley.
+ */
+ @Deprecated protected final HttpStack mHttpStack;
+
+ private final BaseHttpStack mBaseHttpStack;
+
+ protected final ByteArrayPool mPool;
+
+ /**
+ * @param httpStack HTTP stack to be used
+ * @deprecated use {@link #BasicNetwork(BaseHttpStack)} instead to avoid depending on Apache
+ * HTTP. This method may be removed in a future release of Volley.
+ */
+ @Deprecated
+ public BasicNetwork(HttpStack httpStack) {
+ // If a pool isn't passed in, then build a small default pool that will give us a lot of
+ // benefit and not use too much memory.
+ this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
+ }
+
+ /**
+ * @param httpStack HTTP stack to be used
+ * @param pool a buffer pool that improves GC performance in copy operations
+ * @deprecated use {@link #BasicNetwork(BaseHttpStack, ByteArrayPool)} instead to avoid
+ * depending on Apache HTTP. This method may be removed in a future release of Volley.
+ */
+ @Deprecated
+ public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
+ mHttpStack = httpStack;
+ mBaseHttpStack = new AdaptedHttpStack(httpStack);
+ mPool = pool;
+ }
+
+ /** @param httpStack HTTP stack to be used */
+ public BasicNetwork(BaseHttpStack httpStack) {
+ // If a pool isn't passed in, then build a small default pool that will give us a lot of
+ // benefit and not use too much memory.
+ this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
+ }
+
+ /**
+ * @param httpStack HTTP stack to be used
+ * @param pool a buffer pool that improves GC performance in copy operations
+ */
+ public BasicNetwork(BaseHttpStack httpStack, ByteArrayPool pool) {
+ mBaseHttpStack = httpStack;
+ // Populate mHttpStack for backwards compatibility, since it is a protected field. However,
+ // we won't use it directly here, so clients which don't access it directly won't need to
+ // depend on Apache HTTP.
+ mHttpStack = httpStack;
+ mPool = pool;
+ }
+
+ @Override
+ public NetworkResponse performRequest(Request<?> request) throws VolleyError {
+ long requestStart = SystemClock.elapsedRealtime();
+ while (true) {
+ HttpResponse httpResponse = null;
+ byte[] responseContents = null;
+ List<Header> responseHeaders = Collections.emptyList();
+ try {
+ // Gather headers.
+ Map<String, String> additionalRequestHeaders =
+ 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) {
+ 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 =
+ NetworkUtility.inputStreamToBytes(
+ inputStream, httpResponse.getContentLength(), mPool);
+ } else {
+ // Add 0 byte response as a way of honestly representing a
+ // no-content request.
+ responseContents = new byte[0];
+ }
+
+ // if the request is slow, log it.
+ long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
+ NetworkUtility.logSlowRequests(
+ requestLifetime, request, responseContents, statusCode);
+
+ if (statusCode < 200 || statusCode > 299) {
+ throw new IOException();
+ }
+ return new NetworkResponse(
+ statusCode,
+ responseContents,
+ /* notModified= */ false,
+ SystemClock.elapsedRealtime() - requestStart,
+ responseHeaders);
+ } catch (IOException e) {
+ // This will either throw an exception, breaking us from the loop, or will loop
+ // again and retry the request.
+ RetryInfo retryInfo =
+ NetworkUtility.shouldRetryException(
+ request, e, requestStart, httpResponse, responseContents);
+ // We should already be on a background thread, so we can invoke the retry inline.
+ NetworkUtility.attemptRetryOnException(request, retryInfo);
+ }
+ }
+ }
+
+ /**
+ * Converts Headers[] to Map&lt;String, String&gt;.
+ *
+ * @deprecated Should never have been exposed in the API. This method may be removed in a future
+ * release of Volley.
+ */
+ @Deprecated
+ protected static Map<String, String> convertHeaders(Header[] headers) {
+ Map<String, String> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ for (int i = 0; i < headers.length; i++) {
+ result.put(headers[i].getName(), headers[i].getValue());
+ }
+ return result;
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/ByteArrayPool.java b/core/src/main/java/com/android/volley/toolbox/ByteArrayPool.java
new file mode 100644
index 0000000..0134fa2
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/ByteArrayPool.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * ByteArrayPool is a source and repository of <code>byte[]</code> objects. Its purpose is to supply
+ * those buffers to consumers who need to use them for a short period of time and then dispose of
+ * them. Simply creating and disposing such buffers in the conventional manner can considerable heap
+ * churn and garbage collection delays on Android, which lacks good management of short-lived heap
+ * objects. It may be advantageous to trade off some memory in the form of a permanently allocated
+ * pool of buffers in order to gain heap performance improvements; that is what this class does.
+ *
+ * <p>A good candidate user for this class is something like an I/O system that uses large temporary
+ * <code>byte[]</code> buffers to copy data around. In these use cases, often the consumer wants the
+ * buffer to be a certain minimum size to ensure good performance (e.g. when copying data chunks off
+ * of a stream), but doesn't mind if the buffer is larger than the minimum. Taking this into account
+ * and also to maximize the odds of being able to reuse a recycled buffer, this class is free to
+ * return buffers larger than the requested size. The caller needs to be able to gracefully deal
+ * with getting buffers any size over the minimum.
+ *
+ * <p>If there is not a suitably-sized buffer in its recycling pool when a buffer is requested, this
+ * class will allocate a new buffer and return it.
+ *
+ * <p>This class has no special ownership of buffers it creates; the caller is free to take a buffer
+ * it receives from this pool, use it permanently, and never return it to the pool; additionally, it
+ * is not harmful to return to this pool a buffer that was allocated elsewhere, provided there are
+ * no other lingering references to it.
+ *
+ * <p>This class ensures that the total size of the buffers in its recycling pool never exceeds a
+ * certain byte limit. When a buffer is returned that would cause the pool to exceed the limit,
+ * least-recently-used buffers are disposed.
+ */
+public class ByteArrayPool {
+ /** The buffer pool, arranged both by last use and by buffer size */
+ private final List<byte[]> mBuffersByLastUse = new ArrayList<>();
+
+ private final List<byte[]> mBuffersBySize = new ArrayList<>(64);
+
+ /** The total size of the buffers in the pool */
+ private int mCurrentSize = 0;
+
+ /**
+ * The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay
+ * under this limit.
+ */
+ private final int mSizeLimit;
+
+ /** Compares buffers by size */
+ protected static final Comparator<byte[]> BUF_COMPARATOR =
+ new Comparator<byte[]>() {
+ @Override
+ public int compare(byte[] lhs, byte[] rhs) {
+ return lhs.length - rhs.length;
+ }
+ };
+
+ /** @param sizeLimit the maximum size of the pool, in bytes */
+ public ByteArrayPool(int sizeLimit) {
+ mSizeLimit = sizeLimit;
+ }
+
+ /**
+ * Returns a buffer from the pool if one is available in the requested size, or allocates a new
+ * one if a pooled one is not available.
+ *
+ * @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be
+ * larger.
+ * @return a byte[] buffer is always returned.
+ */
+ public synchronized byte[] getBuf(int len) {
+ for (int i = 0; i < mBuffersBySize.size(); i++) {
+ byte[] buf = mBuffersBySize.get(i);
+ if (buf.length >= len) {
+ mCurrentSize -= buf.length;
+ mBuffersBySize.remove(i);
+ mBuffersByLastUse.remove(buf);
+ return buf;
+ }
+ }
+ return new byte[len];
+ }
+
+ /**
+ * Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted
+ * size.
+ *
+ * @param buf the buffer to return to the pool.
+ */
+ public synchronized void returnBuf(byte[] buf) {
+ if (buf == null || buf.length > mSizeLimit) {
+ return;
+ }
+ mBuffersByLastUse.add(buf);
+ int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);
+ if (pos < 0) {
+ pos = -pos - 1;
+ }
+ mBuffersBySize.add(pos, buf);
+ mCurrentSize += buf.length;
+ trim();
+ }
+
+ /** Removes buffers from the pool until it is under its size limit. */
+ private synchronized void trim() {
+ while (mCurrentSize > mSizeLimit) {
+ byte[] buf = mBuffersByLastUse.remove(0);
+ mBuffersBySize.remove(buf);
+ mCurrentSize -= buf.length;
+ }
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java b/core/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java
new file mode 100644
index 0000000..856ef80
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import android.os.Handler;
+import android.os.Looper;
+import com.android.volley.Cache;
+import com.android.volley.NetworkResponse;
+import com.android.volley.Request;
+import com.android.volley.Response;
+
+/** A synthetic request used for clearing the cache. */
+public class ClearCacheRequest extends Request<Object> {
+ private final Cache mCache;
+ private final Runnable mCallback;
+
+ /**
+ * Creates a synthetic request for clearing the cache.
+ *
+ * @param cache Cache to clear
+ * @param callback Callback to make on the main thread once the cache is clear, or null for none
+ */
+ public ClearCacheRequest(Cache cache, Runnable callback) {
+ super(Method.GET, null, null);
+ mCache = cache;
+ mCallback = callback;
+ }
+
+ @Override
+ public boolean isCanceled() {
+ // This is a little bit of a hack, but hey, why not.
+ mCache.clear();
+ if (mCallback != null) {
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.postAtFrontOfQueue(mCallback);
+ }
+ return true;
+ }
+
+ @Override
+ public Priority getPriority() {
+ return Priority.IMMEDIATE;
+ }
+
+ @Override
+ protected Response<Object> parseNetworkResponse(NetworkResponse response) {
+ return null;
+ }
+
+ @Override
+ protected void deliverResponse(Object response) {}
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/core/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
new file mode 100644
index 0000000..d4310e0
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
@@ -0,0 +1,677 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import android.os.SystemClock;
+import android.text.TextUtils;
+import androidx.annotation.VisibleForTesting;
+import com.android.volley.Cache;
+import com.android.volley.Header;
+import com.android.volley.VolleyLog;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Cache implementation that caches files directly onto the hard disk in the specified directory.
+ * The default disk usage size is 5MB, but is configurable.
+ *
+ * <p>This cache supports the {@link Entry#allResponseHeaders} headers field.
+ */
+public class DiskBasedCache implements Cache {
+
+ /** Map of the Key, CacheHeader pairs */
+ private final Map<String, CacheHeader> mEntries = new LinkedHashMap<>(16, .75f, true);
+
+ /** Total amount of space currently used by the cache in bytes. */
+ private long mTotalSize = 0;
+
+ /** The supplier for the root directory to use for the cache. */
+ private final FileSupplier mRootDirectorySupplier;
+
+ /** The maximum size of the cache in bytes. */
+ private final int mMaxCacheSizeInBytes;
+
+ /** Default maximum disk usage in bytes. */
+ private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
+
+ /** High water mark percentage for the cache */
+ @VisibleForTesting static final float HYSTERESIS_FACTOR = 0.9f;
+
+ /** Magic number for current version of cache file format. */
+ private static final int CACHE_MAGIC = 0x20150306;
+
+ /**
+ * Constructs an instance of the DiskBasedCache at the specified directory.
+ *
+ * @param rootDirectory The root directory of the cache.
+ * @param maxCacheSizeInBytes The maximum size of the cache in bytes. Note that the cache may
+ * briefly exceed this size on disk when writing a new entry that pushes it over the limit
+ * until the ensuing pruning completes.
+ */
+ public DiskBasedCache(final File rootDirectory, int maxCacheSizeInBytes) {
+ mRootDirectorySupplier =
+ new FileSupplier() {
+ @Override
+ public File get() {
+ return rootDirectory;
+ }
+ };
+ mMaxCacheSizeInBytes = maxCacheSizeInBytes;
+ }
+
+ /**
+ * Constructs an instance of the DiskBasedCache at the specified directory.
+ *
+ * @param rootDirectorySupplier The supplier for the root directory of the cache.
+ * @param maxCacheSizeInBytes The maximum size of the cache in bytes. Note that the cache may
+ * briefly exceed this size on disk when writing a new entry that pushes it over the limit
+ * until the ensuing pruning completes.
+ */
+ public DiskBasedCache(FileSupplier rootDirectorySupplier, int maxCacheSizeInBytes) {
+ mRootDirectorySupplier = rootDirectorySupplier;
+ mMaxCacheSizeInBytes = maxCacheSizeInBytes;
+ }
+
+ /**
+ * Constructs an instance of the DiskBasedCache at the specified directory using the default
+ * maximum cache size of 5MB.
+ *
+ * @param rootDirectory The root directory of the cache.
+ */
+ public DiskBasedCache(File rootDirectory) {
+ this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
+ }
+
+ /**
+ * Constructs an instance of the DiskBasedCache at the specified directory using the default
+ * maximum cache size of 5MB.
+ *
+ * @param rootDirectorySupplier The supplier for the root directory of the cache.
+ */
+ public DiskBasedCache(FileSupplier rootDirectorySupplier) {
+ this(rootDirectorySupplier, DEFAULT_DISK_USAGE_BYTES);
+ }
+
+ /** Clears the cache. Deletes all cached files from disk. */
+ @Override
+ public synchronized void clear() {
+ File[] files = mRootDirectorySupplier.get().listFiles();
+ if (files != null) {
+ for (File file : files) {
+ file.delete();
+ }
+ }
+ mEntries.clear();
+ mTotalSize = 0;
+ VolleyLog.d("Cache cleared.");
+ }
+
+ /** Returns the cache entry with the specified key if it exists, null otherwise. */
+ @Override
+ public synchronized Entry get(String key) {
+ CacheHeader entry = mEntries.get(key);
+ // if the entry does not exist, return.
+ if (entry == null) {
+ return null;
+ }
+ File file = getFileForKey(key);
+ try {
+ CountingInputStream cis =
+ new CountingInputStream(
+ new BufferedInputStream(createInputStream(file)), file.length());
+ try {
+ CacheHeader entryOnDisk = CacheHeader.readHeader(cis);
+ if (!TextUtils.equals(key, entryOnDisk.key)) {
+ // File was shared by two keys and now holds data for a different entry!
+ VolleyLog.d(
+ "%s: key=%s, found=%s", file.getAbsolutePath(), key, entryOnDisk.key);
+ // Remove key whose contents on disk have been replaced.
+ removeEntry(key);
+ return null;
+ }
+ byte[] data = streamToBytes(cis, cis.bytesRemaining());
+ return entry.toCacheEntry(data);
+ } finally {
+ // Any IOException thrown here is handled by the below catch block by design.
+ //noinspection ThrowFromFinallyBlock
+ cis.close();
+ }
+ } catch (IOException e) {
+ VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
+ remove(key);
+ return null;
+ }
+ }
+
+ /**
+ * Initializes the DiskBasedCache by scanning for all files currently in the specified root
+ * directory. Creates the root directory if necessary.
+ */
+ @Override
+ public synchronized void initialize() {
+ File rootDirectory = mRootDirectorySupplier.get();
+ if (!rootDirectory.exists()) {
+ if (!rootDirectory.mkdirs()) {
+ VolleyLog.e("Unable to create cache dir %s", rootDirectory.getAbsolutePath());
+ }
+ return;
+ }
+ File[] files = rootDirectory.listFiles();
+ if (files == null) {
+ return;
+ }
+ for (File file : files) {
+ try {
+ long entrySize = file.length();
+ CountingInputStream cis =
+ new CountingInputStream(
+ new BufferedInputStream(createInputStream(file)), entrySize);
+ try {
+ CacheHeader entry = CacheHeader.readHeader(cis);
+ entry.size = entrySize;
+ putEntry(entry.key, entry);
+ } finally {
+ // Any IOException thrown here is handled by the below catch block by design.
+ //noinspection ThrowFromFinallyBlock
+ cis.close();
+ }
+ } catch (IOException e) {
+ //noinspection ResultOfMethodCallIgnored
+ file.delete();
+ }
+ }
+ }
+
+ /**
+ * Invalidates an entry in the cache.
+ *
+ * @param key Cache key
+ * @param fullExpire True to fully expire the entry, false to soft expire
+ */
+ @Override
+ public synchronized void invalidate(String key, boolean fullExpire) {
+ Entry entry = get(key);
+ if (entry != null) {
+ entry.softTtl = 0;
+ if (fullExpire) {
+ entry.ttl = 0;
+ }
+ put(key, entry);
+ }
+ }
+
+ /** Puts the entry with the specified key into the cache. */
+ @Override
+ public synchronized void put(String key, Entry entry) {
+ // If adding this entry would trigger a prune, but pruning would cause the new entry to be
+ // deleted, then skip writing the entry in the first place, as this is just churn.
+ // Note that we don't include the cache header overhead in this calculation for simplicity,
+ // so putting entries which are just below the threshold may still cause this churn.
+ if (mTotalSize + entry.data.length > mMaxCacheSizeInBytes
+ && entry.data.length > mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
+ return;
+ }
+ File file = getFileForKey(key);
+ try {
+ BufferedOutputStream fos = new BufferedOutputStream(createOutputStream(file));
+ CacheHeader e = new CacheHeader(key, entry);
+ boolean success = e.writeHeader(fos);
+ if (!success) {
+ fos.close();
+ VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
+ throw new IOException();
+ }
+ fos.write(entry.data);
+ fos.close();
+ e.size = file.length();
+ putEntry(key, e);
+ pruneIfNeeded();
+ } catch (IOException e) {
+ boolean deleted = file.delete();
+ if (!deleted) {
+ VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
+ }
+ initializeIfRootDirectoryDeleted();
+ }
+ }
+
+ /** Removes the specified key from the cache if it exists. */
+ @Override
+ public synchronized void remove(String key) {
+ boolean deleted = getFileForKey(key).delete();
+ removeEntry(key);
+ if (!deleted) {
+ VolleyLog.d(
+ "Could not delete cache entry for key=%s, filename=%s",
+ key, getFilenameForKey(key));
+ }
+ }
+
+ /**
+ * Creates a pseudo-unique filename for the specified cache key.
+ *
+ * @param key The key to generate a file name for.
+ * @return A pseudo-unique filename.
+ */
+ private String getFilenameForKey(String key) {
+ int firstHalfLength = key.length() / 2;
+ String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
+ localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
+ return localFilename;
+ }
+
+ /** Returns a file object for the given cache key. */
+ public File getFileForKey(String key) {
+ return new File(mRootDirectorySupplier.get(), getFilenameForKey(key));
+ }
+
+ /** Re-initialize the cache if the directory was deleted. */
+ private void initializeIfRootDirectoryDeleted() {
+ if (!mRootDirectorySupplier.get().exists()) {
+ VolleyLog.d("Re-initializing cache after external clearing.");
+ mEntries.clear();
+ mTotalSize = 0;
+ initialize();
+ }
+ }
+
+ /** Represents a supplier for {@link File}s. */
+ public interface FileSupplier {
+ File get();
+ }
+
+ /** Prunes the cache to fit the maximum size. */
+ private void pruneIfNeeded() {
+ if (mTotalSize < mMaxCacheSizeInBytes) {
+ return;
+ }
+ if (VolleyLog.DEBUG) {
+ VolleyLog.v("Pruning old cache entries.");
+ }
+
+ long before = mTotalSize;
+ int prunedFiles = 0;
+ long startTime = SystemClock.elapsedRealtime();
+
+ Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry<String, CacheHeader> entry = iterator.next();
+ CacheHeader e = entry.getValue();
+ boolean deleted = getFileForKey(e.key).delete();
+ if (deleted) {
+ mTotalSize -= e.size;
+ } else {
+ VolleyLog.d(
+ "Could not delete cache entry for key=%s, filename=%s",
+ e.key, getFilenameForKey(e.key));
+ }
+ iterator.remove();
+ prunedFiles++;
+
+ if (mTotalSize < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
+ break;
+ }
+ }
+
+ if (VolleyLog.DEBUG) {
+ VolleyLog.v(
+ "pruned %d files, %d bytes, %d ms",
+ prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
+ }
+ }
+
+ /**
+ * Puts the entry with the specified key into the cache.
+ *
+ * @param key The key to identify the entry by.
+ * @param entry The entry to cache.
+ */
+ private void putEntry(String key, CacheHeader entry) {
+ if (!mEntries.containsKey(key)) {
+ mTotalSize += entry.size;
+ } else {
+ CacheHeader oldEntry = mEntries.get(key);
+ mTotalSize += (entry.size - oldEntry.size);
+ }
+ mEntries.put(key, entry);
+ }
+
+ /** Removes the entry identified by 'key' from the cache. */
+ private void removeEntry(String key) {
+ CacheHeader removed = mEntries.remove(key);
+ if (removed != null) {
+ mTotalSize -= removed.size;
+ }
+ }
+
+ /**
+ * Reads length bytes from CountingInputStream into byte array.
+ *
+ * @param cis input stream
+ * @param length number of bytes to read
+ * @throws IOException if fails to read all bytes
+ */
+ @VisibleForTesting
+ static byte[] streamToBytes(CountingInputStream cis, long length) throws IOException {
+ long maxLength = cis.bytesRemaining();
+ // Length cannot be negative or greater than bytes remaining, and must not overflow int.
+ if (length < 0 || length > maxLength || (int) length != length) {
+ throw new IOException("streamToBytes length=" + length + ", maxLength=" + maxLength);
+ }
+ byte[] bytes = new byte[(int) length];
+ new DataInputStream(cis).readFully(bytes);
+ return bytes;
+ }
+
+ @VisibleForTesting
+ InputStream createInputStream(File file) throws FileNotFoundException {
+ return new FileInputStream(file);
+ }
+
+ @VisibleForTesting
+ OutputStream createOutputStream(File file) throws FileNotFoundException {
+ return new FileOutputStream(file);
+ }
+
+ /** Handles holding onto the cache headers for an entry. */
+ @VisibleForTesting
+ static class CacheHeader {
+ /**
+ * The size of the data identified by this CacheHeader on disk (both header and data).
+ *
+ * <p>Must be set by the caller after it has been calculated.
+ *
+ * <p>This is not serialized to disk.
+ */
+ long size;
+
+ /** The key that identifies the cache entry. */
+ final String key;
+
+ /** ETag for cache coherence. */
+ final String etag;
+
+ /** Date of this response as reported by the server. */
+ final long serverDate;
+
+ /** The last modified date for the requested object. */
+ final long lastModified;
+
+ /** TTL for this record. */
+ final long ttl;
+
+ /** Soft TTL for this record. */
+ final long softTtl;
+
+ /** Headers from the response resulting in this cache entry. */
+ final List<Header> allResponseHeaders;
+
+ private CacheHeader(
+ String key,
+ String etag,
+ long serverDate,
+ long lastModified,
+ long ttl,
+ long softTtl,
+ List<Header> allResponseHeaders) {
+ this.key = key;
+ this.etag = "".equals(etag) ? null : etag;
+ this.serverDate = serverDate;
+ this.lastModified = lastModified;
+ this.ttl = ttl;
+ this.softTtl = softTtl;
+ this.allResponseHeaders = allResponseHeaders;
+ }
+
+ /**
+ * Instantiates a new CacheHeader object.
+ *
+ * @param key The key that identifies the cache entry
+ * @param entry The cache entry.
+ */
+ CacheHeader(String key, Entry entry) {
+ this(
+ key,
+ entry.etag,
+ entry.serverDate,
+ entry.lastModified,
+ entry.ttl,
+ entry.softTtl,
+ getAllResponseHeaders(entry));
+ }
+
+ private static List<Header> getAllResponseHeaders(Entry entry) {
+ // If the entry contains all the response headers, use that field directly.
+ if (entry.allResponseHeaders != null) {
+ return entry.allResponseHeaders;
+ }
+
+ // Legacy fallback - copy headers from the map.
+ return HttpHeaderParser.toAllHeaderList(entry.responseHeaders);
+ }
+
+ /**
+ * Reads the header from a CountingInputStream and returns a CacheHeader object.
+ *
+ * @param is The InputStream to read from.
+ * @throws IOException if fails to read header
+ */
+ static CacheHeader readHeader(CountingInputStream is) throws IOException {
+ int magic = readInt(is);
+ if (magic != CACHE_MAGIC) {
+ // don't bother deleting, it'll get pruned eventually
+ throw new IOException();
+ }
+ String key = readString(is);
+ String etag = readString(is);
+ long serverDate = readLong(is);
+ long lastModified = readLong(is);
+ long ttl = readLong(is);
+ long softTtl = readLong(is);
+ List<Header> allResponseHeaders = readHeaderList(is);
+ return new CacheHeader(
+ key, etag, serverDate, lastModified, ttl, softTtl, allResponseHeaders);
+ }
+
+ /** Creates a cache entry for the specified data. */
+ Entry toCacheEntry(byte[] data) {
+ Entry e = new Entry();
+ e.data = data;
+ e.etag = etag;
+ e.serverDate = serverDate;
+ e.lastModified = lastModified;
+ e.ttl = ttl;
+ e.softTtl = softTtl;
+ e.responseHeaders = HttpHeaderParser.toHeaderMap(allResponseHeaders);
+ e.allResponseHeaders = Collections.unmodifiableList(allResponseHeaders);
+ return e;
+ }
+
+ /** Writes the contents of this CacheHeader to the specified OutputStream. */
+ boolean writeHeader(OutputStream os) {
+ try {
+ writeInt(os, CACHE_MAGIC);
+ writeString(os, key);
+ writeString(os, etag == null ? "" : etag);
+ writeLong(os, serverDate);
+ writeLong(os, lastModified);
+ writeLong(os, ttl);
+ writeLong(os, softTtl);
+ writeHeaderList(allResponseHeaders, os);
+ os.flush();
+ return true;
+ } catch (IOException e) {
+ VolleyLog.d("%s", e.toString());
+ return false;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static class CountingInputStream extends FilterInputStream {
+ private final long length;
+ private long bytesRead;
+
+ CountingInputStream(InputStream in, long length) {
+ super(in);
+ this.length = length;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int result = super.read();
+ if (result != -1) {
+ bytesRead++;
+ }
+ return result;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int count) throws IOException {
+ int result = super.read(buffer, offset, count);
+ if (result != -1) {
+ bytesRead += result;
+ }
+ return result;
+ }
+
+ @VisibleForTesting
+ long bytesRead() {
+ return bytesRead;
+ }
+
+ long bytesRemaining() {
+ return length - bytesRead;
+ }
+ }
+
+ /*
+ * Homebrewed simple serialization system used for reading and writing cache
+ * headers on disk. Once upon a time, this used the standard Java
+ * Object{Input,Output}Stream, but the default implementation relies heavily
+ * on reflection (even for standard types) and generates a ton of garbage.
+ *
+ * TODO: Replace by standard DataInput and DataOutput in next cache version.
+ */
+
+ /**
+ * Simple wrapper around {@link InputStream#read()} that throws EOFException instead of
+ * returning -1.
+ */
+ private static int read(InputStream is) throws IOException {
+ int b = is.read();
+ if (b == -1) {
+ throw new EOFException();
+ }
+ return b;
+ }
+
+ static void writeInt(OutputStream os, int n) throws IOException {
+ os.write((n >> 0) & 0xff);
+ os.write((n >> 8) & 0xff);
+ os.write((n >> 16) & 0xff);
+ os.write((n >> 24) & 0xff);
+ }
+
+ static int readInt(InputStream is) throws IOException {
+ int n = 0;
+ n |= (read(is) << 0);
+ n |= (read(is) << 8);
+ n |= (read(is) << 16);
+ n |= (read(is) << 24);
+ return n;
+ }
+
+ static void writeLong(OutputStream os, long n) throws IOException {
+ os.write((byte) (n >>> 0));
+ os.write((byte) (n >>> 8));
+ os.write((byte) (n >>> 16));
+ os.write((byte) (n >>> 24));
+ os.write((byte) (n >>> 32));
+ os.write((byte) (n >>> 40));
+ os.write((byte) (n >>> 48));
+ os.write((byte) (n >>> 56));
+ }
+
+ static long readLong(InputStream is) throws IOException {
+ long n = 0;
+ n |= ((read(is) & 0xFFL) << 0);
+ n |= ((read(is) & 0xFFL) << 8);
+ n |= ((read(is) & 0xFFL) << 16);
+ n |= ((read(is) & 0xFFL) << 24);
+ n |= ((read(is) & 0xFFL) << 32);
+ n |= ((read(is) & 0xFFL) << 40);
+ n |= ((read(is) & 0xFFL) << 48);
+ n |= ((read(is) & 0xFFL) << 56);
+ return n;
+ }
+
+ static void writeString(OutputStream os, String s) throws IOException {
+ byte[] b = s.getBytes("UTF-8");
+ writeLong(os, b.length);
+ os.write(b, 0, b.length);
+ }
+
+ static String readString(CountingInputStream cis) throws IOException {
+ long n = readLong(cis);
+ byte[] b = streamToBytes(cis, n);
+ return new String(b, "UTF-8");
+ }
+
+ static void writeHeaderList(List<Header> headers, OutputStream os) throws IOException {
+ if (headers != null) {
+ writeInt(os, headers.size());
+ for (Header header : headers) {
+ writeString(os, header.getName());
+ writeString(os, header.getValue());
+ }
+ } else {
+ writeInt(os, 0);
+ }
+ }
+
+ static List<Header> readHeaderList(CountingInputStream cis) throws IOException {
+ int size = readInt(cis);
+ if (size < 0) {
+ throw new IOException("readHeaderList size=" + size);
+ }
+ List<Header> result =
+ (size == 0) ? Collections.<Header>emptyList() : new ArrayList<Header>();
+ for (int i = 0; i < size; i++) {
+ String name = readString(cis).intern();
+ String value = readString(cis).intern();
+ result.add(new Header(name, value));
+ }
+ return result;
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/FileSupplier.java b/core/src/main/java/com/android/volley/toolbox/FileSupplier.java
new file mode 100644
index 0000000..70898a6
--- /dev/null
+++ b/core/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/core/src/main/java/com/android/volley/toolbox/HttpClientStack.java b/core/src/main/java/com/android/volley/toolbox/HttpClientStack.java
new file mode 100644
index 0000000..1e9e4b0
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/HttpClientStack.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.Request.Method;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpTrace;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+
+/**
+ * An HttpStack that performs request over an {@link HttpClient}.
+ *
+ * @deprecated The Apache HTTP library on Android is deprecated. Use {@link HurlStack} or another
+ * {@link BaseHttpStack} implementation.
+ */
+@Deprecated
+public class HttpClientStack implements HttpStack {
+ protected final HttpClient mClient;
+
+ private static final String HEADER_CONTENT_TYPE = "Content-Type";
+
+ public HttpClientStack(HttpClient client) {
+ mClient = client;
+ }
+
+ private static void setHeaders(HttpUriRequest httpRequest, Map<String, String> headers) {
+ for (String key : headers.keySet()) {
+ httpRequest.setHeader(key, headers.get(key));
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static List<NameValuePair> getPostParameterPairs(Map<String, String> postParams) {
+ List<NameValuePair> result = new ArrayList<>(postParams.size());
+ for (String key : postParams.keySet()) {
+ result.add(new BasicNameValuePair(key, postParams.get(key)));
+ }
+ return result;
+ }
+
+ @Override
+ public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders);
+ setHeaders(httpRequest, additionalHeaders);
+ // Request.getHeaders() takes precedence over the given additional (cache) headers) and any
+ // headers set by createHttpRequest (like the Content-Type header).
+ setHeaders(httpRequest, request.getHeaders());
+ onPrepareRequest(httpRequest);
+ HttpParams httpParams = httpRequest.getParams();
+ int timeoutMs = request.getTimeoutMs();
+ // TODO: Reevaluate this connection timeout based on more wide-scale
+ // data collection and possibly different for wifi vs. 3G.
+ HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
+ HttpConnectionParams.setSoTimeout(httpParams, timeoutMs);
+ return mClient.execute(httpRequest);
+ }
+
+ /** Creates the appropriate subclass of HttpUriRequest for passed in request. */
+ @SuppressWarnings("deprecation")
+ /* protected */ static HttpUriRequest createHttpRequest(
+ Request<?> request, Map<String, String> additionalHeaders) throws AuthFailureError {
+ switch (request.getMethod()) {
+ case Method.DEPRECATED_GET_OR_POST:
+ {
+ // This is the deprecated way that needs to be handled for backwards
+ // compatibility.
+ // If the request's post body is null, then the assumption is that the request
+ // is
+ // GET. Otherwise, it is assumed that the request is a POST.
+ byte[] postBody = request.getPostBody();
+ if (postBody != null) {
+ HttpPost postRequest = new HttpPost(request.getUrl());
+ postRequest.addHeader(
+ HEADER_CONTENT_TYPE, request.getPostBodyContentType());
+ HttpEntity entity;
+ entity = new ByteArrayEntity(postBody);
+ postRequest.setEntity(entity);
+ return postRequest;
+ } else {
+ return new HttpGet(request.getUrl());
+ }
+ }
+ case Method.GET:
+ return new HttpGet(request.getUrl());
+ case Method.DELETE:
+ return new HttpDelete(request.getUrl());
+ case Method.POST:
+ {
+ HttpPost postRequest = new HttpPost(request.getUrl());
+ postRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
+ setEntityIfNonEmptyBody(postRequest, request);
+ return postRequest;
+ }
+ case Method.PUT:
+ {
+ HttpPut putRequest = new HttpPut(request.getUrl());
+ putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
+ setEntityIfNonEmptyBody(putRequest, request);
+ return putRequest;
+ }
+ case Method.HEAD:
+ return new HttpHead(request.getUrl());
+ case Method.OPTIONS:
+ return new HttpOptions(request.getUrl());
+ case Method.TRACE:
+ return new HttpTrace(request.getUrl());
+ case Method.PATCH:
+ {
+ HttpPatch patchRequest = new HttpPatch(request.getUrl());
+ patchRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
+ setEntityIfNonEmptyBody(patchRequest, request);
+ return patchRequest;
+ }
+ default:
+ throw new IllegalStateException("Unknown request method.");
+ }
+ }
+
+ private static void setEntityIfNonEmptyBody(
+ HttpEntityEnclosingRequestBase httpRequest, Request<?> request)
+ throws AuthFailureError {
+ byte[] body = request.getBody();
+ if (body != null) {
+ HttpEntity entity = new ByteArrayEntity(body);
+ httpRequest.setEntity(entity);
+ }
+ }
+
+ /**
+ * Called before the request is executed using the underlying HttpClient.
+ *
+ * <p>Overwrite in subclasses to augment the request.
+ */
+ protected void onPrepareRequest(HttpUriRequest request) throws IOException {
+ // Nothing.
+ }
+
+ /**
+ * The HttpPatch class does not exist in the Android framework, so this has been defined here.
+ */
+ public static final class HttpPatch extends HttpEntityEnclosingRequestBase {
+
+ public static final String METHOD_NAME = "PATCH";
+
+ public HttpPatch() {
+ super();
+ }
+
+ public HttpPatch(final URI uri) {
+ super();
+ setURI(uri);
+ }
+
+ /** @throws IllegalArgumentException if the uri is invalid. */
+ public HttpPatch(final String uri) {
+ super();
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return METHOD_NAME;
+ }
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
new file mode 100644
index 0000000..0b29e80
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import 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;
+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 {
+
+ @RestrictTo({Scope.LIBRARY_GROUP})
+ public static final String HEADER_CONTENT_TYPE = "Content-Type";
+
+ private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1";
+
+ private static final String RFC1123_PARSE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
+
+ // Hardcode 'GMT' rather than using 'zzz' since some platforms append an extraneous +00:00.
+ // See #287.
+ private static final String RFC1123_OUTPUT_FORMAT = "EEE, dd MMM yyyy HH:mm:ss 'GMT'";
+
+ /**
+ * Extracts a {@link com.android.volley.Cache.Entry} from a {@link NetworkResponse}.
+ *
+ * @param response The network response to parse headers from
+ * @return a cache entry for the given response, or null if the response is not cacheable.
+ */
+ @Nullable
+ public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
+ long now = System.currentTimeMillis();
+
+ Map<String, String> headers = response.headers;
+ if (headers == null) {
+ return null;
+ }
+
+ long serverDate = 0;
+ long lastModified = 0;
+ long serverExpires = 0;
+ long softExpire = 0;
+ long finalExpire = 0;
+ long maxAge = 0;
+ long staleWhileRevalidate = 0;
+ boolean hasCacheControl = false;
+ boolean mustRevalidate = false;
+
+ String serverEtag = null;
+ String headerValue;
+
+ headerValue = headers.get("Date");
+ if (headerValue != null) {
+ serverDate = parseDateAsEpoch(headerValue);
+ }
+
+ headerValue = headers.get("Cache-Control");
+ if (headerValue != null) {
+ hasCacheControl = true;
+ String[] tokens = headerValue.split(",", 0);
+ for (int i = 0; i < tokens.length; i++) {
+ String token = tokens[i].trim();
+ if (token.equals("no-cache") || token.equals("no-store")) {
+ return null;
+ } else if (token.startsWith("max-age=")) {
+ try {
+ maxAge = Long.parseLong(token.substring(8));
+ } catch (Exception e) {
+ }
+ } else if (token.startsWith("stale-while-revalidate=")) {
+ try {
+ staleWhileRevalidate = Long.parseLong(token.substring(23));
+ } catch (Exception e) {
+ }
+ } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
+ mustRevalidate = true;
+ }
+ }
+ }
+
+ headerValue = headers.get("Expires");
+ if (headerValue != null) {
+ serverExpires = parseDateAsEpoch(headerValue);
+ }
+
+ headerValue = headers.get("Last-Modified");
+ if (headerValue != null) {
+ lastModified = parseDateAsEpoch(headerValue);
+ }
+
+ serverEtag = headers.get("ETag");
+
+ // Cache-Control takes precedence over an Expires header, even if both exist and Expires
+ // is more restrictive.
+ if (hasCacheControl) {
+ softExpire = now + maxAge * 1000;
+ finalExpire = mustRevalidate ? softExpire : softExpire + staleWhileRevalidate * 1000;
+ } else if (serverDate > 0 && serverExpires >= serverDate) {
+ // Default semantic for Expire header in HTTP specification is softExpire.
+ softExpire = now + (serverExpires - serverDate);
+ finalExpire = softExpire;
+ }
+
+ Cache.Entry entry = new Cache.Entry();
+ entry.data = response.data;
+ entry.etag = serverEtag;
+ entry.softTtl = softExpire;
+ entry.ttl = finalExpire;
+ entry.serverDate = serverDate;
+ entry.lastModified = lastModified;
+ entry.responseHeaders = headers;
+ entry.allResponseHeaders = response.allHeaders;
+
+ return entry;
+ }
+
+ /** Parse date in RFC1123 format, and return its value as epoch */
+ public static long parseDateAsEpoch(String dateStr) {
+ try {
+ // Parse date in RFC1123 format if this header contains one
+ return newUsGmtFormatter(RFC1123_PARSE_FORMAT).parse(dateStr).getTime();
+ } catch (ParseException e) {
+ // Date in invalid format, fallback to 0
+ // If the value is either "0" or "-1" we only log to verbose,
+ // these values are pretty common and cause log spam.
+ String message = "Unable to parse dateStr: %s, falling back to 0";
+ if ("0".equals(dateStr) || "-1".equals(dateStr)) {
+ VolleyLog.v(message, dateStr);
+ } else {
+ VolleyLog.e(e, message, dateStr);
+ }
+
+ return 0;
+ }
+ }
+
+ /** Format an epoch date in RFC1123 format. */
+ static String formatEpochAsRfc1123(long epoch) {
+ return newUsGmtFormatter(RFC1123_OUTPUT_FORMAT).format(new Date(epoch));
+ }
+
+ private static SimpleDateFormat newUsGmtFormatter(String format) {
+ SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.US);
+ formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return formatter;
+ }
+
+ /**
+ * Retrieve a charset from headers
+ *
+ * @param headers An {@link java.util.Map} of headers
+ * @param defaultCharset Charset to return if none can be found
+ * @return Returns the charset specified in the Content-Type of this header, or the
+ * defaultCharset if none can be found.
+ */
+ public static String parseCharset(
+ @Nullable Map<String, String> headers, String defaultCharset) {
+ if (headers == null) {
+ return defaultCharset;
+ }
+ String contentType = headers.get(HEADER_CONTENT_TYPE);
+ if (contentType != null) {
+ String[] params = contentType.split(";", 0);
+ for (int i = 1; i < params.length; i++) {
+ String[] pair = params[i].trim().split("=", 0);
+ if (pair.length == 2) {
+ if (pair[0].equals("charset")) {
+ return pair[1];
+ }
+ }
+ }
+ }
+
+ return defaultCharset;
+ }
+
+ /**
+ * Returns the charset specified in the Content-Type of this header, or the HTTP default
+ * (ISO-8859-1) if none can be found.
+ */
+ public static String parseCharset(@Nullable Map<String, String> headers) {
+ return parseCharset(headers, DEFAULT_CONTENT_CHARSET);
+ }
+
+ // Note - these are copied from NetworkResponse to avoid making them public (as needed to access
+ // them from the .toolbox package), which would mean they'd become part of the Volley API.
+ // TODO: Consider obfuscating official releases so we can share utility methods between Volley
+ // and Toolbox without making them public APIs.
+
+ static Map<String, String> toHeaderMap(List<Header> allHeaders) {
+ Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ // Later elements in the list take precedence.
+ for (Header header : allHeaders) {
+ headers.put(header.getName(), header.getValue());
+ }
+ return headers;
+ }
+
+ static List<Header> toAllHeaderList(Map<String, String> headers) {
+ List<Header> allHeaders = new ArrayList<>(headers.size());
+ for (Map.Entry<String, String> header : headers.entrySet()) {
+ allHeaders.add(new Header(header.getKey(), header.getValue()));
+ }
+ return allHeaders;
+ }
+
+ /**
+ * 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/core/src/main/java/com/android/volley/toolbox/HttpResponse.java b/core/src/main/java/com/android/volley/toolbox/HttpResponse.java
new file mode 100644
index 0000000..595f926
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/HttpResponse.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import androidx.annotation.Nullable;
+import com.android.volley.Header;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+
+/** A response from an HTTP server. */
+public final class HttpResponse {
+
+ private final int mStatusCode;
+ private final List<Header> mHeaders;
+ private final int mContentLength;
+ @Nullable private final InputStream mContent;
+ @Nullable private final byte[] mContentBytes;
+
+ /**
+ * Construct a new HttpResponse for an empty response body.
+ *
+ * @param statusCode the HTTP status code of the response
+ * @param headers the response headers
+ */
+ public HttpResponse(int statusCode, List<Header> headers) {
+ this(statusCode, headers, /* contentLength= */ -1, /* content= */ null);
+ }
+
+ /**
+ * Construct a new HttpResponse.
+ *
+ * @param statusCode the HTTP status code of the response
+ * @param headers the response headers
+ * @param contentLength the length of the response content. Ignored if there is no content.
+ * @param content an {@link InputStream} of the response content. May be null to indicate that
+ * the response has no content.
+ */
+ public HttpResponse(
+ int statusCode, List<Header> headers, int contentLength, InputStream content) {
+ mStatusCode = statusCode;
+ mHeaders = headers;
+ mContentLength = contentLength;
+ mContent = content;
+ 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. */
+ public final int getStatusCode() {
+ return mStatusCode;
+ }
+
+ /** Returns the response headers. Must not be mutated directly. */
+ public final List<Header> getHeaders() {
+ return Collections.unmodifiableList(mHeaders);
+ }
+
+ /** Returns the length of the content. Only valid if {@link #getContent} is non-null. */
+ public final int getContentLength() {
+ return mContentLength;
+ }
+
+ /**
+ * 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() {
+ if (mContent != null) {
+ return mContent;
+ } else if (mContentBytes != null) {
+ return new ByteArrayInputStream(mContentBytes);
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/HttpStack.java b/core/src/main/java/com/android/volley/toolbox/HttpStack.java
new file mode 100644
index 0000000..85179a7
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/HttpStack.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import java.io.IOException;
+import java.util.Map;
+import org.apache.http.HttpResponse;
+
+/**
+ * An HTTP stack abstraction.
+ *
+ * @deprecated This interface should be avoided as it depends on the deprecated Apache HTTP library.
+ * Use {@link BaseHttpStack} to avoid this dependency. This class may be removed in a future
+ * release of Volley.
+ */
+@Deprecated
+public interface HttpStack {
+ /**
+ * Performs an HTTP request with the given parameters.
+ *
+ * <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
+ * and the Content-Type header is set to request.getPostBodyContentType().
+ *
+ * @param request the request to perform
+ * @param additionalHeaders additional headers to be sent together with {@link
+ * Request#getHeaders()}
+ * @return the HTTP response
+ */
+ HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError;
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/HurlStack.java b/core/src/main/java/com/android/volley/toolbox/HurlStack.java
new file mode 100644
index 0000000..35c6a72
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/HurlStack.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import androidx.annotation.VisibleForTesting;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.Request;
+import com.android.volley.Request.Method;
+import java.io.DataOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
+
+/** A {@link BaseHttpStack} based on {@link HttpURLConnection}. */
+public class HurlStack extends BaseHttpStack {
+
+ private static final int HTTP_CONTINUE = 100;
+
+ /** An interface for transforming URLs before use. */
+ public interface UrlRewriter extends com.android.volley.toolbox.UrlRewriter {}
+
+ private final UrlRewriter mUrlRewriter;
+ private final SSLSocketFactory mSslSocketFactory;
+
+ public HurlStack() {
+ this(/* urlRewriter = */ null);
+ }
+
+ /** @param urlRewriter Rewriter to use for request URLs */
+ public HurlStack(UrlRewriter urlRewriter) {
+ this(urlRewriter, /* sslSocketFactory = */ null);
+ }
+
+ /**
+ * @param urlRewriter Rewriter to use for request URLs
+ * @param sslSocketFactory SSL factory to use for HTTPS connections
+ */
+ public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) {
+ mUrlRewriter = urlRewriter;
+ mSslSocketFactory = sslSocketFactory;
+ }
+
+ @Override
+ public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ String url = request.getUrl();
+ HashMap<String, String> map = new HashMap<>();
+ map.putAll(additionalHeaders);
+ // Request.getHeaders() takes precedence over the given additional (cache) headers).
+ map.putAll(request.getHeaders());
+ if (mUrlRewriter != null) {
+ String rewritten = mUrlRewriter.rewriteUrl(url);
+ if (rewritten == null) {
+ throw new IOException("URL blocked by rewriter: " + url);
+ }
+ url = rewritten;
+ }
+ URL parsedUrl = new URL(url);
+ HttpURLConnection connection = openConnection(parsedUrl, request);
+ boolean keepConnectionOpen = false;
+ try {
+ for (String headerName : map.keySet()) {
+ connection.setRequestProperty(headerName, map.get(headerName));
+ }
+ setConnectionParametersForRequest(connection, request);
+ // Initialize HttpResponse with data from the HttpURLConnection.
+ int responseCode = connection.getResponseCode();
+ if (responseCode == -1) {
+ // -1 is returned by getResponseCode() if the response code could not be retrieved.
+ // Signal to the caller that something was wrong with the connection.
+ throw new IOException("Could not retrieve response code from HttpUrlConnection.");
+ }
+
+ if (!hasResponseBody(request.getMethod(), responseCode)) {
+ return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields()));
+ }
+
+ // Need to keep the connection open until the stream is consumed by the caller. Wrap the
+ // stream such that close() will disconnect the connection.
+ keepConnectionOpen = true;
+ return new HttpResponse(
+ responseCode,
+ convertHeaders(connection.getHeaderFields()),
+ connection.getContentLength(),
+ createInputStream(request, connection));
+ } finally {
+ if (!keepConnectionOpen) {
+ connection.disconnect();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static List<Header> convertHeaders(Map<String, List<String>> responseHeaders) {
+ List<Header> headerList = new ArrayList<>(responseHeaders.size());
+ for (Map.Entry<String, List<String>> entry : responseHeaders.entrySet()) {
+ // HttpUrlConnection includes the status line as a header with a null key; omit it here
+ // since it's not really a header and the rest of Volley assumes non-null keys.
+ if (entry.getKey() != null) {
+ for (String value : entry.getValue()) {
+ headerList.add(new Header(entry.getKey(), value));
+ }
+ }
+ }
+ return headerList;
+ }
+
+ /**
+ * Checks if a response message contains a body.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3">RFC 7230 section 3.3</a>
+ * @param requestMethod request method
+ * @param responseCode response status code
+ * @return whether the response has a body
+ */
+ private static boolean hasResponseBody(int requestMethod, int responseCode) {
+ return requestMethod != Request.Method.HEAD
+ && !(HTTP_CONTINUE <= responseCode && responseCode < HttpURLConnection.HTTP_OK)
+ && responseCode != HttpURLConnection.HTTP_NO_CONTENT
+ && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED;
+ }
+
+ /**
+ * Wrapper for a {@link HttpURLConnection}'s InputStream which disconnects the connection on
+ * stream close.
+ */
+ static class UrlConnectionInputStream extends FilterInputStream {
+ private final HttpURLConnection mConnection;
+
+ UrlConnectionInputStream(HttpURLConnection connection) {
+ super(inputStreamFromConnection(connection));
+ mConnection = connection;
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ mConnection.disconnect();
+ }
+ }
+
+ /**
+ * Create and return an InputStream from which the response will be read.
+ *
+ * <p>May be overridden by subclasses to manipulate or monitor this input stream.
+ *
+ * @param request current request.
+ * @param connection current connection of request.
+ * @return an InputStream from which the response will be read.
+ */
+ protected InputStream createInputStream(Request<?> request, HttpURLConnection connection) {
+ return new UrlConnectionInputStream(connection);
+ }
+
+ /**
+ * Initializes an {@link InputStream} from the given {@link HttpURLConnection}.
+ *
+ * @param connection
+ * @return an HttpEntity populated with data from <code>connection</code>.
+ */
+ private static InputStream inputStreamFromConnection(HttpURLConnection connection) {
+ InputStream inputStream;
+ try {
+ inputStream = connection.getInputStream();
+ } catch (IOException ioe) {
+ inputStream = connection.getErrorStream();
+ }
+ return inputStream;
+ }
+
+ /** Create an {@link HttpURLConnection} for the specified {@code url}. */
+ protected HttpURLConnection createConnection(URL url) throws IOException {
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+
+ // Workaround for the M release HttpURLConnection not observing the
+ // HttpURLConnection.setFollowRedirects() property.
+ // https://code.google.com/p/android/issues/detail?id=194495
+ connection.setInstanceFollowRedirects(HttpURLConnection.getFollowRedirects());
+
+ return connection;
+ }
+
+ /**
+ * Opens an {@link HttpURLConnection} with parameters.
+ *
+ * @param url
+ * @return an open connection
+ * @throws IOException
+ */
+ private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
+ HttpURLConnection connection = createConnection(url);
+
+ int timeoutMs = request.getTimeoutMs();
+ connection.setConnectTimeout(timeoutMs);
+ connection.setReadTimeout(timeoutMs);
+ connection.setUseCaches(false);
+ connection.setDoInput(true);
+
+ // use caller-provided custom SslSocketFactory, if any, for HTTPS
+ if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
+ ((HttpsURLConnection) connection).setSSLSocketFactory(mSslSocketFactory);
+ }
+
+ return connection;
+ }
+
+ // NOTE: Any request headers added here (via setRequestProperty or addRequestProperty) should be
+ // checked against the existing properties in the connection and not overridden if already set.
+ @SuppressWarnings("deprecation")
+ /* package */ void setConnectionParametersForRequest(
+ HttpURLConnection connection, Request<?> request) throws IOException, AuthFailureError {
+ switch (request.getMethod()) {
+ case Method.DEPRECATED_GET_OR_POST:
+ // This is the deprecated way that needs to be handled for backwards compatibility.
+ // If the request's post body is null, then the assumption is that the request is
+ // GET. Otherwise, it is assumed that the request is a POST.
+ byte[] postBody = request.getPostBody();
+ if (postBody != null) {
+ connection.setRequestMethod("POST");
+ addBody(connection, request, postBody);
+ }
+ break;
+ case Method.GET:
+ // Not necessary to set the request method because connection defaults to GET but
+ // being explicit here.
+ connection.setRequestMethod("GET");
+ break;
+ case Method.DELETE:
+ connection.setRequestMethod("DELETE");
+ break;
+ case Method.POST:
+ connection.setRequestMethod("POST");
+ addBodyIfExists(connection, request);
+ break;
+ case Method.PUT:
+ connection.setRequestMethod("PUT");
+ addBodyIfExists(connection, request);
+ break;
+ case Method.HEAD:
+ connection.setRequestMethod("HEAD");
+ break;
+ case Method.OPTIONS:
+ connection.setRequestMethod("OPTIONS");
+ break;
+ case Method.TRACE:
+ connection.setRequestMethod("TRACE");
+ break;
+ case Method.PATCH:
+ connection.setRequestMethod("PATCH");
+ addBodyIfExists(connection, request);
+ break;
+ default:
+ throw new IllegalStateException("Unknown method type.");
+ }
+ }
+
+ private void addBodyIfExists(HttpURLConnection connection, Request<?> request)
+ throws IOException, AuthFailureError {
+ byte[] body = request.getBody();
+ if (body != null) {
+ addBody(connection, request, body);
+ }
+ }
+
+ private void addBody(HttpURLConnection connection, Request<?> request, byte[] body)
+ throws IOException {
+ // Prepare output. There is no need to set Content-Length explicitly,
+ // since this is handled by HttpURLConnection using the size of the prepared
+ // output stream.
+ connection.setDoOutput(true);
+ // Set the content-type unless it was already set (by Request#getHeaders).
+ if (!connection.getRequestProperties().containsKey(HttpHeaderParser.HEADER_CONTENT_TYPE)) {
+ connection.setRequestProperty(
+ HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType());
+ }
+ DataOutputStream out =
+ new DataOutputStream(createOutputStream(request, connection, body.length));
+ out.write(body);
+ out.close();
+ }
+
+ /**
+ * Create and return an OutputStream to which the request body will be written.
+ *
+ * <p>May be overridden by subclasses to manipulate or monitor this output stream.
+ *
+ * @param request current request.
+ * @param connection current connection of request.
+ * @param length size of stream to write.
+ * @return an OutputStream to which the request body will be written.
+ * @throws IOException if an I/O error occurs while creating the stream.
+ */
+ protected OutputStream createOutputStream(
+ Request<?> request, HttpURLConnection connection, int length) throws IOException {
+ return connection.getOutputStream();
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/ImageLoader.java b/core/src/main/java/com/android/volley/toolbox/ImageLoader.java
new file mode 100644
index 0000000..eece2cf
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/ImageLoader.java
@@ -0,0 +1,541 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.os.Handler;
+import android.os.Looper;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.Response.ErrorListener;
+import com.android.volley.Response.Listener;
+import com.android.volley.ResponseDelivery;
+import com.android.volley.VolleyError;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Helper that handles loading and caching images from remote URLs.
+ *
+ * <p>The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)} and
+ * to pass in the default image listener provided by {@link ImageLoader#getImageListener(ImageView,
+ * int, int)}. Note that all function calls to this class must be made from the main thread, and all
+ * responses will be delivered to the main thread as well. Custom {@link ResponseDelivery}s which
+ * don't use the main thread are not supported.
+ */
+public class ImageLoader {
+ /** RequestQueue for dispatching ImageRequests onto. */
+ private final RequestQueue mRequestQueue;
+
+ /** Amount of time to wait after first response arrives before delivering all responses. */
+ private int mBatchResponseDelayMs = 100;
+
+ /** The cache implementation to be used as an L1 cache before calling into volley. */
+ private final ImageCache mCache;
+
+ /**
+ * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so that we can
+ * coalesce multiple requests to the same URL into a single network request.
+ */
+ private final HashMap<String, BatchedImageRequest> mInFlightRequests = new HashMap<>();
+
+ /** HashMap of the currently pending responses (waiting to be delivered). */
+ private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<>();
+
+ /** Handler to the main thread. */
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+ /** Runnable for in-flight response delivery. */
+ private Runnable mRunnable;
+
+ /**
+ * Simple cache adapter interface. If provided to the ImageLoader, it will be used as an L1
+ * cache before dispatch to Volley. Implementations must not block. Implementation with an
+ * LruCache is recommended.
+ */
+ public interface ImageCache {
+ @Nullable
+ Bitmap getBitmap(String url);
+
+ void putBitmap(String url, Bitmap bitmap);
+ }
+
+ /**
+ * Constructs a new ImageLoader.
+ *
+ * @param queue The RequestQueue to use for making image requests.
+ * @param imageCache The cache to use as an L1 cache.
+ */
+ public ImageLoader(RequestQueue queue, ImageCache imageCache) {
+ mRequestQueue = queue;
+ mCache = imageCache;
+ }
+
+ /**
+ * The default implementation of ImageListener which handles basic functionality of showing a
+ * default image until the network response is received, at which point it will switch to either
+ * the actual image or the error image.
+ *
+ * @param view The imageView that the listener is associated with.
+ * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist.
+ * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist.
+ */
+ public static ImageListener getImageListener(
+ final ImageView view, final int defaultImageResId, final int errorImageResId) {
+ return new ImageListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (errorImageResId != 0) {
+ view.setImageResource(errorImageResId);
+ }
+ }
+
+ @Override
+ public void onResponse(ImageContainer response, boolean isImmediate) {
+ if (response.getBitmap() != null) {
+ view.setImageBitmap(response.getBitmap());
+ } else if (defaultImageResId != 0) {
+ view.setImageResource(defaultImageResId);
+ }
+ }
+ };
+ }
+
+ /**
+ * Interface for the response handlers on image requests.
+ *
+ * <p>The call flow is this: 1. Upon being attached to a request, onResponse(response, true)
+ * will be invoked to reflect any cached data that was already available. If the data was
+ * available, response.getBitmap() will be non-null.
+ *
+ * <p>2. After a network response returns, only one of the following cases will happen: -
+ * onResponse(response, false) will be called if the image was loaded. or - onErrorResponse will
+ * be called if there was an error loading the image.
+ */
+ public interface ImageListener extends ErrorListener {
+ /**
+ * Listens for non-error changes to the loading of the image request.
+ *
+ * @param response Holds all information pertaining to the request, as well as the bitmap
+ * (if it is loaded).
+ * @param isImmediate True if this was called during ImageLoader.get() variants. This can be
+ * used to differentiate between a cached image loading and a network image loading in
+ * order to, for example, run an animation to fade in network loaded images.
+ */
+ void onResponse(ImageContainer response, boolean isImmediate);
+ }
+
+ /**
+ * Checks if the item is available in the cache.
+ *
+ * @param requestUrl The url of the remote image
+ * @param maxWidth The maximum width of the returned image.
+ * @param maxHeight The maximum height of the returned image.
+ * @return True if the item exists in cache, false otherwise.
+ */
+ public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
+ return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
+ }
+
+ /**
+ * Checks if the item is available in the cache.
+ *
+ * <p>Must be called from the main thread.
+ *
+ * @param requestUrl The url of the remote image
+ * @param maxWidth The maximum width of the returned image.
+ * @param maxHeight The maximum height of the returned image.
+ * @param scaleType The scaleType of the imageView.
+ * @return True if the item exists in cache, false otherwise.
+ */
+ @MainThread
+ public boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {
+ Threads.throwIfNotOnMainThread();
+
+ String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
+ return mCache.getBitmap(cacheKey) != null;
+ }
+
+ /**
+ * Returns an ImageContainer for the requested URL.
+ *
+ * <p>The ImageContainer will contain either the specified default bitmap or the loaded bitmap.
+ * If the default was returned, the {@link ImageLoader} will be invoked when the request is
+ * fulfilled.
+ *
+ * @param requestUrl The URL of the image to be loaded.
+ */
+ public ImageContainer get(String requestUrl, final ImageListener listener) {
+ return get(requestUrl, listener, /* maxWidth= */ 0, /* maxHeight= */ 0);
+ }
+
+ /**
+ * Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with {@code
+ * Scaletype == ScaleType.CENTER_INSIDE}.
+ */
+ public ImageContainer get(
+ String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight) {
+ return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
+ }
+
+ /**
+ * Issues a bitmap request with the given URL if that image is not available in the cache, and
+ * returns a bitmap container that contains all of the data relating to the request (as well as
+ * the default image if the requested image is not available).
+ *
+ * <p>Must be called from the main thread.
+ *
+ * @param requestUrl The url of the remote image
+ * @param imageListener The listener to call when the remote image is loaded
+ * @param maxWidth The maximum width of the returned image.
+ * @param maxHeight The maximum height of the returned image.
+ * @param scaleType The ImageViews ScaleType used to calculate the needed image size.
+ * @return A container object that contains all of the properties of the request, as well as the
+ * currently available image (default if remote is not loaded).
+ */
+ @MainThread
+ public ImageContainer get(
+ String requestUrl,
+ ImageListener imageListener,
+ int maxWidth,
+ int maxHeight,
+ ScaleType scaleType) {
+
+ // only fulfill requests that were initiated from the main thread.
+ Threads.throwIfNotOnMainThread();
+
+ final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
+
+ // Try to look up the request in the cache of remote images.
+ Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
+ if (cachedBitmap != null) {
+ // Return the cached bitmap.
+ ImageContainer container =
+ new ImageContainer(
+ cachedBitmap, requestUrl, /* cacheKey= */ null, /* listener= */ null);
+ imageListener.onResponse(container, true);
+ return container;
+ }
+
+ // The bitmap did not exist in the cache, fetch it!
+ ImageContainer imageContainer =
+ new ImageContainer(null, requestUrl, cacheKey, imageListener);
+
+ // Update the caller to let them know that they should use the default bitmap.
+ imageListener.onResponse(imageContainer, true);
+
+ // Check to see if a request is already in-flight or completed but pending batch delivery.
+ BatchedImageRequest request = mInFlightRequests.get(cacheKey);
+ if (request == null) {
+ request = mBatchedResponses.get(cacheKey);
+ }
+ if (request != null) {
+ // If it is, add this request to the list of listeners.
+ request.addContainer(imageContainer);
+ return imageContainer;
+ }
+
+ // The request is not already in flight. Send the new request to the network and
+ // track it.
+ Request<Bitmap> newRequest =
+ makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey);
+
+ mRequestQueue.add(newRequest);
+ mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer));
+ return imageContainer;
+ }
+
+ protected Request<Bitmap> makeImageRequest(
+ String requestUrl,
+ int maxWidth,
+ int maxHeight,
+ ScaleType scaleType,
+ final String cacheKey) {
+ return new ImageRequest(
+ requestUrl,
+ new Listener<Bitmap>() {
+ @Override
+ public void onResponse(Bitmap response) {
+ onGetImageSuccess(cacheKey, response);
+ }
+ },
+ maxWidth,
+ maxHeight,
+ scaleType,
+ Config.RGB_565,
+ new ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ onGetImageError(cacheKey, error);
+ }
+ });
+ }
+
+ /**
+ * Sets the amount of time to wait after the first response arrives before delivering all
+ * responses. Batching can be disabled entirely by passing in 0.
+ *
+ * @param newBatchedResponseDelayMs The time in milliseconds to wait.
+ */
+ public void setBatchedResponseDelay(int newBatchedResponseDelayMs) {
+ mBatchResponseDelayMs = newBatchedResponseDelayMs;
+ }
+
+ /**
+ * Handler for when an image was successfully loaded.
+ *
+ * @param cacheKey The cache key that is associated with the image request.
+ * @param response The bitmap that was returned from the network.
+ */
+ protected void onGetImageSuccess(String cacheKey, Bitmap response) {
+ // cache the image that was fetched.
+ mCache.putBitmap(cacheKey, response);
+
+ // remove the request from the list of in-flight requests.
+ BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
+
+ if (request != null) {
+ // Update the response bitmap.
+ request.mResponseBitmap = response;
+
+ // Send the batched response
+ batchResponse(cacheKey, request);
+ }
+ }
+
+ /**
+ * Handler for when an image failed to load.
+ *
+ * @param cacheKey The cache key that is associated with the image request.
+ */
+ protected void onGetImageError(String cacheKey, VolleyError error) {
+ // Notify the requesters that something failed via a null result.
+ // Remove this request from the list of in-flight requests.
+ BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
+
+ if (request != null) {
+ // Set the error for this request
+ request.setError(error);
+
+ // Send the batched response
+ batchResponse(cacheKey, request);
+ }
+ }
+
+ /** Container object for all of the data surrounding an image request. */
+ public class ImageContainer {
+ /**
+ * The most relevant bitmap for the container. If the image was in cache, the Holder to use
+ * for the final bitmap (the one that pairs to the requested URL).
+ */
+ private Bitmap mBitmap;
+
+ private final ImageListener mListener;
+
+ /** The cache key that was associated with the request */
+ private final String mCacheKey;
+
+ /** The request URL that was specified */
+ private final String mRequestUrl;
+
+ /**
+ * Constructs a BitmapContainer object.
+ *
+ * @param bitmap The final bitmap (if it exists).
+ * @param requestUrl The requested URL for this container.
+ * @param cacheKey The cache key that identifies the requested URL for this container.
+ */
+ public ImageContainer(
+ Bitmap bitmap, String requestUrl, String cacheKey, ImageListener listener) {
+ mBitmap = bitmap;
+ mRequestUrl = requestUrl;
+ mCacheKey = cacheKey;
+ mListener = listener;
+ }
+
+ /**
+ * Releases interest in the in-flight request (and cancels it if no one else is listening).
+ *
+ * <p>Must be called from the main thread.
+ */
+ @MainThread
+ public void cancelRequest() {
+ Threads.throwIfNotOnMainThread();
+
+ if (mListener == null) {
+ return;
+ }
+
+ BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
+ if (request != null) {
+ boolean canceled = request.removeContainerAndCancelIfNecessary(this);
+ if (canceled) {
+ mInFlightRequests.remove(mCacheKey);
+ }
+ } else {
+ // check to see if it is already batched for delivery.
+ request = mBatchedResponses.get(mCacheKey);
+ if (request != null) {
+ request.removeContainerAndCancelIfNecessary(this);
+ if (request.mContainers.size() == 0) {
+ mBatchedResponses.remove(mCacheKey);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
+ */
+ public Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ /** Returns the requested URL for this container. */
+ public String getRequestUrl() {
+ return mRequestUrl;
+ }
+ }
+
+ /**
+ * Wrapper class used to map a Request to the set of active ImageContainer objects that are
+ * interested in its results.
+ */
+ private static class BatchedImageRequest {
+ /** The request being tracked */
+ private final Request<?> mRequest;
+
+ /** The result of the request being tracked by this item */
+ private Bitmap mResponseBitmap;
+
+ /** Error if one occurred for this response */
+ private VolleyError mError;
+
+ /** List of all of the active ImageContainers that are interested in the request */
+ private final List<ImageContainer> mContainers = new ArrayList<>();
+
+ /**
+ * Constructs a new BatchedImageRequest object
+ *
+ * @param request The request being tracked
+ * @param container The ImageContainer of the person who initiated the request.
+ */
+ public BatchedImageRequest(Request<?> request, ImageContainer container) {
+ mRequest = request;
+ mContainers.add(container);
+ }
+
+ /** Set the error for this response */
+ public void setError(VolleyError error) {
+ mError = error;
+ }
+
+ /** Get the error for this response */
+ public VolleyError getError() {
+ return mError;
+ }
+
+ /**
+ * Adds another ImageContainer to the list of those interested in the results of the
+ * request.
+ */
+ public void addContainer(ImageContainer container) {
+ mContainers.add(container);
+ }
+
+ /**
+ * Detaches the bitmap container from the request and cancels the request if no one is left
+ * listening.
+ *
+ * @param container The container to remove from the list
+ * @return True if the request was canceled, false otherwise.
+ */
+ public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
+ mContainers.remove(container);
+ if (mContainers.size() == 0) {
+ mRequest.cancel();
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Starts the runnable for batched delivery of responses if it is not already started.
+ *
+ * @param cacheKey The cacheKey of the response being delivered.
+ * @param request The BatchedImageRequest to be delivered.
+ */
+ private void batchResponse(String cacheKey, BatchedImageRequest request) {
+ mBatchedResponses.put(cacheKey, request);
+ // If we don't already have a batch delivery runnable in flight, make a new one.
+ // Note that this will be used to deliver responses to all callers in mBatchedResponses.
+ if (mRunnable == null) {
+ mRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ for (BatchedImageRequest bir : mBatchedResponses.values()) {
+ for (ImageContainer container : bir.mContainers) {
+ // If one of the callers in the batched request canceled the
+ // request
+ // after the response was received but before it was delivered,
+ // skip them.
+ if (container.mListener == null) {
+ continue;
+ }
+ if (bir.getError() == null) {
+ container.mBitmap = bir.mResponseBitmap;
+ container.mListener.onResponse(container, false);
+ } else {
+ container.mListener.onErrorResponse(bir.getError());
+ }
+ }
+ }
+ mBatchedResponses.clear();
+ mRunnable = null;
+ }
+ };
+ // Post the runnable.
+ mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
+ }
+ }
+
+ /**
+ * Creates a cache key for use with the L1 cache.
+ *
+ * @param url The URL of the request.
+ * @param maxWidth The max-width of the output.
+ * @param maxHeight The max-height of the output.
+ * @param scaleType The scaleType of the imageView.
+ */
+ private static String getCacheKey(
+ String url, int maxWidth, int maxHeight, ScaleType scaleType) {
+ return new StringBuilder(url.length() + 12)
+ .append("#W")
+ .append(maxWidth)
+ .append("#H")
+ .append(maxHeight)
+ .append("#S")
+ .append(scaleType.ordinal())
+ .append(url)
+ .toString();
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/ImageRequest.java b/core/src/main/java/com/android/volley/toolbox/ImageRequest.java
new file mode 100644
index 0000000..32b5aa3
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/ImageRequest.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.widget.ImageView.ScaleType;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import com.android.volley.DefaultRetryPolicy;
+import com.android.volley.NetworkResponse;
+import com.android.volley.ParseError;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.VolleyLog;
+
+/** A canned request for getting an image at a given URL and calling back with a decoded Bitmap. */
+public class ImageRequest extends Request<Bitmap> {
+ /** Socket timeout in milliseconds for image requests */
+ public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000;
+
+ /** Default number of retries for image requests */
+ public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;
+
+ /** Default backoff multiplier for image requests */
+ public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f;
+
+ /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ @Nullable
+ private Response.Listener<Bitmap> mListener;
+
+ private final Config mDecodeConfig;
+ private final int mMaxWidth;
+ private final int mMaxHeight;
+ private final ScaleType mScaleType;
+
+ /** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */
+ private static final Object sDecodeLock = new Object();
+
+ /**
+ * Creates a new image request, decoding to a maximum specified width and height. If both width
+ * and height are zero, the image will be decoded to its natural size. If one of the two is
+ * nonzero, that dimension will be clamped and the other one will be set to preserve the image's
+ * aspect ratio. If both width and height are nonzero, the image will be decoded to be fit in
+ * the rectangle of dimensions width x height while keeping its aspect ratio.
+ *
+ * @param url URL of the image
+ * @param listener Listener to receive the decoded bitmap
+ * @param maxWidth Maximum width to decode this bitmap to, or zero for none
+ * @param maxHeight Maximum height to decode this bitmap to, or zero for none
+ * @param scaleType The ImageViews ScaleType used to calculate the needed image size.
+ * @param decodeConfig Format to decode the bitmap to
+ * @param errorListener Error listener, or null to ignore errors
+ */
+ public ImageRequest(
+ String url,
+ Response.Listener<Bitmap> listener,
+ int maxWidth,
+ int maxHeight,
+ ScaleType scaleType,
+ Config decodeConfig,
+ @Nullable Response.ErrorListener errorListener) {
+ super(Method.GET, url, errorListener);
+ setRetryPolicy(
+ new DefaultRetryPolicy(
+ DEFAULT_IMAGE_TIMEOUT_MS,
+ DEFAULT_IMAGE_MAX_RETRIES,
+ DEFAULT_IMAGE_BACKOFF_MULT));
+ mListener = listener;
+ mDecodeConfig = decodeConfig;
+ mMaxWidth = maxWidth;
+ mMaxHeight = maxHeight;
+ mScaleType = scaleType;
+ }
+
+ /**
+ * For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to the
+ * normal constructor with {@code ScaleType.CENTER_INSIDE}.
+ */
+ @Deprecated
+ public ImageRequest(
+ String url,
+ Response.Listener<Bitmap> listener,
+ int maxWidth,
+ int maxHeight,
+ Config decodeConfig,
+ Response.ErrorListener errorListener) {
+ this(
+ url,
+ listener,
+ maxWidth,
+ maxHeight,
+ ScaleType.CENTER_INSIDE,
+ decodeConfig,
+ errorListener);
+ }
+
+ @Override
+ public Priority getPriority() {
+ return Priority.LOW;
+ }
+
+ /**
+ * Scales one side of a rectangle to fit aspect ratio.
+ *
+ * @param maxPrimary Maximum size of the primary dimension (i.e. width for max width), or zero
+ * to maintain aspect ratio with secondary dimension
+ * @param maxSecondary Maximum size of the secondary dimension, or zero to maintain aspect ratio
+ * with primary dimension
+ * @param actualPrimary Actual size of the primary dimension
+ * @param actualSecondary Actual size of the secondary dimension
+ * @param scaleType The ScaleType used to calculate the needed image size.
+ */
+ private static int getResizedDimension(
+ int maxPrimary,
+ int maxSecondary,
+ int actualPrimary,
+ int actualSecondary,
+ ScaleType scaleType) {
+
+ // If no dominant value at all, just return the actual.
+ if ((maxPrimary == 0) && (maxSecondary == 0)) {
+ return actualPrimary;
+ }
+
+ // If ScaleType.FIT_XY fill the whole rectangle, ignore ratio.
+ if (scaleType == ScaleType.FIT_XY) {
+ if (maxPrimary == 0) {
+ return actualPrimary;
+ }
+ return maxPrimary;
+ }
+
+ // If primary is unspecified, scale primary to match secondary's scaling ratio.
+ if (maxPrimary == 0) {
+ double ratio = (double) maxSecondary / (double) actualSecondary;
+ return (int) (actualPrimary * ratio);
+ }
+
+ if (maxSecondary == 0) {
+ return maxPrimary;
+ }
+
+ double ratio = (double) actualSecondary / (double) actualPrimary;
+ int resized = maxPrimary;
+
+ // If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio.
+ if (scaleType == ScaleType.CENTER_CROP) {
+ if ((resized * ratio) < maxSecondary) {
+ resized = (int) (maxSecondary / ratio);
+ }
+ return resized;
+ }
+
+ if ((resized * ratio) > maxSecondary) {
+ resized = (int) (maxSecondary / ratio);
+ }
+ return resized;
+ }
+
+ @Override
+ protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
+ // Serialize all decode on a global lock to reduce concurrent heap usage.
+ synchronized (sDecodeLock) {
+ try {
+ return doParse(response);
+ } catch (OutOfMemoryError e) {
+ VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
+ return Response.error(new ParseError(e));
+ }
+ }
+ }
+
+ /** The real guts of parseNetworkResponse. Broken out for readability. */
+ private Response<Bitmap> doParse(NetworkResponse response) {
+ byte[] data = response.data;
+ BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
+ Bitmap bitmap = null;
+ if (mMaxWidth == 0 && mMaxHeight == 0) {
+ decodeOptions.inPreferredConfig = mDecodeConfig;
+ bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
+ } else {
+ // If we have to resize this image, first get the natural bounds.
+ decodeOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
+ int actualWidth = decodeOptions.outWidth;
+ int actualHeight = decodeOptions.outHeight;
+
+ // Then compute the dimensions we would ideally like to decode to.
+ int desiredWidth =
+ getResizedDimension(
+ mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType);
+ int desiredHeight =
+ getResizedDimension(
+ mMaxHeight, mMaxWidth, actualHeight, actualWidth, mScaleType);
+
+ // Decode to the nearest power of two scaling factor.
+ decodeOptions.inJustDecodeBounds = false;
+ // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
+ // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
+ decodeOptions.inSampleSize =
+ findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
+ Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
+
+ // If necessary, scale down to the maximal acceptable size.
+ if (tempBitmap != null
+ && (tempBitmap.getWidth() > desiredWidth
+ || tempBitmap.getHeight() > desiredHeight)) {
+ bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true);
+ tempBitmap.recycle();
+ } else {
+ bitmap = tempBitmap;
+ }
+ }
+
+ if (bitmap == null) {
+ return Response.error(new ParseError(response));
+ } else {
+ return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
+ }
+ }
+
+ @Override
+ public void cancel() {
+ super.cancel();
+ synchronized (mLock) {
+ mListener = null;
+ }
+ }
+
+ @Override
+ protected void deliverResponse(Bitmap response) {
+ Response.Listener<Bitmap> listener;
+ synchronized (mLock) {
+ listener = mListener;
+ }
+ if (listener != null) {
+ listener.onResponse(response);
+ }
+ }
+
+ /**
+ * Returns the largest power-of-two divisor for use in downscaling a bitmap that will not result
+ * in the scaling past the desired dimensions.
+ *
+ * @param actualWidth Actual width of the bitmap
+ * @param actualHeight Actual height of the bitmap
+ * @param desiredWidth Desired width of the bitmap
+ * @param desiredHeight Desired height of the bitmap
+ */
+ @VisibleForTesting
+ static int findBestSampleSize(
+ int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
+ double wr = (double) actualWidth / desiredWidth;
+ double hr = (double) actualHeight / desiredHeight;
+ double ratio = Math.min(wr, hr);
+ float n = 1.0f;
+ while ((n * 2) <= ratio) {
+ n *= 2;
+ }
+
+ return (int) n;
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java b/core/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java
new file mode 100644
index 0000000..9f56746
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import androidx.annotation.Nullable;
+import com.android.volley.NetworkResponse;
+import com.android.volley.ParseError;
+import com.android.volley.Response;
+import com.android.volley.Response.ErrorListener;
+import com.android.volley.Response.Listener;
+import java.io.UnsupportedEncodingException;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+/**
+ * A request for retrieving a {@link JSONArray} response body at a given URL, allowing for an
+ * optional {@link JSONArray} to be passed in as part of the request body.
+ */
+public class JsonArrayRequest extends JsonRequest<JSONArray> {
+
+ /**
+ * Creates a new request.
+ *
+ * @param url URL to fetch the JSON from
+ * @param listener Listener to receive the JSON response
+ * @param errorListener Error listener, or null to ignore errors.
+ */
+ public JsonArrayRequest(
+ String url, Listener<JSONArray> listener, @Nullable ErrorListener errorListener) {
+ super(Method.GET, url, null, listener, errorListener);
+ }
+
+ /**
+ * Creates a new request.
+ *
+ * @param method the HTTP method to use
+ * @param url URL to fetch the JSON from
+ * @param jsonRequest A {@link JSONArray} to post with the request. Null indicates no parameters
+ * will be posted along with request.
+ * @param listener Listener to receive the JSON response
+ * @param errorListener Error listener, or null to ignore errors.
+ */
+ public JsonArrayRequest(
+ int method,
+ String url,
+ @Nullable JSONArray jsonRequest,
+ Listener<JSONArray> listener,
+ @Nullable ErrorListener errorListener) {
+ super(
+ method,
+ url,
+ jsonRequest != null ? jsonRequest.toString() : null,
+ listener,
+ errorListener);
+ }
+
+ @Override
+ protected Response<JSONArray> parseNetworkResponse(NetworkResponse response) {
+ try {
+ String jsonString =
+ new String(
+ response.data,
+ HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET));
+ return Response.success(
+ new JSONArray(jsonString), HttpHeaderParser.parseCacheHeaders(response));
+ } catch (UnsupportedEncodingException e) {
+ return Response.error(new ParseError(e));
+ } catch (JSONException je) {
+ return Response.error(new ParseError(je));
+ }
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java b/core/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java
new file mode 100644
index 0000000..eccb54b
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import androidx.annotation.Nullable;
+import com.android.volley.NetworkResponse;
+import com.android.volley.ParseError;
+import com.android.volley.Response;
+import com.android.volley.Response.ErrorListener;
+import com.android.volley.Response.Listener;
+import java.io.UnsupportedEncodingException;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * A request for retrieving a {@link JSONObject} response body at a given URL, allowing for an
+ * optional {@link JSONObject} to be passed in as part of the request body.
+ */
+public class JsonObjectRequest extends JsonRequest<JSONObject> {
+
+ /**
+ * Creates a new request.
+ *
+ * @param url URL to fetch the JSON from
+ * @param listener Listener to receive the JSON response
+ * @param errorListener Error listener, or null to ignore errors.
+ */
+ public JsonObjectRequest(
+ String url, Listener<JSONObject> listener, @Nullable ErrorListener errorListener) {
+ super(Method.GET, url, null, listener, errorListener);
+ }
+
+ /**
+ * Constructor which defaults to <code>GET</code> if <code>jsonRequest</code> is <code>null
+ * </code> , <code>POST</code> otherwise.
+ *
+ * @deprecated Use {@link #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener)}.
+ */
+ @Deprecated
+ public JsonObjectRequest(
+ String url,
+ @Nullable JSONObject jsonRequest,
+ Listener<JSONObject> listener,
+ @Nullable ErrorListener errorListener) {
+ super(
+ jsonRequest == null ? Method.GET : Method.POST,
+ url,
+ jsonRequest != null ? jsonRequest.toString() : null,
+ listener,
+ errorListener);
+ }
+
+ /**
+ * Creates a new request.
+ *
+ * @param method the HTTP method to use
+ * @param url URL to fetch the JSON from
+ * @param jsonRequest A {@link JSONObject} to post with the request. Null indicates no
+ * parameters will be posted along with request.
+ * @param listener Listener to receive the JSON response
+ * @param errorListener Error listener, or null to ignore errors.
+ */
+ public JsonObjectRequest(
+ int method,
+ String url,
+ @Nullable JSONObject jsonRequest,
+ Listener<JSONObject> listener,
+ @Nullable ErrorListener errorListener) {
+ super(
+ method,
+ url,
+ jsonRequest != null ? jsonRequest.toString() : null,
+ listener,
+ errorListener);
+ }
+
+ @Override
+ protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
+ try {
+ String jsonString =
+ new String(
+ response.data,
+ HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET));
+ return Response.success(
+ new JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response));
+ } catch (UnsupportedEncodingException e) {
+ return Response.error(new ParseError(e));
+ } catch (JSONException je) {
+ return Response.error(new ParseError(je));
+ }
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/JsonRequest.java b/core/src/main/java/com/android/volley/toolbox/JsonRequest.java
new file mode 100644
index 0000000..c2d1fad
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/JsonRequest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import com.android.volley.NetworkResponse;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.Response.ErrorListener;
+import com.android.volley.Response.Listener;
+import com.android.volley.VolleyLog;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A request for retrieving a T type response body at a given URL that also optionally sends along a
+ * JSON body in the request specified.
+ *
+ * @param <T> JSON type of response expected
+ */
+public abstract class JsonRequest<T> extends Request<T> {
+ /** Default charset for JSON request. */
+ protected static final String PROTOCOL_CHARSET = "utf-8";
+
+ /** Content type for request. */
+ private static final String PROTOCOL_CONTENT_TYPE =
+ String.format("application/json; charset=%s", PROTOCOL_CHARSET);
+
+ /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */
+ private final Object mLock = new Object();
+
+ @Nullable
+ @GuardedBy("mLock")
+ private Listener<T> mListener;
+
+ @Nullable private final String mRequestBody;
+
+ /**
+ * Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()}
+ * or {@link #getPostParams()} is overridden (which defaults to POST).
+ *
+ * @deprecated Use {@link #JsonRequest(int, String, String, Listener, ErrorListener)}.
+ */
+ @Deprecated
+ public JsonRequest(
+ String url, String requestBody, Listener<T> listener, ErrorListener errorListener) {
+ this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener);
+ }
+
+ /**
+ * Creates a new request.
+ *
+ * @param method the HTTP method to use
+ * @param url URL to fetch the JSON from
+ * @param requestBody The content to post as the body of the request. Null indicates no
+ * parameters will be posted along with request.
+ * @param listener Listener to receive the JSON response
+ * @param errorListener Error listener, or null to ignore errors.
+ */
+ public JsonRequest(
+ int method,
+ String url,
+ @Nullable String requestBody,
+ Listener<T> listener,
+ @Nullable ErrorListener errorListener) {
+ super(method, url, errorListener);
+ mListener = listener;
+ mRequestBody = requestBody;
+ }
+
+ @Override
+ public void cancel() {
+ super.cancel();
+ synchronized (mLock) {
+ mListener = null;
+ }
+ }
+
+ @Override
+ protected void deliverResponse(T response) {
+ Response.Listener<T> listener;
+ synchronized (mLock) {
+ listener = mListener;
+ }
+ if (listener != null) {
+ listener.onResponse(response);
+ }
+ }
+
+ @Override
+ protected abstract Response<T> parseNetworkResponse(NetworkResponse response);
+
+ /** @deprecated Use {@link #getBodyContentType()}. */
+ @Deprecated
+ @Override
+ public String getPostBodyContentType() {
+ return getBodyContentType();
+ }
+
+ /** @deprecated Use {@link #getBody()}. */
+ @Deprecated
+ @Override
+ public byte[] getPostBody() {
+ return getBody();
+ }
+
+ @Override
+ public String getBodyContentType() {
+ return PROTOCOL_CONTENT_TYPE;
+ }
+
+ @Override
+ public byte[] getBody() {
+ try {
+ return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET);
+ } catch (UnsupportedEncodingException uee) {
+ VolleyLog.wtf(
+ "Unsupported Encoding while trying to get the bytes of %s using %s",
+ mRequestBody, PROTOCOL_CHARSET);
+ return null;
+ }
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/NetworkImageView.java b/core/src/main/java/com/android/volley/toolbox/NetworkImageView.java
new file mode 100644
index 0000000..a24b3e2
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/NetworkImageView.java
@@ -0,0 +1,332 @@
+/**
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.ImageView;
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader.ImageContainer;
+import com.android.volley.toolbox.ImageLoader.ImageListener;
+
+/** Handles fetching an image from a URL as well as the life-cycle of the associated request. */
+public class NetworkImageView extends ImageView {
+ /** The URL of the network image to load */
+ private String mUrl;
+
+ /**
+ * Resource ID of the image to be used as a placeholder until the network image is loaded. Won't
+ * be set at the same time as mDefaultImageDrawable or mDefaultImageBitmap.
+ */
+ private int mDefaultImageId;
+
+ /**
+ * Drawable of the image to be used as a placeholder until the network image is loaded. Won't be
+ * set at the same time as mDefaultImageId or mDefaultImageBitmap.
+ */
+ @Nullable private Drawable mDefaultImageDrawable;
+
+ /**
+ * Bitmap of the image to be used as a placeholder until the network image is loaded. Won't be
+ * set at the same time as mDefaultImageId or mDefaultImageDrawable.
+ */
+ @Nullable private Bitmap mDefaultImageBitmap;
+
+ /**
+ * Resource ID of the image to be used if the network response fails. Won't be set at the same
+ * time as mErrorImageDrawable or mErrorImageBitmap.
+ */
+ private int mErrorImageId;
+
+ /**
+ * Bitmap of the image to be used if the network response fails. Won't be set at the same time
+ * as mErrorImageId or mErrorImageBitmap.
+ */
+ @Nullable private Drawable mErrorImageDrawable;
+
+ /**
+ * Bitmap of the image to be used if the network response fails. Won't be set at the same time
+ * as mErrorImageId or mErrorImageDrawable.
+ */
+ @Nullable private Bitmap mErrorImageBitmap;
+
+ /** Local copy of the ImageLoader. */
+ private ImageLoader mImageLoader;
+
+ /** Current ImageContainer. (either in-flight or finished) */
+ private ImageContainer mImageContainer;
+
+ public NetworkImageView(Context context) {
+ this(context, null);
+ }
+
+ public NetworkImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public NetworkImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ /**
+ * Sets URL of the image that should be loaded into this view. Note that calling this will
+ * immediately either set the cached image (if available) or the default image specified by
+ * {@link NetworkImageView#setDefaultImageResId(int)} on the view.
+ *
+ * <p>NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} or {@link
+ * NetworkImageView#setDefaultImageBitmap} and {@link NetworkImageView#setErrorImageResId(int)}
+ * or {@link NetworkImageView#setErrorImageBitmap(Bitmap)} should be called prior to calling
+ * this function.
+ *
+ * <p>Must be called from the main thread.
+ *
+ * @param url The URL that should be loaded into this ImageView.
+ * @param imageLoader ImageLoader that will be used to make the request.
+ */
+ @MainThread
+ public void setImageUrl(String url, ImageLoader imageLoader) {
+ Threads.throwIfNotOnMainThread();
+ mUrl = url;
+ mImageLoader = imageLoader;
+ // The URL has potentially changed. See if we need to load it.
+ loadImageIfNecessary(/* isInLayoutPass= */ false);
+ }
+
+ /**
+ * Sets the default image resource ID to be used for this view until the attempt to load it
+ * completes.
+ *
+ * <p>This will clear anything set by {@link NetworkImageView#setDefaultImageBitmap} or {@link
+ * NetworkImageView#setDefaultImageDrawable}.
+ */
+ public void setDefaultImageResId(int defaultImage) {
+ mDefaultImageBitmap = null;
+ mDefaultImageDrawable = null;
+ mDefaultImageId = defaultImage;
+ }
+
+ /**
+ * Sets the default image drawable to be used for this view until the attempt to load it
+ * completes.
+ *
+ * <p>This will clear anything set by {@link NetworkImageView#setDefaultImageResId} or {@link
+ * NetworkImageView#setDefaultImageBitmap}.
+ */
+ public void setDefaultImageDrawable(@Nullable Drawable defaultImageDrawable) {
+ mDefaultImageId = 0;
+ mDefaultImageBitmap = null;
+ mDefaultImageDrawable = defaultImageDrawable;
+ }
+
+ /**
+ * Sets the default image bitmap to be used for this view until the attempt to load it
+ * completes.
+ *
+ * <p>This will clear anything set by {@link NetworkImageView#setDefaultImageResId} or {@link
+ * NetworkImageView#setDefaultImageDrawable}.
+ */
+ public void setDefaultImageBitmap(Bitmap defaultImage) {
+ mDefaultImageId = 0;
+ mDefaultImageDrawable = null;
+ mDefaultImageBitmap = defaultImage;
+ }
+
+ /**
+ * Sets the error image resource ID to be used for this view in the event that the image
+ * requested fails to load.
+ *
+ * <p>This will clear anything set by {@link NetworkImageView#setErrorImageBitmap} or {@link
+ * NetworkImageView#setErrorImageDrawable}.
+ */
+ public void setErrorImageResId(int errorImage) {
+ mErrorImageBitmap = null;
+ mErrorImageDrawable = null;
+ mErrorImageId = errorImage;
+ }
+
+ /**
+ * Sets the error image drawable to be used for this view in the event that the image requested
+ * fails to load.
+ *
+ * <p>This will clear anything set by {@link NetworkImageView#setErrorImageResId} or {@link
+ * NetworkImageView#setDefaultImageBitmap}.
+ */
+ public void setErrorImageDrawable(@Nullable Drawable errorImageDrawable) {
+ mErrorImageId = 0;
+ mErrorImageBitmap = null;
+ mErrorImageDrawable = errorImageDrawable;
+ }
+
+ /**
+ * Sets the error image bitmap to be used for this view in the event that the image requested
+ * fails to load.
+ *
+ * <p>This will clear anything set by {@link NetworkImageView#setErrorImageResId} or {@link
+ * NetworkImageView#setDefaultImageDrawable}.
+ */
+ public void setErrorImageBitmap(Bitmap errorImage) {
+ mErrorImageId = 0;
+ mErrorImageDrawable = null;
+ mErrorImageBitmap = errorImage;
+ }
+
+ /**
+ * Loads the image for the view if it isn't already loaded.
+ *
+ * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
+ */
+ void loadImageIfNecessary(final boolean isInLayoutPass) {
+ int width = getWidth();
+ int height = getHeight();
+ ScaleType scaleType = getScaleType();
+
+ boolean wrapWidth = false, wrapHeight = false;
+ if (getLayoutParams() != null) {
+ wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
+ wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
+ }
+
+ // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
+ // view, hold off on loading the image.
+ boolean isFullyWrapContent = wrapWidth && wrapHeight;
+ if (width == 0 && height == 0 && !isFullyWrapContent) {
+ return;
+ }
+
+ // if the URL to be loaded in this view is empty, cancel any old requests and clear the
+ // currently loaded image.
+ if (TextUtils.isEmpty(mUrl)) {
+ if (mImageContainer != null) {
+ mImageContainer.cancelRequest();
+ mImageContainer = null;
+ }
+ setDefaultImageOrNull();
+ return;
+ }
+
+ // if there was an old request in this view, check if it needs to be canceled.
+ if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
+ if (mImageContainer.getRequestUrl().equals(mUrl)) {
+ // if the request is from the same URL, return.
+ return;
+ } else {
+ // if there is a pre-existing request, cancel it if it's fetching a different URL.
+ mImageContainer.cancelRequest();
+ setDefaultImageOrNull();
+ }
+ }
+
+ // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
+ int maxWidth = wrapWidth ? 0 : width;
+ int maxHeight = wrapHeight ? 0 : height;
+
+ // The pre-existing content of this view didn't match the current URL. Load the new image
+ // from the network.
+
+ // update the ImageContainer to be the new bitmap container.
+ mImageContainer =
+ mImageLoader.get(
+ mUrl,
+ new ImageListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (mErrorImageId != 0) {
+ setImageResource(mErrorImageId);
+ } else if (mErrorImageDrawable != null) {
+ setImageDrawable(mErrorImageDrawable);
+ } else if (mErrorImageBitmap != null) {
+ setImageBitmap(mErrorImageBitmap);
+ }
+ }
+
+ @Override
+ public void onResponse(
+ final ImageContainer response, boolean isImmediate) {
+ // If this was an immediate response that was delivered inside of a
+ // layout
+ // pass do not set the image immediately as it will trigger a
+ // requestLayout
+ // inside of a layout. Instead, defer setting the image by posting
+ // back to
+ // the main thread.
+ if (isImmediate && isInLayoutPass) {
+ post(
+ new Runnable() {
+ @Override
+ public void run() {
+ onResponse(response, /* isImmediate= */ false);
+ }
+ });
+ return;
+ }
+
+ if (response.getBitmap() != null) {
+ setImageBitmap(response.getBitmap());
+ } else if (mDefaultImageId != 0) {
+ setImageResource(mDefaultImageId);
+ } else if (mDefaultImageDrawable != null) {
+ setImageDrawable(mDefaultImageDrawable);
+ } else if (mDefaultImageBitmap != null) {
+ setImageBitmap(mDefaultImageBitmap);
+ }
+ }
+ },
+ maxWidth,
+ maxHeight,
+ scaleType);
+ }
+
+ private void setDefaultImageOrNull() {
+ if (mDefaultImageId != 0) {
+ setImageResource(mDefaultImageId);
+ } else if (mDefaultImageDrawable != null) {
+ setImageDrawable(mDefaultImageDrawable);
+ } else if (mDefaultImageBitmap != null) {
+ setImageBitmap(mDefaultImageBitmap);
+ } else {
+ setImageBitmap(null);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ loadImageIfNecessary(/* isInLayoutPass= */ true);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (mImageContainer != null) {
+ // If the view was bound to an image request, cancel it and clear
+ // out the image from the view.
+ mImageContainer.cancelRequest();
+ setImageBitmap(null);
+ // also clear out the container so we can reload the image if necessary.
+ mImageContainer = null;
+ }
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ invalidate();
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/NetworkUtility.java b/core/src/main/java/com/android/volley/toolbox/NetworkUtility.java
new file mode 100644
index 0000000..58a3bb3
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/NetworkUtility.java
@@ -0,0 +1,206 @@
+/*
+ * 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}
+ */
+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, the provided exception is thrown.
+ *
+ * <p>Must be invoked from a background thread, as client implementations of RetryPolicy#retry
+ * may make blocking calls.
+ *
+ * @param request The request to use.
+ */
+ static void attemptRetryOnException(final Request<?> request, final RetryInfo retryInfo)
+ throws VolleyError {
+ final RetryPolicy retryPolicy = request.getRetryPolicy();
+ final int oldTimeout = request.getTimeoutMs();
+ try {
+ retryPolicy.retry(retryInfo.errorToRetry);
+ } catch (VolleyError e) {
+ request.addMarker(
+ String.format(
+ "%s-timeout-giveup [timeout=%s]", retryInfo.logPrefix, oldTimeout));
+ throw e;
+ }
+ request.addMarker(String.format("%s-retry [timeout=%s]", retryInfo.logPrefix, oldTimeout));
+ }
+
+ static class RetryInfo {
+ private final String logPrefix;
+ private final VolleyError errorToRetry;
+
+ private RetryInfo(String logPrefix, VolleyError errorToRetry) {
+ this.logPrefix = logPrefix;
+ this.errorToRetry = errorToRetry;
+ }
+ }
+
+ /**
+ * Based on the exception thrown, decides whether to attempt to retry, or to throw the error.
+ *
+ * <p>If this method returns without throwing, {@link #attemptRetryOnException} should be called
+ * with the provided {@link RetryInfo} to consult the client's retry policy.
+ */
+ static RetryInfo shouldRetryException(
+ Request<?> request,
+ IOException exception,
+ long requestStartMs,
+ @Nullable HttpResponse httpResponse,
+ @Nullable byte[] responseContents)
+ throws VolleyError {
+ if (exception instanceof SocketTimeoutException) {
+ return new RetryInfo("socket", 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()) {
+ return new RetryInfo("connection", new NoConnectionError());
+ }
+ 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) {
+ return new RetryInfo("auth", new AuthFailureError(networkResponse));
+ }
+ if (statusCode >= 400 && statusCode <= 499) {
+ // Don't retry other client errors.
+ throw new ClientError(networkResponse);
+ }
+ if (statusCode >= 500 && statusCode <= 599) {
+ if (request.shouldRetryServerErrors()) {
+ return new RetryInfo("server", new ServerError(networkResponse));
+ }
+ }
+ // Server error and client has opted out of retries, or 3xx. No reason to retry.
+ throw new ServerError(networkResponse);
+ }
+ return new RetryInfo("network", new NetworkError());
+ }
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/NoAsyncCache.java b/core/src/main/java/com/android/volley/toolbox/NoAsyncCache.java
new file mode 100644
index 0000000..1fda58f
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/NoAsyncCache.java
@@ -0,0 +1,42 @@
+package com.android.volley.toolbox;
+
+import com.android.volley.AsyncCache;
+import com.android.volley.Cache;
+
+/**
+ * An AsyncCache that doesn't cache anything.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
+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/core/src/main/java/com/android/volley/toolbox/NoCache.java b/core/src/main/java/com/android/volley/toolbox/NoCache.java
new file mode 100644
index 0000000..51f9945
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/NoCache.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import com.android.volley.Cache;
+
+/** A cache that doesn't. */
+public class NoCache implements Cache {
+ @Override
+ public void clear() {}
+
+ @Override
+ public Entry get(String key) {
+ return null;
+ }
+
+ @Override
+ public void put(String key, Entry entry) {}
+
+ @Override
+ public void invalidate(String key, boolean fullExpire) {}
+
+ @Override
+ public void remove(String key) {}
+
+ @Override
+ public void initialize() {}
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java b/core/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java
new file mode 100644
index 0000000..bdcc45e
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * A variation of {@link java.io.ByteArrayOutputStream} that uses a pool of byte[] buffers instead
+ * of always allocating them fresh, saving on heap churn.
+ */
+public class PoolingByteArrayOutputStream extends ByteArrayOutputStream {
+ /**
+ * If the {@link #PoolingByteArrayOutputStream(ByteArrayPool)} constructor is called, this is
+ * the default size to which the underlying byte array is initialized.
+ */
+ private static final int DEFAULT_SIZE = 256;
+
+ private final ByteArrayPool mPool;
+
+ /**
+ * Constructs a new PoolingByteArrayOutputStream with a default size. If more bytes are written
+ * to this instance, the underlying byte array will expand.
+ */
+ public PoolingByteArrayOutputStream(ByteArrayPool pool) {
+ this(pool, DEFAULT_SIZE);
+ }
+
+ /**
+ * Constructs a new {@code ByteArrayOutputStream} with a default size of {@code size} bytes. If
+ * more than {@code size} bytes are written to this instance, the underlying byte array will
+ * expand.
+ *
+ * @param size initial size for the underlying byte array. The value will be pinned to a default
+ * minimum size.
+ */
+ public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) {
+ mPool = pool;
+ buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE));
+ }
+
+ @Override
+ public void close() throws IOException {
+ mPool.returnBuf(buf);
+ buf = null;
+ super.close();
+ }
+
+ @Override
+ public void finalize() {
+ mPool.returnBuf(buf);
+ }
+
+ /** Ensures there is enough space in the buffer for the given number of additional bytes. */
+ @SuppressWarnings("UnsafeFinalization")
+ private void expand(int i) {
+ /* Can the buffer handle @i more bytes, if not expand it */
+ if (count + i <= buf.length) {
+ return;
+ }
+ byte[] newbuf = mPool.getBuf((count + i) * 2);
+ System.arraycopy(buf, 0, newbuf, 0, count);
+ mPool.returnBuf(buf);
+ buf = newbuf;
+ }
+
+ @Override
+ public synchronized void write(byte[] buffer, int offset, int len) {
+ expand(len);
+ super.write(buffer, offset, len);
+ }
+
+ @Override
+ public synchronized void write(int oneByte) {
+ expand(1);
+ super.write(oneByte);
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/RequestFuture.java b/core/src/main/java/com/android/volley/toolbox/RequestFuture.java
new file mode 100644
index 0000000..f9cbce2
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/RequestFuture.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import android.os.SystemClock;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A Future that represents a Volley request.
+ *
+ * <p>Used by providing as your response and error listeners. For example:
+ *
+ * <pre>
+ * RequestFuture&lt;JSONObject&gt; future = RequestFuture.newFuture();
+ * MyRequest request = new MyRequest(URL, future, future);
+ *
+ * // If you want to be able to cancel the request:
+ * future.setRequest(requestQueue.add(request));
+ *
+ * // Otherwise:
+ * requestQueue.add(request);
+ *
+ * try {
+ * JSONObject response = future.get();
+ * // do something with response
+ * } catch (InterruptedException e) {
+ * // handle the error
+ * } catch (ExecutionException e) {
+ * // handle the error
+ * }
+ * </pre>
+ *
+ * @param <T> The type of parsed response this future expects.
+ */
+public class RequestFuture<T> implements Future<T>, Response.Listener<T>, Response.ErrorListener {
+ private Request<?> mRequest;
+ private boolean mResultReceived = false;
+ private T mResult;
+ private VolleyError mException;
+
+ public static <E> RequestFuture<E> newFuture() {
+ return new RequestFuture<>();
+ }
+
+ private RequestFuture() {}
+
+ public void setRequest(Request<?> request) {
+ mRequest = request;
+ }
+
+ @Override
+ public synchronized boolean cancel(boolean mayInterruptIfRunning) {
+ if (mRequest == null) {
+ return false;
+ }
+
+ if (!isDone()) {
+ mRequest.cancel();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ try {
+ return doGet(/* timeoutMs= */ null);
+ } catch (TimeoutException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return doGet(TimeUnit.MILLISECONDS.convert(timeout, unit));
+ }
+
+ private synchronized T doGet(Long timeoutMs)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ if (mException != null) {
+ throw new ExecutionException(mException);
+ }
+
+ if (mResultReceived) {
+ return mResult;
+ }
+
+ if (timeoutMs == null) {
+ while (!isDone()) {
+ wait(0);
+ }
+ } else if (timeoutMs > 0) {
+ long nowMs = SystemClock.uptimeMillis();
+ long deadlineMs = nowMs + timeoutMs;
+ while (!isDone() && nowMs < deadlineMs) {
+ wait(deadlineMs - nowMs);
+ nowMs = SystemClock.uptimeMillis();
+ }
+ }
+
+ if (mException != null) {
+ throw new ExecutionException(mException);
+ }
+
+ if (!mResultReceived) {
+ throw new TimeoutException();
+ }
+
+ return mResult;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ if (mRequest == null) {
+ return false;
+ }
+ return mRequest.isCanceled();
+ }
+
+ @Override
+ public synchronized boolean isDone() {
+ return mResultReceived || mException != null || isCancelled();
+ }
+
+ @Override
+ public synchronized void onResponse(T response) {
+ mResultReceived = true;
+ mResult = response;
+ notifyAll();
+ }
+
+ @Override
+ public synchronized void onErrorResponse(VolleyError error) {
+ mException = error;
+ notifyAll();
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/StringRequest.java b/core/src/main/java/com/android/volley/toolbox/StringRequest.java
new file mode 100644
index 0000000..df7b386
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/StringRequest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import com.android.volley.NetworkResponse;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.Response.ErrorListener;
+import com.android.volley.Response.Listener;
+import java.io.UnsupportedEncodingException;
+
+/** A canned request for retrieving the response body at a given URL as a String. */
+public class StringRequest extends Request<String> {
+
+ /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */
+ private final Object mLock = new Object();
+
+ @Nullable
+ @GuardedBy("mLock")
+ private Listener<String> mListener;
+
+ /**
+ * Creates a new request with the given method.
+ *
+ * @param method the request {@link Method} to use
+ * @param url URL to fetch the string at
+ * @param listener Listener to receive the String response
+ * @param errorListener Error listener, or null to ignore errors
+ */
+ public StringRequest(
+ int method,
+ String url,
+ Listener<String> listener,
+ @Nullable ErrorListener errorListener) {
+ super(method, url, errorListener);
+ mListener = listener;
+ }
+
+ /**
+ * Creates a new GET request.
+ *
+ * @param url URL to fetch the string at
+ * @param listener Listener to receive the String response
+ * @param errorListener Error listener, or null to ignore errors
+ */
+ public StringRequest(
+ String url, Listener<String> listener, @Nullable ErrorListener errorListener) {
+ this(Method.GET, url, listener, errorListener);
+ }
+
+ @Override
+ public void cancel() {
+ super.cancel();
+ synchronized (mLock) {
+ mListener = null;
+ }
+ }
+
+ @Override
+ protected void deliverResponse(String response) {
+ Response.Listener<String> listener;
+ synchronized (mLock) {
+ listener = mListener;
+ }
+ if (listener != null) {
+ listener.onResponse(response);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("DefaultCharset")
+ protected Response<String> parseNetworkResponse(NetworkResponse response) {
+ String parsed;
+ try {
+ parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
+ } catch (UnsupportedEncodingException e) {
+ // Since minSdkVersion = 8, we can't call
+ // new String(response.data, Charset.defaultCharset())
+ // So suppress the warning instead.
+ parsed = new String(response.data);
+ }
+ return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/Threads.java b/core/src/main/java/com/android/volley/toolbox/Threads.java
new file mode 100644
index 0000000..66c3e41
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/Threads.java
@@ -0,0 +1,13 @@
+package com.android.volley.toolbox;
+
+import android.os.Looper;
+
+final class Threads {
+ private Threads() {}
+
+ static void throwIfNotOnMainThread() {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ throw new IllegalStateException("Must be invoked from the main thread.");
+ }
+ }
+}
diff --git a/core/src/main/java/com/android/volley/toolbox/UrlRewriter.java b/core/src/main/java/com/android/volley/toolbox/UrlRewriter.java
new file mode 100644
index 0000000..8bbb770
--- /dev/null
+++ b/core/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/core/src/main/java/com/android/volley/toolbox/Volley.java b/core/src/main/java/com/android/volley/toolbox/Volley.java
new file mode 100644
index 0000000..6ab34bb
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/Volley.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.http.AndroidHttpClient;
+import android.os.Build;
+import androidx.annotation.NonNull;
+import com.android.volley.Network;
+import com.android.volley.RequestQueue;
+import java.io.File;
+
+public class Volley {
+
+ /** Default on-disk cache directory. */
+ private static final String DEFAULT_CACHE_DIR = "volley";
+
+ /**
+ * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
+ *
+ * @param context A {@link Context} to use for creating the cache dir.
+ * @param stack A {@link BaseHttpStack} to use for the network, or null for default.
+ * @return A started {@link RequestQueue} instance.
+ */
+ @NonNull
+ public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) {
+ BasicNetwork network;
+ if (stack == null) {
+ if (Build.VERSION.SDK_INT >= 9) {
+ network = new BasicNetwork(new HurlStack());
+ } else {
+ // Prior to Gingerbread, HttpUrlConnection was unreliable.
+ // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
+ // At some point in the future we'll move our minSdkVersion past Froyo and can
+ // delete this fallback (along with all Apache HTTP code).
+ String userAgent = "volley/0";
+ try {
+ String packageName = context.getPackageName();
+ PackageInfo info =
+ context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0);
+ userAgent = packageName + "/" + info.versionCode;
+ } catch (NameNotFoundException e) {
+ }
+
+ network =
+ new BasicNetwork(
+ new HttpClientStack(AndroidHttpClient.newInstance(userAgent)));
+ }
+ } else {
+ network = new BasicNetwork(stack);
+ }
+
+ return newRequestQueue(context, network);
+ }
+
+ /**
+ * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
+ *
+ * @param context A {@link Context} to use for creating the cache dir.
+ * @param stack An {@link HttpStack} to use for the network, or null for default.
+ * @return A started {@link RequestQueue} instance.
+ * @deprecated Use {@link #newRequestQueue(Context, BaseHttpStack)} instead to avoid depending
+ * on Apache HTTP. This method may be removed in a future release of Volley.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ @NonNull
+ public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
+ if (stack == null) {
+ return newRequestQueue(context, (BaseHttpStack) null);
+ }
+ return newRequestQueue(context, new BasicNetwork(stack));
+ }
+
+ @NonNull
+ private static RequestQueue newRequestQueue(Context context, Network network) {
+ final Context appContext = context.getApplicationContext();
+ // Use a lazy supplier for the cache directory so that newRequestQueue() can be called on
+ // main thread without causing strict mode violation.
+ DiskBasedCache.FileSupplier cacheSupplier =
+ new DiskBasedCache.FileSupplier() {
+ private File cacheDir = null;
+
+ @Override
+ public File get() {
+ if (cacheDir == null) {
+ cacheDir = new File(appContext.getCacheDir(), DEFAULT_CACHE_DIR);
+ }
+ return cacheDir;
+ }
+ };
+ RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheSupplier), network);
+ queue.start();
+ return queue;
+ }
+
+ /**
+ * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
+ *
+ * @param context A {@link Context} to use for creating the cache dir.
+ * @return A started {@link RequestQueue} instance.
+ */
+ @NonNull
+ public static RequestQueue newRequestQueue(Context context) {
+ return newRequestQueue(context, (BaseHttpStack) null);
+ }
+}
diff --git a/core/src/test/java/com/android/volley/AsyncRequestQueueTest.java b/core/src/test/java/com/android/volley/AsyncRequestQueueTest.java
new file mode 100644
index 0000000..aef4f01
--- /dev/null
+++ b/core/src/test/java/com/android/volley/AsyncRequestQueueTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+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.AsyncCache.OnGetCompleteCallback;
+import com.android.volley.AsyncCache.OnWriteCompleteCallback;
+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.ArgumentCaptor;
+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 final ResponseDelivery mDelivery = new ImmediateResponseDelivery();
+ private AsyncRequestQueue queue;
+
+ @Before
+ public void setUp() throws Exception {
+ initMocks(this);
+ queue = createRequestQueue(new NoAsyncCache());
+ }
+
+ @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();
+ }
+
+ @Test
+ public void requestsQueuedBeforeCacheInitialization_asyncCache() {
+ // Create a new queue with a mock cache in order to verify the initialization.
+ AsyncCache mockAsyncCache = mock(AsyncCache.class);
+ AsyncRequestQueue queue = createRequestQueue(mockAsyncCache);
+ queue.start();
+
+ ArgumentCaptor<OnWriteCompleteCallback> callbackCaptor =
+ ArgumentCaptor.forClass(OnWriteCompleteCallback.class);
+ verify(mockAsyncCache).initialize(callbackCaptor.capture());
+
+ StringRequest req = mock(StringRequest.class);
+ req.setShouldCache(true);
+ when(req.getCacheKey()).thenReturn("cache-key");
+ queue.add(req);
+
+ // Cache should not be read before initialization completes.
+ verify(mockAsyncCache, never()).get(anyString(), any(OnGetCompleteCallback.class));
+
+ callbackCaptor.getValue().onWriteComplete();
+
+ // Once the write completes, the request should be kicked off (in the form of a cache
+ // lookup).
+ verify(mockAsyncCache).get(eq("cache-key"), any(OnGetCompleteCallback.class));
+
+ queue.stop();
+ }
+
+ private AsyncRequestQueue createRequestQueue(AsyncCache asyncCache) {
+ return new AsyncRequestQueue.Builder(mMockNetwork)
+ .setResponseDelivery(mDelivery)
+ .setAsyncCache(asyncCache)
+ .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();
+ }
+}
diff --git a/core/src/test/java/com/android/volley/CacheDispatcherTest.java b/core/src/test/java/com/android/volley/CacheDispatcherTest.java
new file mode 100644
index 0000000..aef6785
--- /dev/null
+++ b/core/src/test/java/com/android/volley/CacheDispatcherTest.java
@@ -0,0 +1,276 @@
+/*
+ * 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.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.android.volley.toolbox.StringRequest;
+import com.android.volley.utils.CacheTestUtils;
+import java.util.concurrent.BlockingQueue;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+@SuppressWarnings("rawtypes")
+public class CacheDispatcherTest {
+ private CacheDispatcher mDispatcher;
+ private @Mock BlockingQueue<Request<?>> mCacheQueue;
+ private @Mock BlockingQueue<Request<?>> mNetworkQueue;
+ private @Mock Cache mCache;
+ private @Mock ResponseDelivery mDelivery;
+ private @Mock Network mNetwork;
+ private StringRequest mRequest;
+
+ @Before
+ public void setUp() throws Exception {
+ initMocks(this);
+
+ mRequest = new StringRequest(Request.Method.GET, "http://foo", null, null);
+ mDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
+ }
+
+ private static class WaitForever implements Answer {
+ @Override
+ public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
+ Thread.sleep(Long.MAX_VALUE);
+ return null;
+ }
+ }
+
+ @Test
+ public void runStopsOnQuit() throws Exception {
+ when(mCacheQueue.take()).then(new WaitForever());
+ mDispatcher.start();
+ mDispatcher.quit();
+ mDispatcher.join(1000);
+ }
+
+ private static void verifyNoResponse(ResponseDelivery delivery) {
+ verify(delivery, never()).postResponse(any(Request.class), any(Response.class));
+ verify(delivery, never())
+ .postResponse(any(Request.class), any(Response.class), any(Runnable.class));
+ verify(delivery, never()).postError(any(Request.class), any(VolleyError.class));
+ }
+
+ // A cancelled request should not be processed at all.
+ @Test
+ public void cancelledRequest() throws Exception {
+ mRequest.cancel();
+ mDispatcher.processRequest(mRequest);
+ verify(mCache, never()).get(anyString());
+ verifyNoResponse(mDelivery);
+ }
+
+ // A cache miss does not post a response and puts the request on the network queue.
+ @Test
+ public void cacheMiss() throws Exception {
+ mDispatcher.processRequest(mRequest);
+ verifyNoResponse(mDelivery);
+ verify(mNetworkQueue).put(mRequest);
+ assertNull(mRequest.getCacheEntry());
+ }
+
+ // A non-expired cache hit posts a response and does not queue to the network.
+ @Test
+ public void nonExpiredCacheHit() throws Exception {
+ Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false);
+ when(mCache.get(anyString())).thenReturn(entry);
+ mDispatcher.processRequest(mRequest);
+ verify(mDelivery).postResponse(any(Request.class), any(Response.class));
+ verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class));
+ }
+
+ // A soft-expired cache hit posts a response and queues to the network.
+ @Test
+ public void softExpiredCacheHit() throws Exception {
+ Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
+ when(mCache.get(anyString())).thenReturn(entry);
+ mDispatcher.processRequest(mRequest);
+
+ // Soft expiration needs to use the deferred Runnable variant of postResponse,
+ // so make sure it gets to run.
+ ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class);
+ verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture());
+ runnable.getValue().run();
+ // This way we can verify the behavior of the Runnable as well.
+ verify(mNetworkQueue).put(mRequest);
+ assertSame(entry, mRequest.getCacheEntry());
+
+ verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class));
+ }
+
+ // An expired cache hit does not post a response and queues to the network.
+ @Test
+ public void expiredCacheHit() throws Exception {
+ Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, true, true);
+ when(mCache.get(anyString())).thenReturn(entry);
+ mDispatcher.processRequest(mRequest);
+ verifyNoResponse(mDelivery);
+ verify(mNetworkQueue).put(mRequest);
+ assertSame(entry, mRequest.getCacheEntry());
+ }
+
+ // An fresh cache hit with parse error, does not post a response and queues to the network.
+ @Test
+ public void freshCacheHit_parseError() throws Exception {
+ Request request = mock(Request.class);
+ when(request.parseNetworkResponse(any(NetworkResponse.class)))
+ .thenReturn(Response.error(new ParseError()));
+ when(request.getCacheKey()).thenReturn("cache/key");
+ Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false);
+ when(mCache.get(anyString())).thenReturn(entry);
+
+ mDispatcher.processRequest(request);
+
+ verifyNoResponse(mDelivery);
+ verify(mNetworkQueue).put(request);
+ assertNull(request.getCacheEntry());
+ verify(mCache).invalidate("cache/key", true);
+ verify(request).addMarker("cache-parsing-failed");
+ }
+
+ @Test
+ public void duplicateCacheMiss() throws Exception {
+ StringRequest secondRequest =
+ new StringRequest(Request.Method.GET, "http://foo", null, null);
+ mRequest.setSequence(1);
+ secondRequest.setSequence(2);
+ mDispatcher.processRequest(mRequest);
+ mDispatcher.processRequest(secondRequest);
+ verify(mNetworkQueue).put(mRequest);
+ verifyNoResponse(mDelivery);
+ }
+
+ @Test
+ public void tripleCacheMiss_networkErrorOnFirst() throws Exception {
+ StringRequest secondRequest =
+ new StringRequest(Request.Method.GET, "http://foo", null, null);
+ StringRequest thirdRequest =
+ new StringRequest(Request.Method.GET, "http://foo", null, null);
+ mRequest.setSequence(1);
+ secondRequest.setSequence(2);
+ thirdRequest.setSequence(3);
+ mDispatcher.processRequest(mRequest);
+ mDispatcher.processRequest(secondRequest);
+ mDispatcher.processRequest(thirdRequest);
+
+ verify(mNetworkQueue).put(mRequest);
+ verifyNoResponse(mDelivery);
+
+ ((Request<?>) mRequest).notifyListenerResponseNotUsable();
+ // Second request should now be in network queue.
+ verify(mNetworkQueue).put(secondRequest);
+ // Another unusable response, third request should now be added.
+ ((Request<?>) secondRequest).notifyListenerResponseNotUsable();
+ verify(mNetworkQueue).put(thirdRequest);
+ }
+
+ @Test
+ public void duplicateSoftExpiredCacheHit_failedRequest() throws Exception {
+ Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
+ when(mCache.get(anyString())).thenReturn(entry);
+
+ StringRequest secondRequest =
+ new StringRequest(Request.Method.GET, "http://foo", null, null);
+ mRequest.setSequence(1);
+ secondRequest.setSequence(2);
+
+ mDispatcher.processRequest(mRequest);
+ mDispatcher.processRequest(secondRequest);
+
+ // Soft expiration needs to use the deferred Runnable variant of postResponse,
+ // so make sure it gets to run.
+ ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class);
+ verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture());
+ runnable.getValue().run();
+ // This way we can verify the behavior of the Runnable as well.
+
+ verify(mNetworkQueue).put(mRequest);
+ verify(mDelivery)
+ .postResponse(any(Request.class), any(Response.class), any(Runnable.class));
+
+ ((Request<?>) mRequest).notifyListenerResponseNotUsable();
+ // Second request should now be in network queue.
+ verify(mNetworkQueue).put(secondRequest);
+ }
+
+ @Test
+ public void duplicateSoftExpiredCacheHit_successfulRequest() throws Exception {
+ Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
+ when(mCache.get(anyString())).thenReturn(entry);
+
+ StringRequest secondRequest =
+ new StringRequest(Request.Method.GET, "http://foo", null, null);
+ mRequest.setSequence(1);
+ secondRequest.setSequence(2);
+
+ mDispatcher.processRequest(mRequest);
+ mDispatcher.processRequest(secondRequest);
+
+ // Soft expiration needs to use the deferred Runnable variant of postResponse,
+ // so make sure it gets to run.
+ ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class);
+ verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture());
+ runnable.getValue().run();
+ // This way we can verify the behavior of the Runnable as well.
+
+ verify(mNetworkQueue).put(mRequest);
+ verify(mDelivery)
+ .postResponse(any(Request.class), any(Response.class), any(Runnable.class));
+
+ ((Request<?>) mRequest).notifyListenerResponseReceived(Response.success(null, entry));
+ // Second request should have delivered response.
+ verify(mNetworkQueue, never()).put(secondRequest);
+ verify(mDelivery)
+ .postResponse(any(Request.class), any(Response.class), any(Runnable.class));
+ }
+
+ @Test
+ public void processRequestNotifiesListener() throws Exception {
+ RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
+ RequestQueue queue = new RequestQueue(mCache, mNetwork, 0, mDelivery);
+ queue.addRequestEventListener(listener);
+ mRequest.setRequestQueue(queue);
+
+ Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false);
+ when(mCache.get(anyString())).thenReturn(entry);
+ mDispatcher.processRequest(mRequest);
+
+ InOrder inOrder = inOrder(listener);
+ inOrder.verify(listener)
+ .onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED);
+ inOrder.verify(listener)
+ .onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED);
+ inOrder.verifyNoMoreInteractions();
+ }
+}
diff --git a/core/src/test/java/com/android/volley/NetworkDispatcherTest.java b/core/src/test/java/com/android/volley/NetworkDispatcherTest.java
new file mode 100644
index 0000000..74dfe8a
--- /dev/null
+++ b/core/src/test/java/com/android/volley/NetworkDispatcherTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.android.volley.toolbox.NoCache;
+import com.android.volley.toolbox.StringRequest;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.BlockingQueue;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class NetworkDispatcherTest {
+ private NetworkDispatcher mDispatcher;
+ private @Mock ResponseDelivery mDelivery;
+ private @Mock BlockingQueue<Request<?>> mNetworkQueue;
+ private @Mock Network mNetwork;
+ private @Mock Cache mCache;
+ private StringRequest mRequest;
+
+ private static final byte[] CANNED_DATA =
+ "Ceci n'est pas une vraie reponse".getBytes(StandardCharsets.UTF_8);
+
+ @Before
+ public void setUp() throws Exception {
+ initMocks(this);
+ mRequest = new StringRequest(Request.Method.GET, "http://foo", null, null);
+ mDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);
+ }
+
+ @Test
+ public void successPostsResponse() throws Exception {
+ when(mNetwork.performRequest(any(Request.class)))
+ .thenReturn(new NetworkResponse(CANNED_DATA));
+ mDispatcher.processRequest(mRequest);
+
+ ArgumentCaptor<Response> response = ArgumentCaptor.forClass(Response.class);
+ verify(mDelivery).postResponse(any(Request.class), response.capture());
+ assertTrue(response.getValue().isSuccess());
+ assertEquals(response.getValue().result, new String(CANNED_DATA, StandardCharsets.UTF_8));
+
+ verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class));
+ }
+
+ @Test
+ public void successNotifiesListener() throws Exception {
+ RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
+ RequestQueue queue = new RequestQueue(new NoCache(), mNetwork, 0, mDelivery);
+ queue.addRequestEventListener(listener);
+ mRequest.setRequestQueue(queue);
+
+ when(mNetwork.performRequest(any(Request.class)))
+ .thenReturn(new NetworkResponse(CANNED_DATA));
+
+ mDispatcher.processRequest(mRequest);
+
+ InOrder inOrder = inOrder(listener);
+ inOrder.verify(listener)
+ .onRequestEvent(
+ mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED);
+ inOrder.verify(listener)
+ .onRequestEvent(
+ mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void exceptionPostsError() throws Exception {
+ when(mNetwork.performRequest(any(Request.class))).thenThrow(new ServerError());
+ mDispatcher.processRequest(mRequest);
+
+ verify(mDelivery).postError(any(Request.class), any(VolleyError.class));
+ verify(mDelivery, never()).postResponse(any(Request.class), any(Response.class));
+ }
+
+ @Test
+ public void exceptionNotifiesListener() throws Exception {
+ RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
+ RequestQueue queue = new RequestQueue(new NoCache(), mNetwork, 0, mDelivery);
+ queue.addRequestEventListener(listener);
+ mRequest.setRequestQueue(queue);
+
+ when(mNetwork.performRequest(any(Request.class))).thenThrow(new ServerError());
+
+ mDispatcher.processRequest(mRequest);
+
+ InOrder inOrder = inOrder(listener);
+ inOrder.verify(listener)
+ .onRequestEvent(
+ mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED);
+ inOrder.verify(listener)
+ .onRequestEvent(
+ mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void shouldCacheFalse() throws Exception {
+ mRequest.setShouldCache(false);
+ mDispatcher.processRequest(mRequest);
+ verify(mCache, never()).put(anyString(), any(Cache.Entry.class));
+ }
+
+ @Test
+ public void shouldCacheTrue() throws Exception {
+ when(mNetwork.performRequest(any(Request.class)))
+ .thenReturn(new NetworkResponse(CANNED_DATA));
+ mRequest.setShouldCache(true);
+ mDispatcher.processRequest(mRequest);
+ ArgumentCaptor<Cache.Entry> entry = ArgumentCaptor.forClass(Cache.Entry.class);
+ verify(mCache).put(eq(mRequest.getCacheKey()), entry.capture());
+ assertTrue(Arrays.equals(entry.getValue().data, CANNED_DATA));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/NetworkResponseTest.java b/core/src/test/java/com/android/volley/NetworkResponseTest.java
new file mode 100644
index 0000000..70210da
--- /dev/null
+++ b/core/src/test/java/com/android/volley/NetworkResponseTest.java
@@ -0,0 +1,61 @@
+package com.android.volley;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class NetworkResponseTest {
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void mapToList() {
+ Map<String, String> headers = new HashMap<>();
+ headers.put("key1", "value1");
+ headers.put("key2", "value2");
+
+ NetworkResponse resp = new NetworkResponse(200, null, headers, false);
+
+ List<Header> expectedHeaders = new ArrayList<>();
+ expectedHeaders.add(new Header("key1", "value1"));
+ expectedHeaders.add(new Header("key2", "value2"));
+
+ assertThat(expectedHeaders, containsInAnyOrder(resp.allHeaders.toArray(new Header[0])));
+ }
+
+ @Test
+ public void listToMap() {
+ List<Header> headers = new ArrayList<>();
+ headers.add(new Header("key1", "value1"));
+ // Later values should be preferred.
+ headers.add(new Header("key2", "ignoredvalue"));
+ headers.add(new Header("key2", "value2"));
+
+ NetworkResponse resp = new NetworkResponse(200, null, false, 0L, headers);
+
+ Map<String, String> expectedHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ expectedHeaders.put("key1", "value1");
+ expectedHeaders.put("key2", "value2");
+
+ assertEquals(expectedHeaders, resp.headers);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void nullValuesDontCrash() {
+ new NetworkResponse(null);
+ new NetworkResponse(null, null);
+ new NetworkResponse(200, null, null, false);
+ new NetworkResponse(200, null, null, false, 0L);
+ new NetworkResponse(200, null, false, 0L, null);
+ }
+}
diff --git a/core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java b/core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java
new file mode 100644
index 0000000..a2bfbc6
--- /dev/null
+++ b/core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2015 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.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.android.volley.Request.Priority;
+import com.android.volley.RequestQueue.RequestFinishedListener;
+import com.android.volley.mock.MockRequest;
+import com.android.volley.mock.ShadowSystemClock;
+import com.android.volley.toolbox.NoCache;
+import com.android.volley.utils.ImmediateResponseDelivery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/**
+ * Integration tests for {@link RequestQueue} that verify its behavior in conjunction with real
+ * dispatcher, queues and Requests.
+ *
+ * <p>The Network is mocked out.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowSystemClock.class})
+public class RequestQueueIntegrationTest {
+
+ private ResponseDelivery mDelivery;
+ @Mock private Network mMockNetwork;
+ @Mock private RequestFinishedListener<byte[]> mMockListener;
+ @Mock private RequestFinishedListener<byte[]> mMockListener2;
+
+ @Before
+ public void setUp() throws Exception {
+ mDelivery = new ImmediateResponseDelivery();
+ initMocks(this);
+ }
+
+ @Test
+ public void add_requestProcessedInCorrectOrder() throws Exception {
+ // Enqueue 2 requests with different cache keys, and different priorities. The second,
+ // higher priority request takes 20ms.
+ // Assert that the first request is only handled after the first one has been parsed and
+ // delivered.
+ MockRequest lowerPriorityReq = new MockRequest();
+ MockRequest higherPriorityReq = new MockRequest();
+ lowerPriorityReq.setCacheKey("1");
+ higherPriorityReq.setCacheKey("2");
+ lowerPriorityReq.setPriority(Priority.LOW);
+ higherPriorityReq.setPriority(Priority.HIGH);
+
+ Answer<NetworkResponse> delayAnswer =
+ new Answer<NetworkResponse>() {
+ @Override
+ public NetworkResponse answer(InvocationOnMock invocationOnMock)
+ throws Throwable {
+ Thread.sleep(20);
+ return mock(NetworkResponse.class);
+ }
+ };
+ // delay only for higher request
+ when(mMockNetwork.performRequest(higherPriorityReq)).thenAnswer(delayAnswer);
+ when(mMockNetwork.performRequest(lowerPriorityReq)).thenReturn(mock(NetworkResponse.class));
+
+ RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);
+ queue.addRequestFinishedListener(mMockListener);
+ queue.add(lowerPriorityReq);
+ queue.add(higherPriorityReq);
+ queue.start();
+
+ InOrder inOrder = inOrder(mMockListener);
+ // verify higherPriorityReq goes through first
+ inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(higherPriorityReq);
+ // verify lowerPriorityReq goes last
+ inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(lowerPriorityReq);
+
+ queue.stop();
+ }
+
+ /** Asserts that requests with same cache key are processed in order. */
+ @Test
+ public void add_dedupeByCacheKey() throws Exception {
+ // Enqueue 2 requests with the same cache key. The first request takes 20ms. Assert that the
+ // second request is only handled after the first one has been parsed and delivered.
+ MockRequest req1 = new MockRequest();
+ MockRequest req2 = new MockRequest();
+ Answer<NetworkResponse> delayAnswer =
+ new Answer<NetworkResponse>() {
+ @Override
+ public NetworkResponse answer(InvocationOnMock invocationOnMock)
+ throws Throwable {
+ Thread.sleep(20);
+ return mock(NetworkResponse.class);
+ }
+ };
+ // delay only for first
+ when(mMockNetwork.performRequest(req1)).thenAnswer(delayAnswer);
+ when(mMockNetwork.performRequest(req2)).thenReturn(mock(NetworkResponse.class));
+
+ RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 3, mDelivery);
+ queue.addRequestFinishedListener(mMockListener);
+ queue.add(req1);
+ queue.add(req2);
+ queue.start();
+
+ InOrder inOrder = inOrder(mMockListener);
+ // verify req1 goes through first
+ inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(req1);
+ // verify req2 goes last
+ inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(req2);
+
+ queue.stop();
+ }
+
+ /** Verify RequestFinishedListeners are informed when requests are canceled. */
+ @Test
+ public void add_requestFinishedListenerCanceled() throws Exception {
+ MockRequest request = new MockRequest();
+ Answer<NetworkResponse> delayAnswer =
+ new Answer<NetworkResponse>() {
+ @Override
+ public NetworkResponse answer(InvocationOnMock invocationOnMock)
+ throws Throwable {
+ Thread.sleep(200);
+ return mock(NetworkResponse.class);
+ }
+ };
+ RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);
+
+ when(mMockNetwork.performRequest(request)).thenAnswer(delayAnswer);
+
+ queue.addRequestFinishedListener(mMockListener);
+ queue.start();
+ queue.add(request);
+
+ request.cancel();
+ verify(mMockListener, timeout(10000)).onRequestFinished(request);
+ queue.stop();
+ }
+
+ /** Verify RequestFinishedListeners are informed when requests are successfully delivered. */
+ @Test
+ public void add_requestFinishedListenerSuccess() throws Exception {
+ MockRequest request = new MockRequest();
+ RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);
+
+ queue.addRequestFinishedListener(mMockListener);
+ queue.addRequestFinishedListener(mMockListener2);
+ queue.start();
+ queue.add(request);
+
+ verify(mMockListener, timeout(10000)).onRequestFinished(request);
+ verify(mMockListener2, timeout(10000)).onRequestFinished(request);
+
+ queue.stop();
+ }
+
+ /** Verify RequestFinishedListeners are informed when request errors. */
+ @Test
+ public void add_requestFinishedListenerError() throws Exception {
+ MockRequest request = new MockRequest();
+ RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);
+
+ when(mMockNetwork.performRequest(request)).thenThrow(new VolleyError());
+
+ queue.addRequestFinishedListener(mMockListener);
+ queue.start();
+ queue.add(request);
+
+ verify(mMockListener, timeout(10000)).onRequestFinished(request);
+ queue.stop();
+ }
+}
diff --git a/core/src/test/java/com/android/volley/RequestQueueTest.java b/core/src/test/java/com/android/volley/RequestQueueTest.java
new file mode 100644
index 0000000..ba9b0f8
--- /dev/null
+++ b/core/src/test/java/com/android/volley/RequestQueueTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.NoCache;
+import com.android.volley.toolbox.StringRequest;
+import com.android.volley.utils.ImmediateResponseDelivery;
+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 RequestQueue, with all dependencies mocked out */
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowSystemClock.class})
+public class RequestQueueTest {
+
+ private ResponseDelivery mDelivery;
+ @Mock private Network mMockNetwork;
+
+ @Before
+ public void setUp() throws Exception {
+ mDelivery = new ImmediateResponseDelivery();
+ initMocks(this);
+ }
+
+ @Test
+ public void cancelAll_onlyCorrectTag() throws Exception {
+ RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery);
+ 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
+ }
+
+ @Test
+ public void add_notifiesListener() throws Exception {
+ RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
+ RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery);
+ queue.addRequestEventListener(listener);
+ StringRequest req = mock(StringRequest.class);
+
+ queue.add(req);
+
+ verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_QUEUED);
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void finish_notifiesListener() throws Exception {
+ RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
+ RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery);
+ queue.addRequestEventListener(listener);
+ StringRequest req = mock(StringRequest.class);
+
+ queue.finish(req);
+
+ verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_FINISHED);
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void sendRequestEvent_notifiesListener() throws Exception {
+ StringRequest req = mock(StringRequest.class);
+ RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
+ RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery);
+ queue.addRequestEventListener(listener);
+
+ queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED);
+
+ verify(listener)
+ .onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED);
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void removeRequestEventListener_removesListener() throws Exception {
+ StringRequest req = mock(StringRequest.class);
+ RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
+ RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery);
+ queue.addRequestEventListener(listener);
+ queue.removeRequestEventListener(listener);
+
+ queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED);
+
+ verifyNoMoreInteractions(listener);
+ }
+}
diff --git a/core/src/test/java/com/android/volley/RequestTest.java b/core/src/test/java/com/android/volley/RequestTest.java
new file mode 100644
index 0000000..cced39f
--- /dev/null
+++ b/core/src/test/java/com/android/volley/RequestTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.android.volley.Request.Method;
+import com.android.volley.Request.Priority;
+import com.android.volley.toolbox.NoCache;
+import java.util.Collections;
+import java.util.Map;
+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 RequestTest {
+ private @Mock ResponseDelivery mDelivery;
+ private @Mock Network mNetwork;
+
+ @Before
+ public void setUp() throws Exception {
+ initMocks(this);
+ }
+
+ @Test
+ public void compareTo() {
+ int sequence = 0;
+ TestRequest low = new TestRequest(Priority.LOW);
+ low.setSequence(sequence++);
+ TestRequest low2 = new TestRequest(Priority.LOW);
+ low2.setSequence(sequence++);
+ TestRequest high = new TestRequest(Priority.HIGH);
+ high.setSequence(sequence++);
+ TestRequest immediate = new TestRequest(Priority.IMMEDIATE);
+ immediate.setSequence(sequence++);
+
+ // "Low" should sort higher because it's really processing order.
+ assertTrue(low.compareTo(high) > 0);
+ assertTrue(high.compareTo(low) < 0);
+ assertTrue(low.compareTo(low2) < 0);
+ assertTrue(low.compareTo(immediate) > 0);
+ assertTrue(immediate.compareTo(high) < 0);
+ }
+
+ private static class TestRequest extends Request<Object> {
+ private Priority mPriority = Priority.NORMAL;
+
+ public TestRequest(Priority priority) {
+ super(Request.Method.GET, "", null);
+ mPriority = priority;
+ }
+
+ @Override
+ public Priority getPriority() {
+ return mPriority;
+ }
+
+ @Override
+ protected void deliverResponse(Object response) {}
+
+ @Override
+ protected Response<Object> parseNetworkResponse(NetworkResponse response) {
+ return null;
+ }
+ }
+
+ @Test
+ public void urlParsing() {
+ UrlParseRequest nullUrl = new UrlParseRequest(null);
+ assertEquals(0, nullUrl.getTrafficStatsTag());
+ UrlParseRequest emptyUrl = new UrlParseRequest("");
+ assertEquals(0, emptyUrl.getTrafficStatsTag());
+ UrlParseRequest noHost = new UrlParseRequest("http:///");
+ assertEquals(0, noHost.getTrafficStatsTag());
+ UrlParseRequest badProtocol = new UrlParseRequest("bad:http://foo");
+ assertEquals(0, badProtocol.getTrafficStatsTag());
+ UrlParseRequest goodProtocol = new UrlParseRequest("http://foo");
+ assertFalse(0 == goodProtocol.getTrafficStatsTag());
+ }
+
+ @Test
+ public void getCacheKey() {
+ assertEquals(
+ "http://example.com",
+ new UrlParseRequest(Method.GET, "http://example.com").getCacheKey());
+ assertEquals(
+ "http://example.com",
+ new UrlParseRequest(Method.DEPRECATED_GET_OR_POST, "http://example.com")
+ .getCacheKey());
+ assertEquals(
+ "1-http://example.com",
+ new UrlParseRequest(Method.POST, "http://example.com").getCacheKey());
+ assertEquals(
+ "2-http://example.com",
+ new UrlParseRequest(Method.PUT, "http://example.com").getCacheKey());
+ }
+
+ private static class UrlParseRequest extends Request<Object> {
+ UrlParseRequest(String url) {
+ this(Method.GET, url);
+ }
+
+ UrlParseRequest(int method, String url) {
+ super(method, url, null);
+ }
+
+ @Override
+ protected void deliverResponse(Object response) {}
+
+ @Override
+ protected Response<Object> parseNetworkResponse(NetworkResponse response) {
+ return null;
+ }
+ }
+
+ @Test
+ public void nullKeyInPostParams() throws Exception {
+ Request<Object> request =
+ new Request<Object>(Method.POST, "url", null) {
+ @Override
+ protected void deliverResponse(Object response) {}
+
+ @Override
+ protected Response<Object> parseNetworkResponse(NetworkResponse response) {
+ return null;
+ }
+
+ @Override
+ protected Map<String, String> getParams() {
+ return Collections.singletonMap(null, "value");
+ }
+
+ @Override
+ protected Map<String, String> getPostParams() {
+ return Collections.singletonMap(null, "value");
+ }
+ };
+ try {
+ request.getBody();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ try {
+ request.getPostBody();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void nullValueInPostParams() throws Exception {
+ Request<Object> request =
+ new Request<Object>(Method.POST, "url", null) {
+ @Override
+ protected void deliverResponse(Object response) {}
+
+ @Override
+ protected Response<Object> parseNetworkResponse(NetworkResponse response) {
+ return null;
+ }
+
+ @Override
+ protected Map<String, String> getParams() {
+ return Collections.singletonMap("key", null);
+ }
+
+ @Override
+ protected Map<String, String> getPostParams() {
+ return Collections.singletonMap("key", null);
+ }
+ };
+ try {
+ request.getBody();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ try {
+ request.getPostBody();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void sendEvent_notifiesListeners() throws Exception {
+ RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
+ RequestQueue queue = new RequestQueue(new NoCache(), mNetwork, 0, mDelivery);
+ queue.addRequestEventListener(listener);
+
+ Request<Object> request =
+ new Request<Object>(Method.POST, "url", null) {
+ @Override
+ protected void deliverResponse(Object response) {}
+
+ @Override
+ protected Response<Object> parseNetworkResponse(NetworkResponse response) {
+ return null;
+ }
+ };
+ request.setRequestQueue(queue);
+
+ request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED);
+
+ verify(listener)
+ .onRequestEvent(
+ request, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED);
+ verifyNoMoreInteractions(listener);
+ }
+}
diff --git a/core/src/test/java/com/android/volley/ResponseDeliveryTest.java b/core/src/test/java/com/android/volley/ResponseDeliveryTest.java
new file mode 100644
index 0000000..6e71c3b
--- /dev/null
+++ b/core/src/test/java/com/android/volley/ResponseDeliveryTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.volley.mock.MockRequest;
+import com.android.volley.utils.CacheTestUtils;
+import com.android.volley.utils.ImmediateResponseDelivery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ResponseDeliveryTest {
+
+ private ExecutorDelivery mDelivery;
+ private MockRequest mRequest;
+ private Response<byte[]> mSuccessResponse;
+
+ @Before
+ public void setUp() throws Exception {
+ // Make the delivery just run its posted responses immediately.
+ mDelivery = new ImmediateResponseDelivery();
+ mRequest = new MockRequest();
+ mRequest.setSequence(1);
+ byte[] data = new byte[16];
+ Cache.Entry cacheEntry = CacheTestUtils.makeRandomCacheEntry(data);
+ mSuccessResponse = Response.success(data, cacheEntry);
+ }
+
+ @Test
+ public void postResponseCallsDeliverResponse() {
+ mDelivery.postResponse(mRequest, mSuccessResponse);
+ assertTrue(mRequest.deliverResponse_called);
+ assertFalse(mRequest.deliverError_called);
+ }
+
+ @Test
+ public void postResponseSuppressesCanceled() {
+ mRequest.cancel();
+ mDelivery.postResponse(mRequest, mSuccessResponse);
+ assertFalse(mRequest.deliverResponse_called);
+ assertFalse(mRequest.deliverError_called);
+ }
+
+ @Test
+ public void postErrorCallsDeliverError() {
+ Response<byte[]> errorResponse = Response.error(new ServerError());
+
+ mDelivery.postResponse(mRequest, errorResponse);
+ assertTrue(mRequest.deliverError_called);
+ assertFalse(mRequest.deliverResponse_called);
+ }
+}
diff --git a/core/src/test/java/com/android/volley/mock/MockAsyncStack.java b/core/src/test/java/com/android/volley/mock/MockAsyncStack.java
new file mode 100644
index 0000000..5ea8343
--- /dev/null
+++ b/core/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/core/src/test/java/com/android/volley/mock/MockHttpStack.java b/core/src/test/java/com/android/volley/mock/MockHttpStack.java
new file mode 100644
index 0000000..b86e7a0
--- /dev/null
+++ b/core/src/test/java/com/android/volley/mock/MockHttpStack.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.mock;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.toolbox.BaseHttpStack;
+import com.android.volley.toolbox.HttpResponse;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class MockHttpStack extends BaseHttpStack {
+
+ 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 HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ if (mExceptionToThrow != null) {
+ throw mExceptionToThrow;
+ }
+ mLastUrl = request.getUrl();
+ mLastHeaders = new HashMap<>();
+ if (request.getHeaders() != null) {
+ mLastHeaders.putAll(request.getHeaders());
+ }
+ if (additionalHeaders != null) {
+ mLastHeaders.putAll(additionalHeaders);
+ }
+ try {
+ mLastPostBody = request.getBody();
+ } catch (AuthFailureError e) {
+ mLastPostBody = null;
+ }
+ return mResponseToReturn;
+ }
+}
diff --git a/core/src/test/java/com/android/volley/mock/MockRequest.java b/core/src/test/java/com/android/volley/mock/MockRequest.java
new file mode 100644
index 0000000..6fc26b4
--- /dev/null
+++ b/core/src/test/java/com/android/volley/mock/MockRequest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.mock;
+
+import com.android.volley.NetworkResponse;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.Response.ErrorListener;
+import com.android.volley.VolleyError;
+import com.android.volley.utils.CacheTestUtils;
+import java.util.HashMap;
+import java.util.Map;
+
+public class MockRequest extends Request<byte[]> {
+ public MockRequest() {
+ super(Request.Method.GET, "http://foo.com", null);
+ }
+
+ public MockRequest(String url, ErrorListener listener) {
+ super(Request.Method.GET, url, listener);
+ }
+
+ private Map<String, String> mPostParams = new HashMap<String, String>();
+
+ public void setPostParams(Map<String, String> postParams) {
+ mPostParams = postParams;
+ }
+
+ @Override
+ public Map<String, String> getPostParams() {
+ return mPostParams;
+ }
+
+ private String mCacheKey = super.getCacheKey();
+
+ public void setCacheKey(String cacheKey) {
+ mCacheKey = cacheKey;
+ }
+
+ @Override
+ public String getCacheKey() {
+ return mCacheKey;
+ }
+
+ public boolean deliverResponse_called = false;
+ public boolean parseResponse_called = false;
+
+ @Override
+ protected void deliverResponse(byte[] response) {
+ deliverResponse_called = true;
+ }
+
+ public boolean deliverError_called = false;
+
+ @Override
+ public void deliverError(VolleyError error) {
+ super.deliverError(error);
+ deliverError_called = true;
+ }
+
+ public boolean cancel_called = false;
+
+ @Override
+ public void cancel() {
+ cancel_called = true;
+ super.cancel();
+ }
+
+ private Priority mPriority = super.getPriority();
+
+ public void setPriority(Priority priority) {
+ mPriority = priority;
+ }
+
+ @Override
+ public Priority getPriority() {
+ return mPriority;
+ }
+
+ @Override
+ protected Response<byte[]> parseNetworkResponse(NetworkResponse response) {
+ parseResponse_called = true;
+ return Response.success(response.data, CacheTestUtils.makeRandomCacheEntry(response.data));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/mock/ShadowSystemClock.java b/core/src/test/java/com/android/volley/mock/ShadowSystemClock.java
new file mode 100644
index 0000000..6d75d4b
--- /dev/null
+++ b/core/src/test/java/com/android/volley/mock/ShadowSystemClock.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2015 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 android.os.SystemClock;
+import org.robolectric.annotation.Implements;
+
+@Implements(value = SystemClock.class, callThroughByDefault = true)
+public class ShadowSystemClock {
+ public static long elapsedRealtime() {
+ return 0;
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java b/core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java
new file mode 100644
index 0000000..dbd6535
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java
@@ -0,0 +1,128 @@
+package com.android.volley.toolbox;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.when;
+
+import com.android.volley.Header;
+import com.android.volley.Request;
+import com.android.volley.mock.TestRequest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.message.BasicHeader;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AdaptedHttpStackTest {
+ private static final Request<?> REQUEST = new TestRequest.Get();
+ private static final Map<String, String> ADDITIONAL_HEADERS = Collections.emptyMap();
+
+ @Mock private HttpStack mHttpStack;
+ @Mock private HttpResponse mHttpResponse;
+ @Mock private StatusLine mStatusLine;
+ @Mock private HttpEntity mHttpEntity;
+ @Mock private InputStream mContent;
+
+ private AdaptedHttpStack mAdaptedHttpStack;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mAdaptedHttpStack = new AdaptedHttpStack(mHttpStack);
+ when(mHttpResponse.getStatusLine()).thenReturn(mStatusLine);
+ }
+
+ @Test(expected = SocketTimeoutException.class)
+ public void requestTimeout() throws Exception {
+ when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS))
+ .thenThrow(new ConnectTimeoutException());
+
+ mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+ }
+
+ @Test
+ public void emptyResponse() throws Exception {
+ when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse);
+ when(mStatusLine.getStatusCode()).thenReturn(12345);
+ when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]);
+
+ com.android.volley.toolbox.HttpResponse response =
+ mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+
+ assertEquals(12345, response.getStatusCode());
+ assertEquals(Collections.emptyList(), response.getHeaders());
+ assertNull(response.getContent());
+ }
+
+ @Test
+ public void nonEmptyResponse() throws Exception {
+ when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse);
+ when(mStatusLine.getStatusCode()).thenReturn(12345);
+ when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]);
+ when(mHttpResponse.getEntity()).thenReturn(mHttpEntity);
+ when(mHttpEntity.getContentLength()).thenReturn((long) Integer.MAX_VALUE);
+ when(mHttpEntity.getContent()).thenReturn(mContent);
+
+ com.android.volley.toolbox.HttpResponse response =
+ mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+
+ assertEquals(12345, response.getStatusCode());
+ assertEquals(Collections.emptyList(), response.getHeaders());
+ assertEquals(Integer.MAX_VALUE, response.getContentLength());
+ assertSame(mContent, response.getContent());
+ }
+
+ @Test(expected = IOException.class)
+ public void responseTooBig() throws Exception {
+ when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse);
+ when(mStatusLine.getStatusCode()).thenReturn(12345);
+ when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]);
+ when(mHttpResponse.getEntity()).thenReturn(mHttpEntity);
+ when(mHttpEntity.getContentLength()).thenReturn(Integer.MAX_VALUE + 1L);
+ when(mHttpEntity.getContent()).thenReturn(mContent);
+
+ mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+ }
+
+ @Test
+ public void responseWithHeaders() throws Exception {
+ when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse);
+ when(mStatusLine.getStatusCode()).thenReturn(12345);
+ when(mHttpResponse.getAllHeaders())
+ .thenReturn(
+ new org.apache.http.Header[] {
+ new BasicHeader("header1", "value1_B"),
+ new BasicHeader("header3", "value3"),
+ new BasicHeader("HEADER2", "value2"),
+ new BasicHeader("header1", "value1_A")
+ });
+
+ com.android.volley.toolbox.HttpResponse response =
+ mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+
+ assertEquals(12345, response.getStatusCode());
+ assertNull(response.getContent());
+
+ List<Header> expectedHeaders = new ArrayList<>();
+ expectedHeaders.add(new Header("header1", "value1_B"));
+ expectedHeaders.add(new Header("header3", "value3"));
+ expectedHeaders.add(new Header("HEADER2", "value2"));
+ expectedHeaders.add(new Header("header1", "value1_A"));
+ assertEquals(expectedHeaders, response.getHeaders());
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java b/core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java
new file mode 100644
index 0000000..982eda2
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 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.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import com.android.volley.AuthFailureError;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AndroidAuthenticatorTest {
+ @Mock private AccountManager mAccountManager;
+ @Mock private AccountManagerFuture<Bundle> mFuture;
+ private Account mAccount;
+ private AndroidAuthenticator mAuthenticator;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mAccount = new Account("coolperson", "cooltype");
+ mAuthenticator = new AndroidAuthenticator(mAccountManager, mAccount, "cooltype", false);
+ }
+
+ @Test(expected = AuthFailureError.class)
+ public void failedGetAuthToken() throws Exception {
+ when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null))
+ .thenReturn(mFuture);
+ when(mFuture.getResult()).thenThrow(new AuthenticatorException("sadness!"));
+ mAuthenticator.getAuthToken();
+ }
+
+ @Test(expected = AuthFailureError.class)
+ public void resultContainsIntent() throws Exception {
+ Intent intent = new Intent();
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent);
+ when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null))
+ .thenReturn(mFuture);
+ when(mFuture.getResult()).thenReturn(bundle);
+ when(mFuture.isDone()).thenReturn(true);
+ when(mFuture.isCancelled()).thenReturn(false);
+ mAuthenticator.getAuthToken();
+ }
+
+ @Test(expected = AuthFailureError.class)
+ public void missingAuthToken() throws Exception {
+ Bundle bundle = new Bundle();
+ when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null))
+ .thenReturn(mFuture);
+ when(mFuture.getResult()).thenReturn(bundle);
+ when(mFuture.isDone()).thenReturn(true);
+ when(mFuture.isCancelled()).thenReturn(false);
+ mAuthenticator.getAuthToken();
+ }
+
+ @Test
+ public void invalidateAuthToken() throws Exception {
+ mAuthenticator.invalidateAuthToken("monkey");
+ verify(mAccountManager).invalidateAuthToken("cooltype", "monkey");
+ }
+
+ @Test
+ public void goodToken() throws Exception {
+ Bundle bundle = new Bundle();
+ bundle.putString(AccountManager.KEY_AUTHTOKEN, "monkey");
+ when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null))
+ .thenReturn(mFuture);
+ when(mFuture.getResult()).thenReturn(bundle);
+ when(mFuture.isDone()).thenReturn(true);
+ when(mFuture.isCancelled()).thenReturn(false);
+ Assert.assertEquals("monkey", mAuthenticator.getAuthToken());
+ }
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch-all test to find API-breaking changes.
+ Context context = mock(Context.class);
+ new AndroidAuthenticator(context, mAccount, "cooltype");
+ new AndroidAuthenticator(context, mAccount, "cooltype", true);
+ Assert.assertSame(mAccount, mAuthenticator.getAccount());
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java b/core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java
new file mode 100644
index 0000000..1049ad0
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java
@@ -0,0 +1,104 @@
+package com.android.volley.toolbox;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.Request;
+import com.android.volley.mock.TestRequest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class BaseHttpStackTest {
+ private static final Request<?> REQUEST = new TestRequest.Get();
+ private static final Map<String, String> ADDITIONAL_HEADERS = Collections.emptyMap();
+
+ @Mock private InputStream mContent;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void legacyRequestWithoutBody() throws Exception {
+ BaseHttpStack stack =
+ new BaseHttpStack() {
+ @Override
+ public HttpResponse executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ assertSame(REQUEST, request);
+ assertSame(ADDITIONAL_HEADERS, additionalHeaders);
+ return new HttpResponse(12345, Collections.<Header>emptyList());
+ }
+ };
+ org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS);
+ assertEquals(12345, resp.getStatusLine().getStatusCode());
+ assertEquals(0, resp.getAllHeaders().length);
+ assertNull(resp.getEntity());
+ }
+
+ @Test
+ public void legacyResponseWithBody() throws Exception {
+ BaseHttpStack stack =
+ new BaseHttpStack() {
+ @Override
+ public HttpResponse executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ assertSame(REQUEST, request);
+ assertSame(ADDITIONAL_HEADERS, additionalHeaders);
+ return new HttpResponse(
+ 12345, Collections.<Header>emptyList(), 555, mContent);
+ }
+ };
+ org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS);
+ assertEquals(12345, resp.getStatusLine().getStatusCode());
+ assertEquals(0, resp.getAllHeaders().length);
+ assertEquals(555L, resp.getEntity().getContentLength());
+ assertSame(mContent, resp.getEntity().getContent());
+ }
+
+ @Test
+ public void legacyResponseHeaders() throws Exception {
+ BaseHttpStack stack =
+ new BaseHttpStack() {
+ @Override
+ public HttpResponse executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ assertSame(REQUEST, request);
+ assertSame(ADDITIONAL_HEADERS, additionalHeaders);
+ List<Header> headers = new ArrayList<>();
+ headers.add(new Header("HeaderA", "ValueA"));
+ headers.add(new Header("HeaderB", "ValueB_1"));
+ headers.add(new Header("HeaderB", "ValueB_2"));
+ return new HttpResponse(12345, headers);
+ }
+ };
+ org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS);
+ assertEquals(12345, resp.getStatusLine().getStatusCode());
+ assertEquals(3, resp.getAllHeaders().length);
+ assertEquals("HeaderA", resp.getAllHeaders()[0].getName());
+ assertEquals("ValueA", resp.getAllHeaders()[0].getValue());
+ assertEquals("HeaderB", resp.getAllHeaders()[1].getName());
+ assertEquals("ValueB_1", resp.getAllHeaders()[1].getValue());
+ assertEquals("HeaderB", resp.getAllHeaders()[2].getName());
+ assertEquals("ValueB_2", resp.getAllHeaders()[2].getValue());
+ assertNull(resp.getEntity());
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java b/core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java
new file mode 100644
index 0000000..91d4062
--- /dev/null
+++ b/core/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/core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java b/core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
new file mode 100644
index 0000000..3630379
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+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.MockHttpStack;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+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 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 BasicNetworkTest {
+
+ @Mock private Request<String> mMockRequest;
+ @Mock private RetryPolicy mMockRetryPolicy;
+
+ @Before
+ public void setUp() throws Exception {
+ initMocks(this);
+ }
+
+ @Test
+ public void headersAndPostParams() throws Exception {
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ InputStream responseStream =
+ new ByteArrayInputStream("foobar".getBytes(StandardCharsets.UTF_8));
+ HttpResponse fakeResponse =
+ new HttpResponse(200, Collections.<Header>emptyList(), 6, responseStream);
+ mockHttpStack.setResponseToReturn(fakeResponse);
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ Entry entry = new Entry();
+ entry.etag = "foobar";
+ entry.lastModified = 1503102002000L;
+ request.setCacheEntry(entry);
+ httpNetwork.performRequest(request);
+ assertEquals("foo", mockHttpStack.getLastHeaders().get("requestheader"));
+ assertEquals("foobar", mockHttpStack.getLastHeaders().get("If-None-Match"));
+ assertEquals(
+ "Sat, 19 Aug 2017 00:20:02 GMT",
+ mockHttpStack.getLastHeaders().get("If-Modified-Since"));
+ assertEquals(
+ "requestpost=foo&",
+ new String(mockHttpStack.getLastPostBody(), StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void notModified() throws Exception {
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ List<Header> headers = new ArrayList<>();
+ headers.add(new Header("ServerKeyA", "ServerValueA"));
+ headers.add(new Header("ServerKeyB", "ServerValueB"));
+ headers.add(new Header("SharedKey", "ServerValueShared"));
+ headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1"));
+ headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2"));
+ HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers);
+ mockHttpStack.setResponseToReturn(fakeResponse);
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ Entry entry = new Entry();
+ entry.allResponseHeaders = new ArrayList<>();
+ entry.allResponseHeaders.add(new Header("CachedKeyA", "CachedValueA"));
+ entry.allResponseHeaders.add(new Header("CachedKeyB", "CachedValueB"));
+ entry.allResponseHeaders.add(new Header("SharedKey", "CachedValueShared"));
+ entry.allResponseHeaders.add(new Header("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1"));
+ entry.allResponseHeaders.add(new Header("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2"));
+ request.setCacheEntry(entry);
+ NetworkResponse response = httpNetwork.performRequest(request);
+ List<Header> expectedHeaders = new ArrayList<>();
+ // Should have all server headers + cache headers that didn't show up in server response.
+ expectedHeaders.add(new Header("ServerKeyA", "ServerValueA"));
+ expectedHeaders.add(new Header("ServerKeyB", "ServerValueB"));
+ expectedHeaders.add(new Header("SharedKey", "ServerValueShared"));
+ expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1"));
+ expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2"));
+ expectedHeaders.add(new Header("CachedKeyA", "CachedValueA"));
+ expectedHeaders.add(new Header("CachedKeyB", "CachedValueB"));
+ assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0])));
+ }
+
+ @Test
+ public void notModified_legacyCache() throws Exception {
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ List<Header> headers = new ArrayList<>();
+ headers.add(new Header("ServerKeyA", "ServerValueA"));
+ headers.add(new Header("ServerKeyB", "ServerValueB"));
+ headers.add(new Header("SharedKey", "ServerValueShared"));
+ headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1"));
+ headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2"));
+ HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers);
+ mockHttpStack.setResponseToReturn(fakeResponse);
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ Entry entry = new Entry();
+ entry.responseHeaders = new HashMap<>();
+ entry.responseHeaders.put("CachedKeyA", "CachedValueA");
+ entry.responseHeaders.put("CachedKeyB", "CachedValueB");
+ entry.responseHeaders.put("SharedKey", "CachedValueShared");
+ entry.responseHeaders.put("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1");
+ entry.responseHeaders.put("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2");
+ request.setCacheEntry(entry);
+ NetworkResponse response = httpNetwork.performRequest(request);
+ List<Header> expectedHeaders = new ArrayList<>();
+ // Should have all server headers + cache headers that didn't show up in server response.
+ expectedHeaders.add(new Header("ServerKeyA", "ServerValueA"));
+ expectedHeaders.add(new Header("ServerKeyB", "ServerValueB"));
+ expectedHeaders.add(new Header("SharedKey", "ServerValueShared"));
+ expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1"));
+ expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2"));
+ expectedHeaders.add(new Header("CachedKeyA", "CachedValueA"));
+ expectedHeaders.add(new Header("CachedKeyB", "CachedValueB"));
+ assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0])));
+ }
+
+ @Test
+ public void socketTimeout() throws Exception {
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ mockHttpStack.setExceptionToThrow(new SocketTimeoutException());
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ try {
+ httpNetwork.performRequest(request);
+ } catch (VolleyError e) {
+ // expected
+ }
+ // should retry socket timeouts
+ verify(mMockRetryPolicy).retry(any(TimeoutError.class));
+ }
+
+ @Test
+ public void noConnectionDefault() throws Exception {
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ mockHttpStack.setExceptionToThrow(new IOException());
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ try {
+ httpNetwork.performRequest(request);
+ } catch (VolleyError e) {
+ // expected
+ }
+ // should not retry when there is no connection
+ verify(mMockRetryPolicy, never()).retry(any(VolleyError.class));
+ }
+
+ @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());
+ mockHttpStack.setResponseToReturn(fakeResponse);
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ try {
+ httpNetwork.performRequest(request);
+ } catch (VolleyError e) {
+ // expected
+ }
+ // should retry in case it's an auth failure.
+ verify(mMockRetryPolicy).retry(any(AuthFailureError.class));
+ }
+
+ @Test
+ public void forbidden() throws Exception {
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ HttpResponse fakeResponse = new HttpResponse(403, Collections.<Header>emptyList());
+ mockHttpStack.setResponseToReturn(fakeResponse);
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ try {
+ httpNetwork.performRequest(request);
+ } catch (VolleyError e) {
+ // expected
+ }
+ // should retry in case it's an auth failure.
+ verify(mMockRetryPolicy).retry(any(AuthFailureError.class));
+ }
+
+ @Test
+ public void redirect() throws Exception {
+ for (int i = 300; i <= 399; i++) {
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
+ mockHttpStack.setResponseToReturn(fakeResponse);
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ try {
+ httpNetwork.performRequest(request);
+ } catch (VolleyError e) {
+ // expected
+ }
+ // should not retry 300 responses.
+ verify(mMockRetryPolicy, never()).retry(any(VolleyError.class));
+ reset(mMockRetryPolicy);
+ }
+ }
+
+ @Test
+ public void otherClientError() throws Exception {
+ for (int i = 400; i <= 499; i++) {
+ if (i == 401 || i == 403) {
+ // covered above.
+ continue;
+ }
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
+ mockHttpStack.setResponseToReturn(fakeResponse);
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ try {
+ httpNetwork.performRequest(request);
+ } catch (VolleyError e) {
+ // expected
+ }
+ // should not retry other 400 errors.
+ verify(mMockRetryPolicy, never()).retry(any(VolleyError.class));
+ reset(mMockRetryPolicy);
+ }
+ }
+
+ @Test
+ public void serverError_enableRetries() throws Exception {
+ for (int i = 500; i <= 599; i++) {
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
+ mockHttpStack.setResponseToReturn(fakeResponse);
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack, new ByteArrayPool(4096));
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ request.setShouldRetryServerErrors(true);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ try {
+ httpNetwork.performRequest(request);
+ } catch (VolleyError e) {
+ // expected
+ }
+ // should retry all 500 errors
+ verify(mMockRetryPolicy).retry(any(ServerError.class));
+ reset(mMockRetryPolicy);
+ }
+ }
+
+ @Test
+ public void serverError_disableRetries() throws Exception {
+ for (int i = 500; i <= 599; i++) {
+ MockHttpStack mockHttpStack = new MockHttpStack();
+ HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
+ mockHttpStack.setResponseToReturn(fakeResponse);
+ BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+ Request<String> request = buildRequest();
+ request.setRetryPolicy(mMockRetryPolicy);
+ doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
+ try {
+ httpNetwork.performRequest(request);
+ } catch (VolleyError e) {
+ // expected
+ }
+ // should not retry any 500 error w/ HTTP 500 retries turned off (the default).
+ verify(mMockRetryPolicy, never()).retry(any(VolleyError.class));
+ reset(mMockRetryPolicy);
+ }
+ }
+
+ 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/core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java b/core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java
new file mode 100644
index 0000000..62da207
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class ByteArrayPoolTest {
+ @Test
+ public void reusesBuffer() {
+ ByteArrayPool pool = new ByteArrayPool(32);
+
+ byte[] buf1 = pool.getBuf(16);
+ byte[] buf2 = pool.getBuf(16);
+
+ pool.returnBuf(buf1);
+ pool.returnBuf(buf2);
+
+ byte[] buf3 = pool.getBuf(16);
+ byte[] buf4 = pool.getBuf(16);
+ assertTrue(buf3 == buf1 || buf3 == buf2);
+ assertTrue(buf4 == buf1 || buf4 == buf2);
+ assertTrue(buf3 != buf4);
+ }
+
+ @Test
+ public void obeysSizeLimit() {
+ ByteArrayPool pool = new ByteArrayPool(32);
+
+ byte[] buf1 = pool.getBuf(16);
+ byte[] buf2 = pool.getBuf(16);
+ byte[] buf3 = pool.getBuf(16);
+
+ pool.returnBuf(buf1);
+ pool.returnBuf(buf2);
+ pool.returnBuf(buf3);
+
+ byte[] buf4 = pool.getBuf(16);
+ byte[] buf5 = pool.getBuf(16);
+ byte[] buf6 = pool.getBuf(16);
+
+ assertTrue(buf4 == buf2 || buf4 == buf3);
+ assertTrue(buf5 == buf2 || buf5 == buf3);
+ assertTrue(buf4 != buf5);
+ assertTrue(buf6 != buf1 && buf6 != buf2 && buf6 != buf3);
+ }
+
+ @Test
+ public void returnsBufferWithRightSize() {
+ ByteArrayPool pool = new ByteArrayPool(32);
+
+ byte[] buf1 = pool.getBuf(16);
+ pool.returnBuf(buf1);
+
+ byte[] buf2 = pool.getBuf(17);
+ assertNotSame(buf2, buf1);
+
+ byte[] buf3 = pool.getBuf(15);
+ assertSame(buf3, buf1);
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/CacheTest.java b/core/src/test/java/com/android/volley/toolbox/CacheTest.java
new file mode 100644
index 0000000..22dae22
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/CacheTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 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.junit.Assert.assertNotNull;
+
+import com.android.volley.Cache;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class CacheTest {
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch-all test to find API-breaking changes.
+ assertNotNull(Cache.class.getMethod("get", String.class));
+ assertNotNull(Cache.class.getMethod("put", String.class, Cache.Entry.class));
+ assertNotNull(Cache.class.getMethod("initialize"));
+ assertNotNull(Cache.class.getMethod("invalidate", String.class, boolean.class));
+ assertNotNull(Cache.class.getMethod("remove", String.class));
+ assertNotNull(Cache.class.getMethod("clear"));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
new file mode 100644
index 0000000..db6e491
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
@@ -0,0 +1,646 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import static org.hamcrest.Matchers.arrayWithSize;
+import static org.hamcrest.Matchers.emptyArray;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import com.android.volley.Cache;
+import com.android.volley.Header;
+import com.android.volley.toolbox.DiskBasedCache.CacheHeader;
+import com.android.volley.toolbox.DiskBasedCache.CountingInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Random;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 16)
+public class DiskBasedCacheTest {
+
+ private static final int MAX_SIZE = 1024 * 1024;
+
+ private Cache cache;
+
+ @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Rule public ExpectedException exception = ExpectedException.none();
+
+ @Before
+ public void setup() throws IOException {
+ // Initialize empty cache
+ cache = new DiskBasedCache(temporaryFolder.getRoot(), MAX_SIZE);
+ cache.initialize();
+ }
+
+ @After
+ public void teardown() {
+ cache = null;
+ }
+
+ @Test
+ public void testEmptyInitialize() {
+ assertThat(cache.get("key"), is(nullValue()));
+ }
+
+ @Test
+ public void testPutGetZeroBytes() {
+ Cache.Entry entry = new Cache.Entry();
+ entry.data = new byte[0];
+ entry.serverDate = 1234567L;
+ entry.lastModified = 13572468L;
+ entry.ttl = 9876543L;
+ entry.softTtl = 8765432L;
+ entry.etag = "etag";
+ entry.responseHeaders = new HashMap<>();
+ entry.responseHeaders.put("fruit", "banana");
+ entry.responseHeaders.put("color", "yellow");
+ cache.put("my-magical-key", entry);
+
+ assertThatEntriesAreEqual(cache.get("my-magical-key"), entry);
+ assertThat(cache.get("unknown-key"), is(nullValue()));
+ }
+
+ @Test
+ public void testPutRemoveGet() {
+ Cache.Entry entry = randomData(511);
+ cache.put("key", entry);
+
+ assertThatEntriesAreEqual(cache.get("key"), entry);
+
+ cache.remove("key");
+ assertThat(cache.get("key"), is(nullValue()));
+ assertThat(listCachedFiles(), is(emptyArray()));
+ }
+
+ @Test
+ public void testPutClearGet() {
+ Cache.Entry entry = randomData(511);
+ cache.put("key", entry);
+
+ assertThatEntriesAreEqual(cache.get("key"), entry);
+
+ cache.clear();
+ assertThat(cache.get("key"), is(nullValue()));
+ assertThat(listCachedFiles(), is(emptyArray()));
+ }
+
+ @Test
+ public void testReinitialize() {
+ Cache.Entry entry = randomData(1023);
+ cache.put("key", entry);
+
+ Cache copy = new DiskBasedCache(temporaryFolder.getRoot(), MAX_SIZE);
+ copy.initialize();
+
+ assertThatEntriesAreEqual(copy.get("key"), entry);
+ }
+
+ @Test
+ public void testInvalidate() {
+ Cache.Entry entry = randomData(32);
+ entry.softTtl = 8765432L;
+ entry.ttl = 9876543L;
+ cache.put("key", entry);
+
+ cache.invalidate("key", false);
+ entry.softTtl = 0; // expired
+ assertThatEntriesAreEqual(cache.get("key"), entry);
+ }
+
+ @Test
+ public void testInvalidateFullExpire() {
+ Cache.Entry entry = randomData(32);
+ entry.softTtl = 8765432L;
+ entry.ttl = 9876543L;
+ cache.put("key", entry);
+
+ cache.invalidate("key", true);
+ entry.softTtl = 0; // expired
+ entry.ttl = 0; // expired
+ assertThatEntriesAreEqual(cache.get("key"), entry);
+ }
+
+ @Test
+ public void testTooLargeEntry() {
+ Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("oversize"));
+ cache.put("oversize", entry);
+
+ assertThat(cache.get("oversize"), is(nullValue()));
+ }
+
+ @Test
+ public void testMaxSizeEntry() {
+ Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("maxsize") - 1);
+ cache.put("maxsize", entry);
+
+ assertThatEntriesAreEqual(cache.get("maxsize"), entry);
+ }
+
+ @Test
+ public void testTrimAtThreshold() {
+ // Start with the largest possible entry.
+ Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("maxsize") - 1);
+ cache.put("maxsize", entry);
+
+ assertThatEntriesAreEqual(cache.get("maxsize"), entry);
+
+ // Now any new entry should cause the first one to be cleared.
+ entry = randomData(0);
+ cache.put("bit", entry);
+
+ assertThat(cache.get("goodsize"), is(nullValue()));
+ assertThatEntriesAreEqual(cache.get("bit"), entry);
+ }
+
+ @Test
+ public void testTrimWithMultipleEvictions_underHysteresisThreshold() {
+ Cache.Entry entry1 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry1") - 1);
+ cache.put("entry1", entry1);
+ Cache.Entry entry2 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry2") - 1);
+ cache.put("entry2", entry2);
+ Cache.Entry entry3 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry3") - 1);
+ cache.put("entry3", entry3);
+
+ assertThatEntriesAreEqual(cache.get("entry1"), entry1);
+ assertThatEntriesAreEqual(cache.get("entry2"), entry2);
+ assertThatEntriesAreEqual(cache.get("entry3"), entry3);
+
+ Cache.Entry entry =
+ randomData(
+ (int) (DiskBasedCache.HYSTERESIS_FACTOR * MAX_SIZE)
+ - getEntrySizeOnDisk("max"));
+ cache.put("max", entry);
+
+ assertThat(cache.get("entry1"), is(nullValue()));
+ assertThat(cache.get("entry2"), is(nullValue()));
+ assertThat(cache.get("entry3"), is(nullValue()));
+ assertThatEntriesAreEqual(cache.get("max"), entry);
+ }
+
+ @Test
+ public void testTrimWithMultipleEvictions_atHysteresisThreshold() {
+ Cache.Entry entry1 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry1") - 1);
+ cache.put("entry1", entry1);
+ Cache.Entry entry2 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry2") - 1);
+ cache.put("entry2", entry2);
+ Cache.Entry entry3 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry3") - 1);
+ cache.put("entry3", entry3);
+
+ assertThatEntriesAreEqual(cache.get("entry1"), entry1);
+ assertThatEntriesAreEqual(cache.get("entry2"), entry2);
+ assertThatEntriesAreEqual(cache.get("entry3"), entry3);
+
+ Cache.Entry entry =
+ randomData(
+ (int) (DiskBasedCache.HYSTERESIS_FACTOR * MAX_SIZE)
+ - getEntrySizeOnDisk("max")
+ + 1);
+ cache.put("max", entry);
+
+ assertThat(cache.get("entry1"), is(nullValue()));
+ assertThat(cache.get("entry2"), is(nullValue()));
+ assertThat(cache.get("entry3"), is(nullValue()));
+ assertThat(cache.get("max"), is(nullValue()));
+ }
+
+ @Test
+ public void testTrimWithPartialEvictions() {
+ Cache.Entry entry1 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry1") - 1);
+ cache.put("entry1", entry1);
+ Cache.Entry entry2 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry2") - 1);
+ cache.put("entry2", entry2);
+ Cache.Entry entry3 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry3") - 1);
+ cache.put("entry3", entry3);
+
+ assertThatEntriesAreEqual(cache.get("entry1"), entry1);
+ assertThatEntriesAreEqual(cache.get("entry2"), entry2);
+ assertThatEntriesAreEqual(cache.get("entry3"), entry3);
+
+ Cache.Entry entry4 = randomData((MAX_SIZE - getEntrySizeOnDisk("entry4") - 1) / 2);
+ cache.put("entry4", entry4);
+
+ assertThat(cache.get("entry1"), is(nullValue()));
+ assertThat(cache.get("entry2"), is(nullValue()));
+ assertThatEntriesAreEqual(cache.get("entry3"), entry3);
+ assertThatEntriesAreEqual(cache.get("entry4"), entry4);
+ }
+
+ @Test
+ public void testLargeEntryDoesntClearCache() {
+ // Writing a large entry to an empty cache should succeed
+ Cache.Entry largeEntry = randomData(MAX_SIZE - getEntrySizeOnDisk("largeEntry") - 1);
+ cache.put("largeEntry", largeEntry);
+
+ assertThatEntriesAreEqual(cache.get("largeEntry"), largeEntry);
+
+ // Reset and fill up ~half the cache.
+ cache.clear();
+ Cache.Entry entry = randomData(MAX_SIZE / 2 - getEntrySizeOnDisk("entry") - 1);
+ cache.put("entry", entry);
+
+ assertThatEntriesAreEqual(cache.get("entry"), entry);
+
+ // Writing the large entry should no-op, because otherwise the pruning algorithm would clear
+ // the whole cache, since the large entry is above the hysteresis threshold.
+ cache.put("largeEntry", largeEntry);
+
+ assertThat(cache.get("largeEntry"), is(nullValue()));
+ assertThatEntriesAreEqual(cache.get("entry"), entry);
+ }
+
+ @Test
+ @SuppressWarnings("TryFinallyCanBeTryWithResources")
+ public void testGetBadMagic() throws IOException {
+ // Cache something
+ Cache.Entry entry = randomData(1023);
+ cache.put("key", entry);
+ assertThatEntriesAreEqual(cache.get("key"), entry);
+
+ // Overwrite the magic header
+ File cacheFolder = temporaryFolder.getRoot();
+ File file = cacheFolder.listFiles()[0];
+ FileOutputStream fos = new FileOutputStream(file);
+ try {
+ DiskBasedCache.writeInt(fos, 0); // overwrite magic
+ } finally {
+ //noinspection ThrowFromFinallyBlock
+ fos.close();
+ }
+
+ assertThat(cache.get("key"), is(nullValue()));
+ assertThat(listCachedFiles(), is(emptyArray()));
+ }
+
+ @Test
+ @SuppressWarnings("TryFinallyCanBeTryWithResources")
+ public void testGetWrongKey() throws IOException {
+ // Cache something
+ Cache.Entry entry = randomData(1023);
+ cache.put("key", entry);
+ assertThatEntriesAreEqual(cache.get("key"), entry);
+
+ // Access the cached file
+ File cacheFolder = temporaryFolder.getRoot();
+ File file = cacheFolder.listFiles()[0];
+ FileOutputStream fos = new FileOutputStream(file);
+ try {
+ // Overwrite with a different key
+ CacheHeader wrongHeader = new CacheHeader("bad", entry);
+ wrongHeader.writeHeader(fos);
+ } finally {
+ //noinspection ThrowFromFinallyBlock
+ fos.close();
+ }
+
+ // key is gone, but file is still there
+ assertThat(cache.get("key"), is(nullValue()));
+ assertThat(listCachedFiles(), is(arrayWithSize(1)));
+
+ // Note: file is now a zombie because its key does not map to its name
+ }
+
+ @Test
+ public void testStreamToBytesNegativeLength() throws IOException {
+ byte[] data = new byte[1];
+ CountingInputStream cis =
+ new CountingInputStream(new ByteArrayInputStream(data), data.length);
+ exception.expect(IOException.class);
+ DiskBasedCache.streamToBytes(cis, -1);
+ }
+
+ @Test
+ public void testStreamToBytesExcessiveLength() throws IOException {
+ byte[] data = new byte[1];
+ CountingInputStream cis =
+ new CountingInputStream(new ByteArrayInputStream(data), data.length);
+ exception.expect(IOException.class);
+ DiskBasedCache.streamToBytes(cis, 2);
+ }
+
+ @Test
+ public void testStreamToBytesOverflow() throws IOException {
+ byte[] data = new byte[0];
+ CountingInputStream cis =
+ new CountingInputStream(new ByteArrayInputStream(data), 0x100000000L);
+ exception.expect(IOException.class);
+ DiskBasedCache.streamToBytes(cis, 0x100000000L); // int value is 0
+ }
+
+ @Test
+ public void testReadHeaderListWithNegativeSize() throws IOException {
+ // If a cached header list is corrupted and begins with a negative size,
+ // verify that readHeaderList will throw an IOException.
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DiskBasedCache.writeInt(baos, -1); // negative size
+ CountingInputStream cis =
+ new CountingInputStream(
+ new ByteArrayInputStream(baos.toByteArray()), Integer.MAX_VALUE);
+ // Expect IOException due to negative size
+ exception.expect(IOException.class);
+ DiskBasedCache.readHeaderList(cis);
+ }
+
+ @Test
+ public void testReadHeaderListWithGinormousSize() throws IOException {
+ // If a cached header list is corrupted and begins with 2GB size, verify
+ // that readHeaderList will throw EOFException rather than OutOfMemoryError.
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DiskBasedCache.writeInt(baos, Integer.MAX_VALUE); // 2GB size
+ CountingInputStream cis =
+ new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size());
+ // Expect EOFException when end of stream is reached
+ exception.expect(EOFException.class);
+ DiskBasedCache.readHeaderList(cis);
+ }
+
+ @Test
+ public void testFileIsDeletedWhenWriteHeaderFails() throws IOException {
+ // Create DataOutputStream that throws IOException
+ OutputStream mockedOutputStream = spy(OutputStream.class);
+ doThrow(IOException.class).when(mockedOutputStream).write(anyInt());
+
+ // Create read-only copy that fails to write anything
+ DiskBasedCache readonly = spy((DiskBasedCache) cache);
+ doReturn(mockedOutputStream).when(readonly).createOutputStream(any(File.class));
+
+ // Attempt to write
+ readonly.put("key", randomData(1111));
+
+ // write is called at least once because each linked stream flushes when closed
+ verify(mockedOutputStream, atLeastOnce()).write(anyInt());
+ assertThat(readonly.get("key"), is(nullValue()));
+ assertThat(listCachedFiles(), is(emptyArray()));
+
+ // Note: original cache will try (without success) to read from file
+ assertThat(cache.get("key"), is(nullValue()));
+ }
+
+ @Test
+ public void testIOExceptionInInitialize() throws IOException {
+ // Cache a few kilobytes
+ cache.put("kilobyte", randomData(1024));
+ cache.put("kilobyte2", randomData(1024));
+ cache.put("kilobyte3", randomData(1024));
+
+ // Create DataInputStream that throws IOException
+ InputStream mockedInputStream = spy(InputStream.class);
+ //noinspection ResultOfMethodCallIgnored
+ doThrow(IOException.class).when(mockedInputStream).read();
+
+ // Create broken cache that fails to read anything
+ DiskBasedCache broken = spy(new DiskBasedCache(temporaryFolder.getRoot()));
+ doReturn(mockedInputStream).when(broken).createInputStream(any(File.class));
+
+ // Attempt to initialize
+ broken.initialize();
+
+ // Everything is gone
+ assertThat(broken.get("kilobyte"), is(nullValue()));
+ assertThat(broken.get("kilobyte2"), is(nullValue()));
+ assertThat(broken.get("kilobyte3"), is(nullValue()));
+ assertThat(listCachedFiles(), is(emptyArray()));
+
+ // Verify that original cache can cope with missing files
+ assertThat(cache.get("kilobyte"), is(nullValue()));
+ assertThat(cache.get("kilobyte2"), is(nullValue()));
+ assertThat(cache.get("kilobyte3"), is(nullValue()));
+ }
+
+ @Test
+ public void testManyResponseHeaders() {
+ Cache.Entry entry = new Cache.Entry();
+ entry.data = new byte[0];
+ entry.responseHeaders = new HashMap<>();
+ for (int i = 0; i < 0xFFFF; i++) {
+ entry.responseHeaders.put(Integer.toString(i), "");
+ }
+ cache.put("key", entry);
+ }
+
+ @Test
+ @SuppressWarnings("TryFinallyCanBeTryWithResources")
+ public void testCountingInputStreamByteCount() throws IOException {
+ // Write some bytes
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ //noinspection ThrowFromFinallyBlock
+ try {
+ DiskBasedCache.writeInt(out, 1);
+ DiskBasedCache.writeLong(out, -1L);
+ DiskBasedCache.writeString(out, "hamburger");
+ } finally {
+ //noinspection ThrowFromFinallyBlock
+ out.close();
+ }
+ long bytesWritten = out.size();
+
+ // Read the bytes and compare the counts
+ CountingInputStream cis =
+ new CountingInputStream(new ByteArrayInputStream(out.toByteArray()), bytesWritten);
+ try {
+ assertThat(cis.bytesRemaining(), is(bytesWritten));
+ assertThat(cis.bytesRead(), is(0L));
+ assertThat(DiskBasedCache.readInt(cis), is(1));
+ assertThat(DiskBasedCache.readLong(cis), is(-1L));
+ assertThat(DiskBasedCache.readString(cis), is("hamburger"));
+ assertThat(cis.bytesRead(), is(bytesWritten));
+ assertThat(cis.bytesRemaining(), is(0L));
+ } finally {
+ //noinspection ThrowFromFinallyBlock
+ cis.close();
+ }
+ }
+
+ /* Serialization tests */
+
+ @Test
+ public void testEmptyReadThrowsEOF() throws IOException {
+ ByteArrayInputStream empty = new ByteArrayInputStream(new byte[] {});
+ exception.expect(EOFException.class);
+ DiskBasedCache.readInt(empty);
+ }
+
+ @Test
+ public void serializeInt() throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DiskBasedCache.writeInt(baos, 0);
+ DiskBasedCache.writeInt(baos, 19791214);
+ DiskBasedCache.writeInt(baos, -20050711);
+ DiskBasedCache.writeInt(baos, Integer.MIN_VALUE);
+ DiskBasedCache.writeInt(baos, Integer.MAX_VALUE);
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ assertEquals(DiskBasedCache.readInt(bais), 0);
+ assertEquals(DiskBasedCache.readInt(bais), 19791214);
+ assertEquals(DiskBasedCache.readInt(bais), -20050711);
+ assertEquals(DiskBasedCache.readInt(bais), Integer.MIN_VALUE);
+ assertEquals(DiskBasedCache.readInt(bais), Integer.MAX_VALUE);
+ }
+
+ @Test
+ public void serializeLong() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DiskBasedCache.writeLong(baos, 0);
+ DiskBasedCache.writeLong(baos, 31337);
+ DiskBasedCache.writeLong(baos, -4160);
+ DiskBasedCache.writeLong(baos, 4295032832L);
+ DiskBasedCache.writeLong(baos, -4314824046L);
+ DiskBasedCache.writeLong(baos, Long.MIN_VALUE);
+ DiskBasedCache.writeLong(baos, Long.MAX_VALUE);
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ assertEquals(DiskBasedCache.readLong(bais), 0);
+ assertEquals(DiskBasedCache.readLong(bais), 31337);
+ assertEquals(DiskBasedCache.readLong(bais), -4160);
+ assertEquals(DiskBasedCache.readLong(bais), 4295032832L);
+ assertEquals(DiskBasedCache.readLong(bais), -4314824046L);
+ assertEquals(DiskBasedCache.readLong(bais), Long.MIN_VALUE);
+ assertEquals(DiskBasedCache.readLong(bais), Long.MAX_VALUE);
+ }
+
+ @Test
+ public void serializeString() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DiskBasedCache.writeString(baos, "");
+ DiskBasedCache.writeString(baos, "This is a string.");
+ DiskBasedCache.writeString(baos, "ファイカス");
+ CountingInputStream cis =
+ new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size());
+ assertEquals(DiskBasedCache.readString(cis), "");
+ assertEquals(DiskBasedCache.readString(cis), "This is a string.");
+ assertEquals(DiskBasedCache.readString(cis), "ファイカス");
+ }
+
+ @Test
+ public void serializeHeaders() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ List<Header> empty = new ArrayList<>();
+ DiskBasedCache.writeHeaderList(empty, baos);
+ DiskBasedCache.writeHeaderList(null, baos);
+ List<Header> twoThings = new ArrayList<>();
+ twoThings.add(new Header("first", "thing"));
+ twoThings.add(new Header("second", "item"));
+ DiskBasedCache.writeHeaderList(twoThings, baos);
+ List<Header> emptyKey = new ArrayList<>();
+ emptyKey.add(new Header("", "value"));
+ DiskBasedCache.writeHeaderList(emptyKey, baos);
+ List<Header> emptyValue = new ArrayList<>();
+ emptyValue.add(new Header("key", ""));
+ DiskBasedCache.writeHeaderList(emptyValue, baos);
+ List<Header> sameKeys = new ArrayList<>();
+ sameKeys.add(new Header("key", "value"));
+ sameKeys.add(new Header("key", "value2"));
+ DiskBasedCache.writeHeaderList(sameKeys, baos);
+ CountingInputStream cis =
+ new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size());
+ assertEquals(DiskBasedCache.readHeaderList(cis), empty);
+ assertEquals(DiskBasedCache.readHeaderList(cis), empty); // null reads back empty
+ assertEquals(DiskBasedCache.readHeaderList(cis), twoThings);
+ assertEquals(DiskBasedCache.readHeaderList(cis), emptyKey);
+ assertEquals(DiskBasedCache.readHeaderList(cis), emptyValue);
+ assertEquals(DiskBasedCache.readHeaderList(cis), sameKeys);
+ }
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch-all test to find API-breaking changes.
+ assertNotNull(DiskBasedCache.class.getConstructor(File.class, int.class));
+ assertNotNull(
+ DiskBasedCache.class.getConstructor(DiskBasedCache.FileSupplier.class, int.class));
+ assertNotNull(DiskBasedCache.class.getConstructor(File.class));
+ assertNotNull(DiskBasedCache.class.getConstructor(DiskBasedCache.FileSupplier.class));
+
+ assertNotNull(DiskBasedCache.class.getMethod("getFileForKey", String.class));
+ }
+
+ @Test
+ public void initializeIfRootDirectoryDeleted() {
+ temporaryFolder.delete();
+
+ Cache.Entry entry = randomData(101);
+ cache.put("key1", entry);
+
+ assertThat(cache.get("key1"), is(nullValue()));
+
+ // confirm that we can now store entries
+ cache.put("key2", entry);
+ assertThatEntriesAreEqual(cache.get("key2"), entry);
+ }
+
+ /* Test helpers */
+
+ private void assertThatEntriesAreEqual(Cache.Entry actual, Cache.Entry expected) {
+ 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)));
+ }
+
+ private 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;
+ }
+
+ private File[] listCachedFiles() {
+ return temporaryFolder.getRoot().listFiles();
+ }
+
+ private 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();
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java b/core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java
new file mode 100644
index 0000000..2a451dc
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.volley.Request.Method;
+import com.android.volley.mock.TestRequest;
+import com.android.volley.toolbox.HttpClientStack.HttpPatch;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpTrace;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class HttpClientStackTest {
+
+ @Test
+ public void createDeprecatedGetRequest() throws Exception {
+ TestRequest.DeprecatedGet request = new TestRequest.DeprecatedGet();
+ assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpGet);
+ }
+
+ @Test
+ public void createDeprecatedPostRequest() throws Exception {
+ TestRequest.DeprecatedPost request = new TestRequest.DeprecatedPost();
+ assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpPost);
+ }
+
+ @Test
+ public void createGetRequest() throws Exception {
+ TestRequest.Get request = new TestRequest.Get();
+ assertEquals(request.getMethod(), Method.GET);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpGet);
+ }
+
+ @Test
+ public void createPostRequest() throws Exception {
+ TestRequest.Post request = new TestRequest.Post();
+ assertEquals(request.getMethod(), Method.POST);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpPost);
+ }
+
+ @Test
+ public void createPostRequestWithBody() throws Exception {
+ TestRequest.PostWithBody request = new TestRequest.PostWithBody();
+ assertEquals(request.getMethod(), Method.POST);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpPost);
+ }
+
+ @Test
+ public void createPutRequest() throws Exception {
+ TestRequest.Put request = new TestRequest.Put();
+ assertEquals(request.getMethod(), Method.PUT);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpPut);
+ }
+
+ @Test
+ public void createPutRequestWithBody() throws Exception {
+ TestRequest.PutWithBody request = new TestRequest.PutWithBody();
+ assertEquals(request.getMethod(), Method.PUT);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpPut);
+ }
+
+ @Test
+ public void createDeleteRequest() throws Exception {
+ TestRequest.Delete request = new TestRequest.Delete();
+ assertEquals(request.getMethod(), Method.DELETE);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpDelete);
+ }
+
+ @Test
+ public void createHeadRequest() throws Exception {
+ TestRequest.Head request = new TestRequest.Head();
+ assertEquals(request.getMethod(), Method.HEAD);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpHead);
+ }
+
+ @Test
+ public void createOptionsRequest() throws Exception {
+ TestRequest.Options request = new TestRequest.Options();
+ assertEquals(request.getMethod(), Method.OPTIONS);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpOptions);
+ }
+
+ @Test
+ public void createTraceRequest() throws Exception {
+ TestRequest.Trace request = new TestRequest.Trace();
+ assertEquals(request.getMethod(), Method.TRACE);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpTrace);
+ }
+
+ @Test
+ public void createPatchRequest() throws Exception {
+ TestRequest.Patch request = new TestRequest.Patch();
+ assertEquals(request.getMethod(), Method.PATCH);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpPatch);
+ }
+
+ @Test
+ public void createPatchRequestWithBody() throws Exception {
+ TestRequest.PatchWithBody request = new TestRequest.PatchWithBody();
+ assertEquals(request.getMethod(), Method.PATCH);
+
+ HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null);
+ assertTrue(httpRequest instanceof HttpPatch);
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
new file mode 100644
index 0000000..7780c3e
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.volley.Cache;
+import com.android.volley.Header;
+import com.android.volley.NetworkResponse;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class HttpHeaderParserTest {
+
+ private static long ONE_MINUTE_MILLIS = 1000L * 60;
+ private static long ONE_HOUR_MILLIS = 1000L * 60 * 60;
+ private static long ONE_DAY_MILLIS = ONE_HOUR_MILLIS * 24;
+ private static long ONE_WEEK_MILLIS = ONE_DAY_MILLIS * 7;
+
+ private NetworkResponse response;
+ private Map<String, String> headers;
+
+ @Before
+ public void setUp() throws Exception {
+ headers = new HashMap<String, String>();
+ response = new NetworkResponse(0, null, headers, false);
+ }
+
+ @Test
+ public void parseCacheHeaders_noHeaders() {
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+
+ assertNotNull(entry);
+ assertNull(entry.etag);
+ assertEquals(0, entry.serverDate);
+ assertEquals(0, entry.lastModified);
+ assertEquals(0, entry.ttl);
+ assertEquals(0, entry.softTtl);
+ }
+
+ @Test
+ public void parseCacheHeaders_nullHeaders() {
+ response = new NetworkResponse(0, null, null, false);
+ assertNull(HttpHeaderParser.parseCacheHeaders(response));
+ }
+
+ @Test
+ public void parseCacheHeaders_headersSet() {
+ headers.put("MyCustomHeader", "42");
+
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+
+ assertNotNull(entry);
+ assertNotNull(entry.responseHeaders);
+ assertEquals(1, entry.responseHeaders.size());
+ assertEquals("42", entry.responseHeaders.get("MyCustomHeader"));
+ }
+
+ @Test
+ public void parseCacheHeaders_etag() {
+ headers.put("ETag", "Yow!");
+
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+
+ assertNotNull(entry);
+ assertEquals("Yow!", entry.etag);
+ }
+
+ @Test
+ public void parseCacheHeaders_normalExpire() {
+ long now = System.currentTimeMillis();
+ headers.put("Date", rfc1123Date(now));
+ headers.put("Last-Modified", rfc1123Date(now - ONE_DAY_MILLIS));
+ headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS));
+
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+
+ assertNotNull(entry);
+ assertNull(entry.etag);
+ assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS);
+ assertEqualsWithin(entry.lastModified, (now - ONE_DAY_MILLIS), ONE_MINUTE_MILLIS);
+ assertTrue(entry.softTtl >= (now + ONE_HOUR_MILLIS));
+ assertTrue(entry.ttl == entry.softTtl);
+ }
+
+ @Test
+ public void parseCacheHeaders_expiresInPast() {
+ long now = System.currentTimeMillis();
+ headers.put("Date", rfc1123Date(now));
+ headers.put("Expires", rfc1123Date(now - ONE_HOUR_MILLIS));
+
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+
+ assertNotNull(entry);
+ assertNull(entry.etag);
+ assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS);
+ assertEquals(0, entry.ttl);
+ assertEquals(0, entry.softTtl);
+ }
+
+ @Test
+ public void parseCacheHeaders_serverRelative() {
+
+ long now = System.currentTimeMillis();
+ // Set "current" date as one hour in the future
+ headers.put("Date", rfc1123Date(now + ONE_HOUR_MILLIS));
+ // TTL four hours in the future, so should be three hours from now
+ headers.put("Expires", rfc1123Date(now + 4 * ONE_HOUR_MILLIS));
+
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+
+ assertEqualsWithin(now + 3 * ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
+ assertEquals(entry.softTtl, entry.ttl);
+ }
+
+ @Test
+ public void parseCacheHeaders_cacheControlOverridesExpires() {
+ long now = System.currentTimeMillis();
+ headers.put("Date", rfc1123Date(now));
+ headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS));
+ headers.put("Cache-Control", "public, max-age=86400");
+
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+
+ assertNotNull(entry);
+ assertNull(entry.etag);
+ assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
+ assertEquals(entry.softTtl, entry.ttl);
+ }
+
+ @Test
+ public void testParseCacheHeaders_staleWhileRevalidate() {
+ long now = System.currentTimeMillis();
+ headers.put("Date", rfc1123Date(now));
+ headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS));
+
+ // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day
+ // - stale-while-revalidate (entry.ttl) indicates that the asset may
+ // continue to be served stale for up to additional 7 days
+ headers.put("Cache-Control", "max-age=86400, stale-while-revalidate=604800");
+
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+
+ assertNotNull(entry);
+ assertNull(entry.etag);
+ assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS);
+ assertEqualsWithin(now + ONE_DAY_MILLIS + ONE_WEEK_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
+ }
+
+ @Test
+ public void parseCacheHeaders_cacheControlNoCache() {
+ long now = System.currentTimeMillis();
+ headers.put("Date", rfc1123Date(now));
+ headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS));
+ headers.put("Cache-Control", "no-cache");
+
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+
+ assertNull(entry);
+ }
+
+ @Test
+ public void parseCacheHeaders_cacheControlMustRevalidateNoMaxAge() {
+ long now = System.currentTimeMillis();
+ headers.put("Date", rfc1123Date(now));
+ headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS));
+ headers.put("Cache-Control", "must-revalidate");
+
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+ assertNotNull(entry);
+ assertNull(entry.etag);
+ assertEqualsWithin(now, entry.ttl, ONE_MINUTE_MILLIS);
+ assertEquals(entry.softTtl, entry.ttl);
+ }
+
+ @Test
+ public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAge() {
+ long now = System.currentTimeMillis();
+ headers.put("Date", rfc1123Date(now));
+ headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS));
+ headers.put("Cache-Control", "must-revalidate, max-age=3600");
+
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+ assertNotNull(entry);
+ assertNull(entry.etag);
+ assertEqualsWithin(now + ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
+ assertEquals(entry.softTtl, entry.ttl);
+ }
+
+ @Test
+ public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAgeAndStale() {
+ long now = System.currentTimeMillis();
+ headers.put("Date", rfc1123Date(now));
+ headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS));
+
+ // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day
+ // - stale-while-revalidate (entry.ttl) indicates that the asset may
+ // continue to be served stale for up to additional 7 days, but this is
+ // ignored in this case because of the must-revalidate header.
+ headers.put(
+ "Cache-Control", "must-revalidate, max-age=86400, stale-while-revalidate=604800");
+
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+ assertNotNull(entry);
+ assertNull(entry.etag);
+ assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS);
+ assertEquals(entry.softTtl, entry.ttl);
+ }
+
+ private void assertEqualsWithin(long expected, long value, long fudgeFactor) {
+ long diff = Math.abs(expected - value);
+ assertTrue(diff < fudgeFactor);
+ }
+
+ private static String rfc1123Date(long millis) {
+ DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
+ return df.format(new Date(millis));
+ }
+
+ // --------------------------
+
+ @Test
+ public void parseCharset() {
+ // Like the ones we usually see
+ headers.put("Content-Type", "text/plain; charset=utf-8");
+ assertEquals("utf-8", HttpHeaderParser.parseCharset(headers));
+
+ // Charset specified, ignore default charset
+ headers.put("Content-Type", "text/plain; charset=utf-8");
+ assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "ISO-8859-1"));
+
+ // Extra whitespace
+ headers.put("Content-Type", "text/plain; charset=utf-8 ");
+ assertEquals("utf-8", HttpHeaderParser.parseCharset(headers));
+
+ // Extra parameters
+ headers.put("Content-Type", "text/plain; charset=utf-8; frozzle=bar");
+ assertEquals("utf-8", HttpHeaderParser.parseCharset(headers));
+
+ // No Content-Type header
+ headers.clear();
+ assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers));
+
+ // No Content-Type header, use default charset
+ headers.clear();
+ assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "utf-8"));
+
+ // Empty value
+ headers.put("Content-Type", "text/plain; charset=");
+ assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers));
+
+ // None specified
+ headers.put("Content-Type", "text/plain");
+ assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers));
+
+ // None charset specified, use default charset
+ headers.put("Content-Type", "application/json");
+ assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "utf-8"));
+
+ // None specified, extra semicolon
+ headers.put("Content-Type", "text/plain;");
+ assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers));
+
+ // No headers, use default charset
+ assertEquals("utf-8", HttpHeaderParser.parseCharset(null, "utf-8"));
+ }
+
+ @Test
+ public void parseCaseInsensitive() {
+ long now = System.currentTimeMillis();
+
+ List<Header> headers = new ArrayList<>();
+ headers.add(new Header("eTAG", "Yow!"));
+ headers.add(new Header("DATE", rfc1123Date(now)));
+ headers.add(new Header("expires", rfc1123Date(now + ONE_HOUR_MILLIS)));
+ headers.add(new Header("cache-control", "public, max-age=86400"));
+ headers.add(new Header("content-type", "text/plain"));
+
+ NetworkResponse response = new NetworkResponse(0, null, false, 0, headers);
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+
+ assertNotNull(entry);
+ assertEquals("Yow!", entry.etag);
+ assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
+ assertEquals(entry.softTtl, entry.ttl);
+ assertEquals(
+ "ISO-8859-1", HttpHeaderParser.parseCharset(HttpHeaderParser.toHeaderMap(headers)));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java b/core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java
new file mode 100644
index 0000000..6794af8
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java
@@ -0,0 +1,192 @@
+package com.android.volley.toolbox;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import com.android.volley.Request;
+import com.android.volley.RetryPolicy;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.http.Header;
+import org.apache.http.HttpRequest;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests to validate that HttpStack implementations conform with expected behavior. */
+@RunWith(RobolectricTestRunner.class)
+public class HttpStackConformanceTest {
+ @Mock private RetryPolicy mMockRetryPolicy;
+ @Mock private Request mMockRequest;
+
+ @Mock private HttpURLConnection mMockConnection;
+ @Mock private OutputStream mMockOutputStream;
+ @Spy private HurlStack mHurlStack = new HurlStack();
+
+ @Mock private HttpClient mMockHttpClient;
+ private HttpClientStack mHttpClientStack;
+
+ private final TestCase[] mTestCases =
+ new TestCase[] {
+ // TestCase for HurlStack.
+ new TestCase() {
+ @Override
+ public HttpStack getStack() {
+ return mHurlStack;
+ }
+
+ @Override
+ public void setOutputHeaderMap(final Map<String, String> outputHeaderMap) {
+ doAnswer(
+ new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ outputHeaderMap.put(
+ invocation.<String>getArgument(0),
+ invocation.<String>getArgument(1));
+ return null;
+ }
+ })
+ .when(mMockConnection)
+ .setRequestProperty(anyString(), anyString());
+ doAnswer(
+ new Answer<Map<String, List<String>>>() {
+ @Override
+ public Map<String, List<String>> answer(
+ InvocationOnMock invocation) {
+ Map<String, List<String>> result = new HashMap<>();
+ for (Map.Entry<String, String> entry :
+ outputHeaderMap.entrySet()) {
+ result.put(
+ entry.getKey(),
+ Collections.singletonList(
+ entry.getValue()));
+ }
+ return result;
+ }
+ })
+ .when(mMockConnection)
+ .getRequestProperties();
+ }
+ },
+
+ // TestCase for HttpClientStack.
+ new TestCase() {
+ @Override
+ public HttpStack getStack() {
+ return mHttpClientStack;
+ }
+
+ @Override
+ public void setOutputHeaderMap(final Map<String, String> outputHeaderMap) {
+ try {
+ doAnswer(
+ new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation)
+ throws Throwable {
+ HttpRequest request = invocation.getArgument(0);
+ for (Header header : request.getAllHeaders()) {
+ if (outputHeaderMap.containsKey(
+ header.getName())) {
+ fail(
+ "Multiple values for header "
+ + header.getName());
+ }
+ outputHeaderMap.put(
+ header.getName(),
+ header.getValue());
+ }
+ return null;
+ }
+ })
+ .when(mMockHttpClient)
+ .execute(any(HttpUriRequest.class));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ };
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mHttpClientStack = spy(new HttpClientStack(mMockHttpClient));
+
+ doReturn(mMockConnection).when(mHurlStack).createConnection(any(URL.class));
+ doReturn(mMockOutputStream).when(mMockConnection).getOutputStream();
+ when(mMockRequest.getUrl()).thenReturn("http://127.0.0.1");
+ when(mMockRequest.getRetryPolicy()).thenReturn(mMockRetryPolicy);
+ }
+
+ @Test
+ public void headerPrecedence() throws Exception {
+ Map<String, String> additionalHeaders = new HashMap<>();
+ additionalHeaders.put("A", "AddlA");
+ additionalHeaders.put("B", "AddlB");
+
+ Map<String, String> requestHeaders = new HashMap<>();
+ requestHeaders.put("A", "RequestA");
+ requestHeaders.put("C", "RequestC");
+ when(mMockRequest.getHeaders()).thenReturn(requestHeaders);
+
+ when(mMockRequest.getMethod()).thenReturn(Request.Method.POST);
+ when(mMockRequest.getBody()).thenReturn(new byte[0]);
+ when(mMockRequest.getBodyContentType()).thenReturn("BodyContentType");
+
+ for (TestCase testCase : mTestCases) {
+ // Test once without a Content-Type header in getHeaders().
+ Map<String, String> combinedHeaders = new HashMap<>();
+ testCase.setOutputHeaderMap(combinedHeaders);
+
+ testCase.getStack().performRequest(mMockRequest, additionalHeaders);
+
+ Map<String, String> expectedHeaders = new HashMap<>();
+ expectedHeaders.put("A", "RequestA");
+ expectedHeaders.put("B", "AddlB");
+ expectedHeaders.put("C", "RequestC");
+ expectedHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "BodyContentType");
+
+ assertEquals(expectedHeaders, combinedHeaders);
+
+ // Reset and test again with a Content-Type header in getHeaders().
+ combinedHeaders.clear();
+
+ requestHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "RequestContentType");
+ expectedHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "RequestContentType");
+
+ testCase.getStack().performRequest(mMockRequest, additionalHeaders);
+ assertEquals(expectedHeaders, combinedHeaders);
+
+ // Clear the Content-Type header for the next TestCase.
+ requestHeaders.remove(HttpHeaderParser.HEADER_CONTENT_TYPE);
+ }
+ }
+
+ private interface TestCase {
+ HttpStack getStack();
+
+ void setOutputHeaderMap(Map<String, String> outputHeaderMap);
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/HurlStackTest.java b/core/src/test/java/com/android/volley/toolbox/HurlStackTest.java
new file mode 100644
index 0000000..7508244
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/HurlStackTest.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+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.Request;
+import com.android.volley.Request.Method;
+import com.android.volley.mock.TestRequest;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+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 org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class HurlStackTest {
+
+ @Mock private HttpURLConnection mMockConnection;
+ private HurlStack mHurlStack;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ when(mMockConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream());
+
+ mHurlStack =
+ new HurlStack() {
+ @Override
+ protected HttpURLConnection createConnection(URL url) {
+ return mMockConnection;
+ }
+
+ @Override
+ protected InputStream createInputStream(
+ Request<?> request, HttpURLConnection connection) {
+ return new MonitoringInputStream(
+ super.createInputStream(request, connection));
+ }
+
+ @Override
+ protected OutputStream createOutputStream(
+ Request<?> request, HttpURLConnection connection, int length)
+ throws IOException {
+ if (request instanceof MonitoredRequest) {
+ return new MonitoringOutputStream(
+ super.createOutputStream(request, connection, length),
+ (MonitoredRequest) request,
+ length);
+ }
+ return super.createOutputStream(request, connection, length);
+ }
+ };
+ }
+
+ @Test
+ public void connectionForDeprecatedGetRequest() throws Exception {
+ TestRequest.DeprecatedGet request = new TestRequest.DeprecatedGet();
+ assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection, never()).setRequestMethod(anyString());
+ verify(mMockConnection, never()).setDoOutput(true);
+ }
+
+ @Test
+ public void connectionForDeprecatedPostRequest() throws Exception {
+ TestRequest.DeprecatedPost request = new TestRequest.DeprecatedPost();
+ assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection).setRequestMethod("POST");
+ verify(mMockConnection).setDoOutput(true);
+ }
+
+ @Test
+ public void connectionForGetRequest() throws Exception {
+ TestRequest.Get request = new TestRequest.Get();
+ assertEquals(request.getMethod(), Method.GET);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection).setRequestMethod("GET");
+ verify(mMockConnection, never()).setDoOutput(true);
+ }
+
+ @Test
+ public void connectionForPostRequest() throws Exception {
+ TestRequest.Post request = new TestRequest.Post();
+ assertEquals(request.getMethod(), Method.POST);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection).setRequestMethod("POST");
+ verify(mMockConnection, never()).setDoOutput(true);
+ }
+
+ @Test
+ public void connectionForPostWithBodyRequest() throws Exception {
+ TestRequest.PostWithBody request = new TestRequest.PostWithBody();
+ assertEquals(request.getMethod(), Method.POST);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection).setRequestMethod("POST");
+ verify(mMockConnection).setDoOutput(true);
+ }
+
+ @Test
+ public void connectionForPutRequest() throws Exception {
+ TestRequest.Put request = new TestRequest.Put();
+ assertEquals(request.getMethod(), Method.PUT);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection).setRequestMethod("PUT");
+ verify(mMockConnection, never()).setDoOutput(true);
+ }
+
+ @Test
+ public void connectionForPutWithBodyRequest() throws Exception {
+ TestRequest.PutWithBody request = new TestRequest.PutWithBody();
+ assertEquals(request.getMethod(), Method.PUT);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection).setRequestMethod("PUT");
+ verify(mMockConnection).setDoOutput(true);
+ }
+
+ @Test
+ public void connectionForDeleteRequest() throws Exception {
+ TestRequest.Delete request = new TestRequest.Delete();
+ assertEquals(request.getMethod(), Method.DELETE);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection).setRequestMethod("DELETE");
+ verify(mMockConnection, never()).setDoOutput(true);
+ }
+
+ @Test
+ public void connectionForHeadRequest() throws Exception {
+ TestRequest.Head request = new TestRequest.Head();
+ assertEquals(request.getMethod(), Method.HEAD);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection).setRequestMethod("HEAD");
+ verify(mMockConnection, never()).setDoOutput(true);
+ }
+
+ @Test
+ public void connectionForOptionsRequest() throws Exception {
+ TestRequest.Options request = new TestRequest.Options();
+ assertEquals(request.getMethod(), Method.OPTIONS);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection).setRequestMethod("OPTIONS");
+ verify(mMockConnection, never()).setDoOutput(true);
+ }
+
+ @Test
+ public void connectionForTraceRequest() throws Exception {
+ TestRequest.Trace request = new TestRequest.Trace();
+ assertEquals(request.getMethod(), Method.TRACE);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection).setRequestMethod("TRACE");
+ verify(mMockConnection, never()).setDoOutput(true);
+ }
+
+ @Test
+ public void connectionForPatchRequest() throws Exception {
+ TestRequest.Patch request = new TestRequest.Patch();
+ assertEquals(request.getMethod(), Method.PATCH);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection).setRequestMethod("PATCH");
+ verify(mMockConnection, never()).setDoOutput(true);
+ }
+
+ @Test
+ public void connectionForPatchWithBodyRequest() throws Exception {
+ TestRequest.PatchWithBody request = new TestRequest.PatchWithBody();
+ assertEquals(request.getMethod(), Method.PATCH);
+
+ mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
+ verify(mMockConnection).setRequestMethod("PATCH");
+ verify(mMockConnection).setDoOutput(true);
+ }
+
+ @Test
+ public void executeRequestClosesConnection_connectionError() throws Exception {
+ when(mMockConnection.getResponseCode()).thenThrow(new SocketTimeoutException());
+ try {
+ mHurlStack.executeRequest(
+ new TestRequest.Get(), Collections.<String, String>emptyMap());
+ fail("Should have thrown exception");
+ } catch (IOException e) {
+ verify(mMockConnection).disconnect();
+ }
+ }
+
+ @Test
+ public void executeRequestClosesConnection_invalidResponseCode() throws Exception {
+ when(mMockConnection.getResponseCode()).thenReturn(-1);
+ try {
+ mHurlStack.executeRequest(
+ new TestRequest.Get(), Collections.<String, String>emptyMap());
+ fail("Should have thrown exception");
+ } catch (IOException e) {
+ verify(mMockConnection).disconnect();
+ }
+ }
+
+ @Test
+ public void executeRequestClosesConnection_noResponseBody() throws Exception {
+ when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NO_CONTENT);
+ mHurlStack.executeRequest(new TestRequest.Get(), Collections.<String, String>emptyMap());
+ verify(mMockConnection).disconnect();
+ }
+
+ @Test
+ public void executeRequestClosesConnection_hasResponseBody() throws Exception {
+ when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
+ when(mMockConnection.getInputStream())
+ .thenReturn(new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8)));
+ HttpResponse response =
+ mHurlStack.executeRequest(
+ new TestRequest.Get(), Collections.<String, String>emptyMap());
+ // Shouldn't be disconnected until the stream is consumed.
+ verify(mMockConnection, never()).disconnect();
+ response.getContent().close();
+ verify(mMockConnection).disconnect();
+ }
+
+ @Test
+ public void convertHeaders() {
+ Map<String, List<String>> headers = new HashMap<>();
+ headers.put(null, Collections.singletonList("Ignored"));
+ headers.put("HeaderA", Collections.singletonList("ValueA"));
+ List<String> values = new ArrayList<>();
+ values.add("ValueB_1");
+ values.add("ValueB_2");
+ headers.put("HeaderB", values);
+ List<Header> result = HurlStack.convertHeaders(headers);
+ List<Header> expected = new ArrayList<>();
+ expected.add(new Header("HeaderA", "ValueA"));
+ expected.add(new Header("HeaderB", "ValueB_1"));
+ expected.add(new Header("HeaderB", "ValueB_2"));
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void interceptResponseStream() throws Exception {
+ when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
+ when(mMockConnection.getInputStream())
+ .thenReturn(new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8)));
+ HttpResponse response =
+ mHurlStack.executeRequest(
+ new TestRequest.Get(), Collections.<String, String>emptyMap());
+ assertTrue(response.getContent() instanceof MonitoringInputStream);
+ }
+
+ @Test
+ public void interceptRequestStream() throws Exception {
+ MonitoredRequest request = new MonitoredRequest();
+ mHurlStack.executeRequest(request, Collections.<String, String>emptyMap());
+ assertTrue(request.totalRequestBytes > 0);
+ assertEquals(request.totalRequestBytes, request.requestBytesRead);
+ }
+
+ private static class MonitoringInputStream extends FilterInputStream {
+ private MonitoringInputStream(InputStream in) {
+ super(in);
+ }
+ }
+
+ private static class MonitoringOutputStream extends FilterOutputStream {
+ private MonitoredRequest request;
+
+ private MonitoringOutputStream(OutputStream out, MonitoredRequest request, int length) {
+ super(out);
+ this.request = request;
+ this.request.totalRequestBytes = length;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ this.request.requestBytesRead++;
+ out.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ this.request.requestBytesRead += len;
+ out.write(b, off, len);
+ }
+ }
+
+ private static class MonitoredRequest extends TestRequest.PostWithBody {
+ int requestBytesRead = 0;
+ int totalRequestBytes = 0;
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java b/core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java
new file mode 100644
index 0000000..59a0b1b
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2015 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.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.widget.ImageView;
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ImageLoaderTest {
+ private RequestQueue mRequestQueue;
+ private ImageLoader.ImageCache mImageCache;
+ private ImageLoader mImageLoader;
+
+ @Before
+ public void setUp() {
+ mRequestQueue = mock(RequestQueue.class);
+ mImageCache = mock(ImageLoader.ImageCache.class);
+ mImageLoader = new ImageLoader(mRequestQueue, mImageCache);
+ }
+
+ @Test
+ public void isCachedChecksCache() throws Exception {
+ when(mImageCache.getBitmap(anyString())).thenReturn(null);
+ Assert.assertFalse(mImageLoader.isCached("http://foo", 0, 0));
+ }
+
+ @Test
+ public void getWithCacheHit() throws Exception {
+ Bitmap bitmap = Bitmap.createBitmap(1, 1, null);
+ ImageLoader.ImageListener listener = mock(ImageLoader.ImageListener.class);
+ when(mImageCache.getBitmap(anyString())).thenReturn(bitmap);
+ ImageLoader.ImageContainer ic = mImageLoader.get("http://foo", listener);
+ Assert.assertSame(bitmap, ic.getBitmap());
+ verify(listener).onResponse(ic, true);
+ }
+
+ @Test
+ public void getWithCacheMiss() throws Exception {
+ when(mImageCache.getBitmap(anyString())).thenReturn(null);
+ ImageLoader.ImageListener listener = mock(ImageLoader.ImageListener.class);
+ // Ask for the image to be loaded.
+ mImageLoader.get("http://foo", listener);
+ // Second pass to test deduping logic.
+ mImageLoader.get("http://foo", listener);
+ // Response callback should be called both times.
+ verify(listener, times(2)).onResponse(any(ImageLoader.ImageContainer.class), eq(true));
+ // But request should be enqueued only once.
+ verify(mRequestQueue, times(1)).add(Mockito.<Request<?>>any());
+ }
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch API breaking changes.
+ ImageLoader.getImageListener(null, -1, -1);
+ mImageLoader.setBatchedResponseDelay(1000);
+
+ assertNotNull(
+ ImageLoader.class.getConstructor(RequestQueue.class, ImageLoader.ImageCache.class));
+
+ assertNotNull(
+ ImageLoader.class.getMethod(
+ "getImageListener", ImageView.class, int.class, int.class));
+ assertNotNull(ImageLoader.class.getMethod("isCached", String.class, int.class, int.class));
+ assertNotNull(
+ ImageLoader.class.getMethod(
+ "isCached", String.class, int.class, int.class, ImageView.ScaleType.class));
+ assertNotNull(
+ ImageLoader.class.getMethod("get", String.class, ImageLoader.ImageListener.class));
+ assertNotNull(
+ ImageLoader.class.getMethod(
+ "get",
+ String.class,
+ ImageLoader.ImageListener.class,
+ int.class,
+ int.class));
+ assertNotNull(
+ ImageLoader.class.getMethod(
+ "get",
+ String.class,
+ ImageLoader.ImageListener.class,
+ int.class,
+ int.class,
+ ImageView.ScaleType.class));
+ assertNotNull(ImageLoader.class.getMethod("setBatchedResponseDelay", int.class));
+
+ assertNotNull(
+ ImageLoader.ImageListener.class.getMethod(
+ "onResponse", ImageLoader.ImageContainer.class, boolean.class));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java b/core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java
new file mode 100644
index 0000000..6b50319
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import com.android.volley.NetworkResponse;
+import com.android.volley.Response;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowBitmapFactory;
+
+@RunWith(RobolectricTestRunner.class)
+public class ImageRequestTest {
+
+ @Test
+ public void parseNetworkResponse_resizing() throws Exception {
+ // This is a horrible hack but Robolectric doesn't have a way to provide
+ // width and height hints for decodeByteArray. It works because the byte array
+ // "file:fake" is ASCII encodable and thus the name in Robolectric's fake
+ // bitmap creator survives as-is, and provideWidthAndHeightHints puts
+ // "file:" + name in its lookaside map. I write all this because it will
+ // probably break mysteriously at some point and I feel terrible about your
+ // having to debug it.
+ byte[] jpegBytes = "file:fake".getBytes(StandardCharsets.UTF_8);
+ ShadowBitmapFactory.provideWidthAndHeightHints("fake", 1024, 500);
+ NetworkResponse jpeg = new NetworkResponse(jpegBytes);
+
+ // Scale the image uniformly (maintain the image's aspect ratio) so that
+ // both dimensions (width and height) of the image will be equal to or
+ // less than the corresponding dimension of the view.
+ ScaleType scalteType = ScaleType.CENTER_INSIDE;
+
+ // Exact sizes
+ verifyResize(jpeg, 512, 250, scalteType, 512, 250); // exactly half
+ verifyResize(jpeg, 511, 249, scalteType, 509, 249); // just under half
+ verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); // larger
+ verifyResize(jpeg, 500, 500, scalteType, 500, 244); // keep same ratio
+
+ // Specify only width, preserve aspect ratio
+ verifyResize(jpeg, 512, 0, scalteType, 512, 250);
+ verifyResize(jpeg, 800, 0, scalteType, 800, 390);
+ verifyResize(jpeg, 1024, 0, scalteType, 1024, 500);
+
+ // Specify only height, preserve aspect ratio
+ verifyResize(jpeg, 0, 250, scalteType, 512, 250);
+ verifyResize(jpeg, 0, 391, scalteType, 800, 391);
+ verifyResize(jpeg, 0, 500, scalteType, 1024, 500);
+
+ // No resize
+ verifyResize(jpeg, 0, 0, scalteType, 1024, 500);
+
+ // Scale the image uniformly (maintain the image's aspect ratio) so that
+ // both dimensions (width and height) of the image will be equal to or
+ // larger than the corresponding dimension of the view.
+ scalteType = ScaleType.CENTER_CROP;
+
+ // Exact sizes
+ verifyResize(jpeg, 512, 250, scalteType, 512, 250);
+ verifyResize(jpeg, 511, 249, scalteType, 511, 249);
+ verifyResize(jpeg, 1080, 500, scalteType, 1024, 500);
+ verifyResize(jpeg, 500, 500, scalteType, 1024, 500);
+
+ // Specify only width
+ verifyResize(jpeg, 512, 0, scalteType, 512, 250);
+ verifyResize(jpeg, 800, 0, scalteType, 800, 390);
+ verifyResize(jpeg, 1024, 0, scalteType, 1024, 500);
+
+ // Specify only height
+ verifyResize(jpeg, 0, 250, scalteType, 512, 250);
+ verifyResize(jpeg, 0, 391, scalteType, 800, 391);
+ verifyResize(jpeg, 0, 500, scalteType, 1024, 500);
+
+ // No resize
+ verifyResize(jpeg, 0, 0, scalteType, 1024, 500);
+
+ // Scale in X and Y independently, so that src matches dst exactly. This
+ // may change the aspect ratio of the src.
+ scalteType = ScaleType.FIT_XY;
+
+ // Exact sizes
+ verifyResize(jpeg, 512, 250, scalteType, 512, 250);
+ verifyResize(jpeg, 511, 249, scalteType, 511, 249);
+ verifyResize(jpeg, 1080, 500, scalteType, 1024, 500);
+ verifyResize(jpeg, 500, 500, scalteType, 500, 500);
+
+ // Specify only width
+ verifyResize(jpeg, 512, 0, scalteType, 512, 500);
+ verifyResize(jpeg, 800, 0, scalteType, 800, 500);
+ verifyResize(jpeg, 1024, 0, scalteType, 1024, 500);
+
+ // Specify only height
+ verifyResize(jpeg, 0, 250, scalteType, 1024, 250);
+ verifyResize(jpeg, 0, 391, scalteType, 1024, 391);
+ verifyResize(jpeg, 0, 500, scalteType, 1024, 500);
+
+ // No resize
+ verifyResize(jpeg, 0, 0, scalteType, 1024, 500);
+ }
+
+ private void verifyResize(
+ NetworkResponse networkResponse,
+ int maxWidth,
+ int maxHeight,
+ ScaleType scaleType,
+ int expectedWidth,
+ int expectedHeight) {
+ ImageRequest request =
+ new ImageRequest("", null, maxWidth, maxHeight, scaleType, Config.RGB_565, null);
+ Response<Bitmap> response = request.parseNetworkResponse(networkResponse);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ Bitmap bitmap = response.result;
+ assertNotNull(bitmap);
+ assertEquals(expectedWidth, bitmap.getWidth());
+ assertEquals(expectedHeight, bitmap.getHeight());
+ }
+
+ @Test
+ public void findBestSampleSize() {
+ // desired == actual == 1
+ assertEquals(1, ImageRequest.findBestSampleSize(100, 150, 100, 150));
+
+ // exactly half == 2
+ assertEquals(2, ImageRequest.findBestSampleSize(280, 160, 140, 80));
+
+ // just over half == 1
+ assertEquals(1, ImageRequest.findBestSampleSize(1000, 800, 501, 401));
+
+ // just under 1/4 == 4
+ assertEquals(4, ImageRequest.findBestSampleSize(100, 200, 24, 50));
+ }
+
+ private static byte[] readInputStream(InputStream in) throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int count;
+ while ((count = in.read(buffer)) != -1) {
+ bytes.write(buffer, 0, count);
+ }
+ in.close();
+ return bytes.toByteArray();
+ }
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch-all test to find API-breaking changes.
+ assertNotNull(
+ ImageRequest.class.getConstructor(
+ String.class,
+ Response.Listener.class,
+ int.class,
+ int.class,
+ Bitmap.Config.class,
+ Response.ErrorListener.class));
+ assertNotNull(
+ ImageRequest.class.getConstructor(
+ String.class,
+ Response.Listener.class,
+ int.class,
+ int.class,
+ ImageView.ScaleType.class,
+ Bitmap.Config.class,
+ Response.ErrorListener.class));
+ assertEquals(ImageRequest.DEFAULT_IMAGE_TIMEOUT_MS, 1000);
+ assertEquals(ImageRequest.DEFAULT_IMAGE_MAX_RETRIES, 2);
+ assertEquals(ImageRequest.DEFAULT_IMAGE_BACKOFF_MULT, 2f, 0);
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java b/core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java
new file mode 100644
index 0000000..70bb2ea
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.volley.NetworkResponse;
+import com.android.volley.Response;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class JsonRequestCharsetTest {
+
+ /** String in Czech - "Retezec v cestine." */
+ private static final String TEXT_VALUE = "\u0158et\u011bzec v \u010de\u0161tin\u011b.";
+
+ private static final String TEXT_NAME = "text";
+ private static final int TEXT_INDEX = 0;
+
+ /**
+ * Copyright symbol has different encoding in utf-8 and ISO-8859-1, and it doesn't exists in
+ * ISO-8859-2
+ */
+ private static final String COPY_VALUE = "\u00a9";
+
+ private static final String COPY_NAME = "copyright";
+ private static final int COPY_INDEX = 1;
+
+ @Test
+ public void defaultCharsetJsonObject() throws Exception {
+ // UTF-8 is default charset for JSON
+ byte[] data = jsonObjectString().getBytes(Charset.forName("UTF-8"));
+ NetworkResponse network = new NetworkResponse(data);
+ JsonObjectRequest objectRequest = new JsonObjectRequest("", null, null, null);
+ Response<JSONObject> objectResponse = objectRequest.parseNetworkResponse(network);
+
+ assertNotNull(objectResponse);
+ assertTrue(objectResponse.isSuccess());
+ assertEquals(TEXT_VALUE, objectResponse.result.getString(TEXT_NAME));
+ assertEquals(COPY_VALUE, objectResponse.result.getString(COPY_NAME));
+ }
+
+ @Test
+ public void defaultCharsetJsonArray() throws Exception {
+ // UTF-8 is default charset for JSON
+ byte[] data = jsonArrayString().getBytes(Charset.forName("UTF-8"));
+ NetworkResponse network = new NetworkResponse(data);
+ JsonArrayRequest arrayRequest = new JsonArrayRequest("", null, null);
+ Response<JSONArray> arrayResponse = arrayRequest.parseNetworkResponse(network);
+
+ assertNotNull(arrayResponse);
+ assertTrue(arrayResponse.isSuccess());
+ assertEquals(TEXT_VALUE, arrayResponse.result.getString(TEXT_INDEX));
+ assertEquals(COPY_VALUE, arrayResponse.result.getString(COPY_INDEX));
+ }
+
+ @Test
+ public void specifiedCharsetJsonObject() throws Exception {
+ byte[] data = jsonObjectString().getBytes(Charset.forName("ISO-8859-1"));
+ Map<String, String> headers = new HashMap<String, String>();
+ headers.put("Content-Type", "application/json; charset=iso-8859-1");
+ NetworkResponse network = new NetworkResponse(data, headers);
+ JsonObjectRequest objectRequest = new JsonObjectRequest("", null, null, null);
+ Response<JSONObject> objectResponse = objectRequest.parseNetworkResponse(network);
+
+ assertNotNull(objectResponse);
+ assertTrue(objectResponse.isSuccess());
+ // don't check the text in Czech, ISO-8859-1 doesn't support some Czech characters
+ assertEquals(COPY_VALUE, objectResponse.result.getString(COPY_NAME));
+ }
+
+ @Test
+ public void specifiedCharsetJsonArray() throws Exception {
+ byte[] data = jsonArrayString().getBytes(Charset.forName("ISO-8859-2"));
+ Map<String, String> headers = new HashMap<String, String>();
+ headers.put("Content-Type", "application/json; charset=iso-8859-2");
+ NetworkResponse network = new NetworkResponse(data, headers);
+ JsonArrayRequest arrayRequest = new JsonArrayRequest("", null, null);
+ Response<JSONArray> arrayResponse = arrayRequest.parseNetworkResponse(network);
+
+ assertNotNull(arrayResponse);
+ assertTrue(arrayResponse.isSuccess());
+ assertEquals(TEXT_VALUE, arrayResponse.result.getString(TEXT_INDEX));
+ // don't check the copyright symbol, ISO-8859-2 doesn't have it, but it has Czech characters
+ }
+
+ private static String jsonObjectString() throws Exception {
+ JSONObject json = new JSONObject().put(TEXT_NAME, TEXT_VALUE).put(COPY_NAME, COPY_VALUE);
+ return json.toString();
+ }
+
+ private static String jsonArrayString() throws Exception {
+ JSONArray json = new JSONArray().put(TEXT_INDEX, TEXT_VALUE).put(COPY_INDEX, COPY_VALUE);
+ return json.toString();
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java b/core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java
new file mode 100644
index 0000000..44c0ad9
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2015 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.junit.Assert.assertNotNull;
+
+import com.android.volley.Response;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class JsonRequestTest {
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch-all test to find API-breaking changes.
+ assertNotNull(
+ JsonRequest.class.getConstructor(
+ String.class,
+ String.class,
+ Response.Listener.class,
+ Response.ErrorListener.class));
+ assertNotNull(
+ JsonRequest.class.getConstructor(
+ int.class,
+ String.class,
+ String.class,
+ Response.Listener.class,
+ Response.ErrorListener.class));
+
+ assertNotNull(
+ JsonArrayRequest.class.getConstructor(
+ String.class, Response.Listener.class, Response.ErrorListener.class));
+ assertNotNull(
+ JsonArrayRequest.class.getConstructor(
+ int.class,
+ String.class,
+ JSONArray.class,
+ Response.Listener.class,
+ Response.ErrorListener.class));
+
+ assertNotNull(
+ JsonObjectRequest.class.getConstructor(
+ String.class,
+ JSONObject.class,
+ Response.Listener.class,
+ Response.ErrorListener.class));
+ assertNotNull(
+ JsonObjectRequest.class.getConstructor(
+ int.class,
+ String.class,
+ JSONObject.class,
+ Response.Listener.class,
+ Response.ErrorListener.class));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java b/core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java
new file mode 100644
index 0000000..fd2073e
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2014 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.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.ImageView.ScaleType;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class NetworkImageViewTest {
+ private NetworkImageView mNIV;
+ private MockImageLoader mMockImageLoader;
+
+ @Before
+ public void setUp() throws Exception {
+ mMockImageLoader = new MockImageLoader();
+ mNIV = new NetworkImageView(RuntimeEnvironment.application);
+ }
+
+ @Test
+ public void setImageUrl_requestsImage() {
+ mNIV.setLayoutParams(
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+ mNIV.setImageUrl("http://foo", mMockImageLoader);
+ assertEquals("http://foo", mMockImageLoader.lastRequestUrl);
+ assertEquals(0, mMockImageLoader.lastMaxWidth);
+ assertEquals(0, mMockImageLoader.lastMaxHeight);
+ }
+
+ // public void testSetImageUrl_setsMaxSize() {
+ // // TODO: Not sure how to make getWidth() return something from an
+ // // instrumentation test. Write this test once it's figured out.
+ // }
+
+ private static class MockImageLoader extends ImageLoader {
+ public MockImageLoader() {
+ super(null, null);
+ }
+
+ public String lastRequestUrl;
+ public int lastMaxWidth;
+ public int lastMaxHeight;
+
+ @Override
+ public ImageContainer get(
+ String requestUrl,
+ ImageListener imageListener,
+ int maxWidth,
+ int maxHeight,
+ ScaleType scaleType) {
+ lastRequestUrl = requestUrl;
+ lastMaxWidth = maxWidth;
+ lastMaxHeight = maxHeight;
+ return null;
+ }
+ }
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch-all test to find API-breaking changes.
+ assertNotNull(NetworkImageView.class.getConstructor(Context.class));
+ assertNotNull(NetworkImageView.class.getConstructor(Context.class, AttributeSet.class));
+ assertNotNull(
+ NetworkImageView.class.getConstructor(
+ Context.class, AttributeSet.class, int.class));
+
+ assertNotNull(
+ NetworkImageView.class.getMethod("setImageUrl", String.class, ImageLoader.class));
+ assertNotNull(NetworkImageView.class.getMethod("setDefaultImageDrawable", Drawable.class));
+ assertNotNull(NetworkImageView.class.getMethod("setDefaultImageBitmap", Bitmap.class));
+ assertNotNull(NetworkImageView.class.getMethod("setDefaultImageResId", int.class));
+ assertNotNull(NetworkImageView.class.getMethod("setErrorImageDrawable", Drawable.class));
+ assertNotNull(NetworkImageView.class.getMethod("setErrorImageBitmap", Bitmap.class));
+ assertNotNull(NetworkImageView.class.getMethod("setErrorImageResId", int.class));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java b/core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java
new file mode 100644
index 0000000..266edcd
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Arrays;
+import org.junit.Test;
+
+public class PoolingByteArrayOutputStreamTest {
+ @Test
+ public void pooledOneBuffer() throws IOException {
+ ByteArrayPool pool = new ByteArrayPool(32768);
+ writeOneBuffer(pool);
+ writeOneBuffer(pool);
+ writeOneBuffer(pool);
+ }
+
+ @Test
+ public void pooledIndividualWrites() throws IOException {
+ ByteArrayPool pool = new ByteArrayPool(32768);
+ writeBytesIndividually(pool);
+ writeBytesIndividually(pool);
+ writeBytesIndividually(pool);
+ }
+
+ @Test
+ public void unpooled() throws IOException {
+ ByteArrayPool pool = new ByteArrayPool(0);
+ writeOneBuffer(pool);
+ writeOneBuffer(pool);
+ writeOneBuffer(pool);
+ }
+
+ @Test
+ public void unpooledIndividualWrites() throws IOException {
+ ByteArrayPool pool = new ByteArrayPool(0);
+ writeBytesIndividually(pool);
+ writeBytesIndividually(pool);
+ writeBytesIndividually(pool);
+ }
+
+ private void writeOneBuffer(ByteArrayPool pool) throws IOException {
+ byte[] data = new byte[16384];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) (i & 0xff);
+ }
+ PoolingByteArrayOutputStream os = new PoolingByteArrayOutputStream(pool);
+ os.write(data);
+
+ assertTrue(Arrays.equals(data, os.toByteArray()));
+ }
+
+ private void writeBytesIndividually(ByteArrayPool pool) {
+ byte[] data = new byte[16384];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) (i & 0xff);
+ }
+ PoolingByteArrayOutputStream os = new PoolingByteArrayOutputStream(pool);
+ for (int i = 0; i < data.length; i++) {
+ os.write(data[i]);
+ }
+
+ assertTrue(Arrays.equals(data, os.toByteArray()));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java b/core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java
new file mode 100644
index 0000000..5b5c975
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 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.junit.Assert.assertNotNull;
+
+import com.android.volley.Request;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class RequestFutureTest {
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch-all test to find API-breaking changes.
+ assertNotNull(RequestFuture.class.getMethod("newFuture"));
+ assertNotNull(RequestFuture.class.getMethod("setRequest", Request.class));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java b/core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java
new file mode 100644
index 0000000..1899b71
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 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.junit.Assert.assertNotNull;
+
+import com.android.volley.Cache;
+import com.android.volley.Network;
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.ResponseDelivery;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class RequestQueueTest {
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch-all test to find API-breaking changes.
+ assertNotNull(
+ RequestQueue.class.getConstructor(
+ Cache.class, Network.class, int.class, ResponseDelivery.class));
+ assertNotNull(RequestQueue.class.getConstructor(Cache.class, Network.class, int.class));
+ assertNotNull(RequestQueue.class.getConstructor(Cache.class, Network.class));
+
+ assertNotNull(RequestQueue.class.getMethod("start"));
+ assertNotNull(RequestQueue.class.getMethod("stop"));
+ assertNotNull(RequestQueue.class.getMethod("getSequenceNumber"));
+ assertNotNull(RequestQueue.class.getMethod("getCache"));
+ assertNotNull(RequestQueue.class.getMethod("cancelAll", RequestQueue.RequestFilter.class));
+ assertNotNull(RequestQueue.class.getMethod("cancelAll", Object.class));
+ assertNotNull(RequestQueue.class.getMethod("add", Request.class));
+ assertNotNull(RequestQueue.class.getDeclaredMethod("finish", Request.class));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/RequestTest.java b/core/src/test/java/com/android/volley/toolbox/RequestTest.java
new file mode 100644
index 0000000..0911ad6
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/RequestTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 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.junit.Assert.assertNotNull;
+
+import com.android.volley.Cache;
+import com.android.volley.NetworkResponse;
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.Response;
+import com.android.volley.RetryPolicy;
+import com.android.volley.VolleyError;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class RequestTest {
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch-all test to find API-breaking changes.
+ assertNotNull(
+ Request.class.getConstructor(
+ int.class, String.class, Response.ErrorListener.class));
+
+ assertNotNull(Request.class.getMethod("getMethod"));
+ assertNotNull(Request.class.getMethod("setTag", Object.class));
+ assertNotNull(Request.class.getMethod("getTag"));
+ assertNotNull(Request.class.getMethod("getErrorListener"));
+ assertNotNull(Request.class.getMethod("getTrafficStatsTag"));
+ assertNotNull(Request.class.getMethod("setRetryPolicy", RetryPolicy.class));
+ assertNotNull(Request.class.getMethod("addMarker", String.class));
+ assertNotNull(Request.class.getDeclaredMethod("finish", String.class));
+ assertNotNull(Request.class.getMethod("setRequestQueue", RequestQueue.class));
+ assertNotNull(Request.class.getMethod("setSequence", int.class));
+ assertNotNull(Request.class.getMethod("getSequence"));
+ assertNotNull(Request.class.getMethod("getUrl"));
+ assertNotNull(Request.class.getMethod("getCacheKey"));
+ assertNotNull(Request.class.getMethod("setCacheEntry", Cache.Entry.class));
+ assertNotNull(Request.class.getMethod("getCacheEntry"));
+ assertNotNull(Request.class.getMethod("cancel"));
+ assertNotNull(Request.class.getMethod("isCanceled"));
+ assertNotNull(Request.class.getMethod("getHeaders"));
+ assertNotNull(Request.class.getDeclaredMethod("getParams"));
+ assertNotNull(Request.class.getDeclaredMethod("getParamsEncoding"));
+ assertNotNull(Request.class.getMethod("getBodyContentType"));
+ assertNotNull(Request.class.getMethod("getBody"));
+ assertNotNull(Request.class.getMethod("setShouldCache", boolean.class));
+ assertNotNull(Request.class.getMethod("shouldCache"));
+ assertNotNull(Request.class.getMethod("getPriority"));
+ assertNotNull(Request.class.getMethod("getTimeoutMs"));
+ assertNotNull(Request.class.getMethod("getRetryPolicy"));
+ assertNotNull(Request.class.getMethod("markDelivered"));
+ assertNotNull(Request.class.getMethod("hasHadResponseDelivered"));
+ assertNotNull(
+ Request.class.getDeclaredMethod("parseNetworkResponse", NetworkResponse.class));
+ assertNotNull(Request.class.getDeclaredMethod("parseNetworkError", VolleyError.class));
+ assertNotNull(Request.class.getDeclaredMethod("deliverResponse", Object.class));
+ assertNotNull(Request.class.getMethod("deliverError", VolleyError.class));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/ResponseTest.java b/core/src/test/java/com/android/volley/toolbox/ResponseTest.java
new file mode 100644
index 0000000..44438fa
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/ResponseTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2015 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.junit.Assert.assertNotNull;
+
+import com.android.volley.Cache;
+import com.android.volley.NetworkResponse;
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ResponseTest {
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch-all test to find API-breaking changes.
+ assertNotNull(Response.class.getMethod("success", Object.class, Cache.Entry.class));
+ assertNotNull(Response.class.getMethod("error", VolleyError.class));
+ assertNotNull(Response.class.getMethod("isSuccess"));
+
+ assertNotNull(Response.Listener.class.getDeclaredMethod("onResponse", Object.class));
+
+ assertNotNull(
+ Response.ErrorListener.class.getDeclaredMethod(
+ "onErrorResponse", VolleyError.class));
+
+ assertNotNull(
+ NetworkResponse.class.getConstructor(
+ int.class, byte[].class, Map.class, boolean.class, long.class));
+ assertNotNull(
+ NetworkResponse.class.getConstructor(
+ int.class, byte[].class, Map.class, boolean.class));
+ assertNotNull(NetworkResponse.class.getConstructor(byte[].class));
+ assertNotNull(NetworkResponse.class.getConstructor(byte[].class, Map.class));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/toolbox/StringRequestTest.java b/core/src/test/java/com/android/volley/toolbox/StringRequestTest.java
new file mode 100644
index 0000000..0ecb06b
--- /dev/null
+++ b/core/src/test/java/com/android/volley/toolbox/StringRequestTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015 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.junit.Assert.assertNotNull;
+
+import com.android.volley.Response;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class StringRequestTest {
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch-all test to find API-breaking changes.
+ assertNotNull(
+ StringRequest.class.getConstructor(
+ String.class, Response.Listener.class, Response.ErrorListener.class));
+ assertNotNull(
+ StringRequest.class.getConstructor(
+ int.class,
+ String.class,
+ Response.Listener.class,
+ Response.ErrorListener.class));
+ }
+}
diff --git a/core/src/test/java/com/android/volley/utils/CacheTestUtils.java b/core/src/test/java/com/android/volley/utils/CacheTestUtils.java
new file mode 100644
index 0000000..5980712
--- /dev/null
+++ b/core/src/test/java/com/android/volley/utils/CacheTestUtils.java
@@ -0,0 +1,89 @@
+/*
+ * 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.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;
+
+public class CacheTestUtils {
+
+ /**
+ * Makes a random cache entry.
+ *
+ * @param data Data to use, or null to use random data
+ * @param isExpired Whether the TTLs should be set such that this entry is expired
+ * @param needsRefresh Whether the TTLs should be set such that this entry needs refresh
+ */
+ public static Cache.Entry makeRandomCacheEntry(
+ byte[] data, boolean isExpired, boolean needsRefresh) {
+ Random random = new Random();
+ Cache.Entry entry = new Cache.Entry();
+ if (data != null) {
+ entry.data = data;
+ } else {
+ entry.data = new byte[random.nextInt(1024)];
+ }
+ entry.etag = String.valueOf(random.nextLong());
+ entry.lastModified = random.nextLong();
+ entry.ttl = isExpired ? 0 : Long.MAX_VALUE;
+ entry.softTtl = needsRefresh ? 0 : Long.MAX_VALUE;
+ return entry;
+ }
+
+ /**
+ * Like {@link #makeRandomCacheEntry(byte[], boolean, boolean)} but defaults to an unexpired
+ * entry.
+ */
+ 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();
+ }
+}
diff --git a/core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java b/core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java
new file mode 100644
index 0000000..67e5923
--- /dev/null
+++ b/core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java
@@ -0,0 +1,37 @@
+/*
+ * 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.utils;
+
+import com.android.volley.ExecutorDelivery;
+import java.util.concurrent.Executor;
+
+/**
+ * A ResponseDelivery for testing that immediately delivers responses instead of posting back to the
+ * main thread.
+ */
+public class ImmediateResponseDelivery extends ExecutorDelivery {
+
+ public ImmediateResponseDelivery() {
+ super(
+ new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ });
+ }
+}
diff --git a/core/src/test/resources/org.robolectric.Config.properties b/core/src/test/resources/org.robolectric.Config.properties
new file mode 100644
index 0000000..9daf692
--- /dev/null
+++ b/core/src/test/resources/org.robolectric.Config.properties
@@ -0,0 +1 @@
+manifest=src/main/AndroidManifest.xml