aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/android/volley/toolbox
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/android/volley/toolbox')
-rw-r--r--src/main/java/com/android/volley/toolbox/AsyncHttpStack.java170
-rw-r--r--src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java288
-rw-r--r--src/main/java/com/android/volley/toolbox/BasicNetwork.java230
-rw-r--r--src/main/java/com/android/volley/toolbox/FileSupplier.java24
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpHeaderParser.java74
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpResponse.java42
-rw-r--r--src/main/java/com/android/volley/toolbox/HurlStack.java8
-rw-r--r--src/main/java/com/android/volley/toolbox/NetworkUtility.java196
-rw-r--r--src/main/java/com/android/volley/toolbox/NoAsyncCache.java37
-rw-r--r--src/main/java/com/android/volley/toolbox/UrlRewriter.java29
10 files changed, 870 insertions, 228 deletions
diff --git a/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java b/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java
new file mode 100644
index 0000000..bafab8c
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.VolleyLog;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Asynchronous extension of the {@link BaseHttpStack} class. */
+public abstract class AsyncHttpStack extends BaseHttpStack {
+ private ExecutorService mBlockingExecutor;
+ private ExecutorService mNonBlockingExecutor;
+
+ public interface OnRequestComplete {
+ /** Invoked when the stack successfully completes a request. */
+ void onSuccess(HttpResponse httpResponse);
+
+ /** Invoked when the stack throws an {@link AuthFailureError} during a request. */
+ void onAuthError(AuthFailureError authFailureError);
+
+ /** Invoked when the stack throws an {@link IOException} during a request. */
+ void onError(IOException ioException);
+ }
+
+ /**
+ * Makes an HTTP request with the given parameters, and calls the {@link OnRequestComplete}
+ * callback, with either the {@link HttpResponse} or error that was thrown.
+ *
+ * @param request to perform
+ * @param additionalHeaders to be sent together with {@link Request#getHeaders()}
+ * @param callback to be called after retrieving the {@link HttpResponse} or throwing an error.
+ */
+ public abstract void executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders, OnRequestComplete callback);
+
+ /**
+ * This method sets the non blocking executor to be used by the stack for non-blocking tasks.
+ * This method must be called before executing any requests.
+ */
+ @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+ public void setNonBlockingExecutor(ExecutorService executor) {
+ mNonBlockingExecutor = executor;
+ }
+
+ /**
+ * This method sets the blocking executor to be used by the stack for potentially blocking
+ * tasks. This method must be called before executing any requests.
+ */
+ @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+ public void setBlockingExecutor(ExecutorService executor) {
+ mBlockingExecutor = executor;
+ }
+
+ /** Gets blocking executor to perform any potentially blocking tasks. */
+ protected ExecutorService getBlockingExecutor() {
+ return mBlockingExecutor;
+ }
+
+ /** Gets non-blocking executor to perform any non-blocking tasks. */
+ protected ExecutorService getNonBlockingExecutor() {
+ return mNonBlockingExecutor;
+ }
+
+ /**
+ * Performs an HTTP request with the given parameters.
+ *
+ * @param request the request to perform
+ * @param additionalHeaders additional headers to be sent together with {@link
+ * Request#getHeaders()}
+ * @return the {@link HttpResponse}
+ * @throws IOException if an I/O error occurs during the request
+ * @throws AuthFailureError if an authentication failure occurs during the request
+ */
+ @Override
+ public final HttpResponse executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference<Response> entry = new AtomicReference<>();
+ executeRequest(
+ request,
+ additionalHeaders,
+ new OnRequestComplete() {
+ @Override
+ public void onSuccess(HttpResponse httpResponse) {
+ Response response =
+ new Response(
+ httpResponse,
+ /* ioException= */ null,
+ /* authFailureError= */ null);
+ entry.set(response);
+ latch.countDown();
+ }
+
+ @Override
+ public void onAuthError(AuthFailureError authFailureError) {
+ Response response =
+ new Response(
+ /* httpResponse= */ null,
+ /* ioException= */ null,
+ authFailureError);
+ entry.set(response);
+ latch.countDown();
+ }
+
+ @Override
+ public void onError(IOException ioException) {
+ Response response =
+ new Response(
+ /* httpResponse= */ null,
+ ioException,
+ /* authFailureError= */ null);
+ entry.set(response);
+ latch.countDown();
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ VolleyLog.e(e, "while waiting for CountDownLatch");
+ Thread.currentThread().interrupt();
+ throw new InterruptedIOException(e.toString());
+ }
+ Response response = entry.get();
+ if (response.httpResponse != null) {
+ return response.httpResponse;
+ } else if (response.ioException != null) {
+ throw response.ioException;
+ } else {
+ throw response.authFailureError;
+ }
+ }
+
+ private static class Response {
+ HttpResponse httpResponse;
+ IOException ioException;
+ AuthFailureError authFailureError;
+
+ private Response(
+ @Nullable HttpResponse httpResponse,
+ @Nullable IOException ioException,
+ @Nullable AuthFailureError authFailureError) {
+ this.httpResponse = httpResponse;
+ this.ioException = ioException;
+ this.authFailureError = authFailureError;
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java b/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java
new file mode 100644
index 0000000..55892a0
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import static com.android.volley.toolbox.NetworkUtility.logSlowRequests;
+
+import android.os.SystemClock;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import com.android.volley.AsyncNetwork;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.NetworkResponse;
+import com.android.volley.Request;
+import com.android.volley.RequestTask;
+import com.android.volley.VolleyError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+
+/** A network performing Volley requests over an {@link HttpStack}. */
+public class BasicAsyncNetwork extends AsyncNetwork {
+
+ private final AsyncHttpStack mAsyncStack;
+ private final ByteArrayPool mPool;
+
+ /**
+ * @param httpStack HTTP stack to be used
+ * @param pool a buffer pool that improves GC performance in copy operations
+ */
+ private BasicAsyncNetwork(AsyncHttpStack httpStack, ByteArrayPool pool) {
+ mAsyncStack = httpStack;
+ mPool = pool;
+ }
+
+ @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+ @Override
+ public void setBlockingExecutor(ExecutorService executor) {
+ super.setBlockingExecutor(executor);
+ mAsyncStack.setBlockingExecutor(executor);
+ }
+
+ @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+ @Override
+ public void setNonBlockingExecutor(ExecutorService executor) {
+ super.setNonBlockingExecutor(executor);
+ mAsyncStack.setNonBlockingExecutor(executor);
+ }
+
+ /* Method to be called after a successful network request */
+ private void onRequestSucceeded(
+ final Request<?> request,
+ final long requestStartMs,
+ final HttpResponse httpResponse,
+ final OnRequestComplete callback) {
+ final int statusCode = httpResponse.getStatusCode();
+ final List<Header> responseHeaders = httpResponse.getHeaders();
+ // Handle cache validation.
+ if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
+ long requestDuration = SystemClock.elapsedRealtime() - requestStartMs;
+ callback.onSuccess(
+ NetworkUtility.getNotModifiedNetworkResponse(
+ request, requestDuration, responseHeaders));
+ return;
+ }
+
+ byte[] responseContents = httpResponse.getContentBytes();
+ if (responseContents == null && httpResponse.getContent() == null) {
+ // Add 0 byte response as a way of honestly representing a
+ // no-content request.
+ responseContents = new byte[0];
+ }
+
+ if (responseContents != null) {
+ onResponseRead(
+ requestStartMs,
+ statusCode,
+ httpResponse,
+ request,
+ callback,
+ responseHeaders,
+ responseContents);
+ return;
+ }
+
+ // The underlying AsyncHttpStack does not support asynchronous reading of the response into
+ // a byte array, so we need to submit a blocking task to copy the response from the
+ // InputStream instead.
+ final InputStream inputStream = httpResponse.getContent();
+ getBlockingExecutor()
+ .execute(
+ new ResponseParsingTask<>(
+ inputStream,
+ httpResponse,
+ request,
+ callback,
+ requestStartMs,
+ responseHeaders,
+ statusCode));
+ }
+
+ /* Method to be called after a failed network request */
+ private void onRequestFailed(
+ Request<?> request,
+ OnRequestComplete callback,
+ IOException exception,
+ long requestStartMs,
+ @Nullable HttpResponse httpResponse,
+ @Nullable byte[] responseContents) {
+ try {
+ NetworkUtility.handleException(
+ request, exception, requestStartMs, httpResponse, responseContents);
+ } catch (VolleyError volleyError) {
+ callback.onError(volleyError);
+ return;
+ }
+ performRequest(request, callback);
+ }
+
+ @Override
+ public void performRequest(final Request<?> request, final OnRequestComplete callback) {
+ if (getBlockingExecutor() == null) {
+ throw new IllegalStateException(
+ "mBlockingExecuter must be set before making a request");
+ }
+ final long requestStartMs = SystemClock.elapsedRealtime();
+ // Gather headers.
+ final Map<String, String> additionalRequestHeaders =
+ HttpHeaderParser.getCacheHeaders(request.getCacheEntry());
+ mAsyncStack.executeRequest(
+ request,
+ additionalRequestHeaders,
+ new AsyncHttpStack.OnRequestComplete() {
+ @Override
+ public void onSuccess(HttpResponse httpResponse) {
+ onRequestSucceeded(request, requestStartMs, httpResponse, callback);
+ }
+
+ @Override
+ public void onAuthError(AuthFailureError authFailureError) {
+ callback.onError(authFailureError);
+ }
+
+ @Override
+ public void onError(IOException ioException) {
+ onRequestFailed(
+ request,
+ callback,
+ ioException,
+ requestStartMs,
+ /* httpResponse= */ null,
+ /* responseContents= */ null);
+ }
+ });
+ }
+
+ /* Helper method that determines what to do after byte[] is received */
+ private void onResponseRead(
+ long requestStartMs,
+ int statusCode,
+ HttpResponse httpResponse,
+ Request<?> request,
+ OnRequestComplete callback,
+ List<Header> responseHeaders,
+ byte[] responseContents) {
+ // if the request is slow, log it.
+ long requestLifetime = SystemClock.elapsedRealtime() - requestStartMs;
+ logSlowRequests(requestLifetime, request, responseContents, statusCode);
+
+ if (statusCode < 200 || statusCode > 299) {
+ onRequestFailed(
+ request,
+ callback,
+ new IOException(),
+ requestStartMs,
+ httpResponse,
+ responseContents);
+ return;
+ }
+
+ callback.onSuccess(
+ new NetworkResponse(
+ statusCode,
+ responseContents,
+ /* notModified= */ false,
+ SystemClock.elapsedRealtime() - requestStartMs,
+ responseHeaders));
+ }
+
+ private class ResponseParsingTask<T> extends RequestTask<T> {
+ InputStream inputStream;
+ HttpResponse httpResponse;
+ Request<T> request;
+ OnRequestComplete callback;
+ long requestStartMs;
+ List<Header> responseHeaders;
+ int statusCode;
+
+ ResponseParsingTask(
+ InputStream inputStream,
+ HttpResponse httpResponse,
+ Request<T> request,
+ OnRequestComplete callback,
+ long requestStartMs,
+ List<Header> responseHeaders,
+ int statusCode) {
+ super(request);
+ this.inputStream = inputStream;
+ this.httpResponse = httpResponse;
+ this.request = request;
+ this.callback = callback;
+ this.requestStartMs = requestStartMs;
+ this.responseHeaders = responseHeaders;
+ this.statusCode = statusCode;
+ }
+
+ @Override
+ public void run() {
+ byte[] finalResponseContents;
+ try {
+ finalResponseContents =
+ NetworkUtility.inputStreamToBytes(
+ inputStream, httpResponse.getContentLength(), mPool);
+ } catch (IOException e) {
+ onRequestFailed(request, callback, e, requestStartMs, httpResponse, null);
+ return;
+ }
+ onResponseRead(
+ requestStartMs,
+ statusCode,
+ httpResponse,
+ request,
+ callback,
+ responseHeaders,
+ finalResponseContents);
+ }
+ }
+
+ /**
+ * Builder is used to build an instance of {@link BasicAsyncNetwork} from values configured by
+ * the setters.
+ */
+ public static class Builder {
+ private static final int DEFAULT_POOL_SIZE = 4096;
+ @NonNull private AsyncHttpStack mAsyncStack;
+ private ByteArrayPool mPool;
+
+ public Builder(@NonNull AsyncHttpStack httpStack) {
+ mAsyncStack = httpStack;
+ mPool = null;
+ }
+
+ /**
+ * Sets the ByteArrayPool to be used. If not set, it will default to a pool with the default
+ * pool size.
+ */
+ public Builder setPool(ByteArrayPool pool) {
+ mPool = pool;
+ return this;
+ }
+
+ /** Builds the {@link com.android.volley.toolbox.BasicAsyncNetwork} */
+ public BasicAsyncNetwork build() {
+ if (mPool == null) {
+ mPool = new ByteArrayPool(DEFAULT_POOL_SIZE);
+ }
+ return new BasicAsyncNetwork(mAsyncStack, mPool);
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
index b527cb9..06427fe 100644
--- a/src/main/java/com/android/volley/toolbox/BasicNetwork.java
+++ b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
@@ -17,41 +17,21 @@
package com.android.volley.toolbox;
import android.os.SystemClock;
-import com.android.volley.AuthFailureError;
-import com.android.volley.Cache;
-import com.android.volley.Cache.Entry;
-import com.android.volley.ClientError;
import com.android.volley.Header;
import com.android.volley.Network;
-import com.android.volley.NetworkError;
import com.android.volley.NetworkResponse;
-import com.android.volley.NoConnectionError;
import com.android.volley.Request;
-import com.android.volley.RetryPolicy;
-import com.android.volley.ServerError;
-import com.android.volley.TimeoutError;
import com.android.volley.VolleyError;
-import com.android.volley.VolleyLog;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.SocketTimeoutException;
-import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.TreeMap;
-import java.util.TreeSet;
/** A network performing Volley requests over an {@link HttpStack}. */
public class BasicNetwork implements Network {
- protected static final boolean DEBUG = VolleyLog.DEBUG;
-
- private static final int SLOW_REQUEST_THRESHOLD_MS = 3000;
-
private static final int DEFAULT_POOL_SIZE = 4096;
/**
@@ -119,37 +99,24 @@ public class BasicNetwork implements Network {
try {
// Gather headers.
Map<String, String> additionalRequestHeaders =
- getCacheHeaders(request.getCacheEntry());
+ HttpHeaderParser.getCacheHeaders(request.getCacheEntry());
httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);
int statusCode = httpResponse.getStatusCode();
responseHeaders = httpResponse.getHeaders();
// Handle cache validation.
if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
- Entry entry = request.getCacheEntry();
- if (entry == null) {
- return new NetworkResponse(
- HttpURLConnection.HTTP_NOT_MODIFIED,
- /* data= */ null,
- /* notModified= */ true,
- SystemClock.elapsedRealtime() - requestStart,
- responseHeaders);
- }
- // Combine cached and response headers so the response will be complete.
- List<Header> combinedHeaders = combineHeaders(responseHeaders, entry);
- return new NetworkResponse(
- HttpURLConnection.HTTP_NOT_MODIFIED,
- entry.data,
- /* notModified= */ true,
- SystemClock.elapsedRealtime() - requestStart,
- combinedHeaders);
+ long requestDuration = SystemClock.elapsedRealtime() - requestStart;
+ return NetworkUtility.getNotModifiedNetworkResponse(
+ request, requestDuration, responseHeaders);
}
// Some responses such as 204s do not have content. We must check.
InputStream inputStream = httpResponse.getContent();
if (inputStream != null) {
responseContents =
- inputStreamToBytes(inputStream, httpResponse.getContentLength());
+ NetworkUtility.inputStreamToBytes(
+ inputStream, httpResponse.getContentLength(), mPool);
} else {
// Add 0 byte response as a way of honestly representing a
// no-content request.
@@ -158,7 +125,8 @@ public class BasicNetwork implements Network {
// if the request is slow, log it.
long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
- logSlowRequests(requestLifetime, request, responseContents, statusCode);
+ NetworkUtility.logSlowRequests(
+ requestLifetime, request, responseContents, statusCode);
if (statusCode < 200 || statusCode > 299) {
throw new IOException();
@@ -169,141 +137,12 @@ public class BasicNetwork implements Network {
/* notModified= */ false,
SystemClock.elapsedRealtime() - requestStart,
responseHeaders);
- } catch (SocketTimeoutException e) {
- attemptRetryOnException("socket", request, new TimeoutError());
- } catch (MalformedURLException e) {
- throw new RuntimeException("Bad URL " + request.getUrl(), e);
- } catch (IOException e) {
- int statusCode;
- if (httpResponse != null) {
- statusCode = httpResponse.getStatusCode();
- } else {
- throw new NoConnectionError(e);
- }
- VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
- NetworkResponse networkResponse;
- if (responseContents != null) {
- networkResponse =
- new NetworkResponse(
- statusCode,
- responseContents,
- /* notModified= */ false,
- SystemClock.elapsedRealtime() - requestStart,
- responseHeaders);
- if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED
- || statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
- attemptRetryOnException(
- "auth", request, new AuthFailureError(networkResponse));
- } else if (statusCode >= 400 && statusCode <= 499) {
- // Don't retry other client errors.
- throw new ClientError(networkResponse);
- } else if (statusCode >= 500 && statusCode <= 599) {
- if (request.shouldRetryServerErrors()) {
- attemptRetryOnException(
- "server", request, new ServerError(networkResponse));
- } else {
- throw new ServerError(networkResponse);
- }
- } else {
- // 3xx? No reason to retry.
- throw new ServerError(networkResponse);
- }
- } else {
- attemptRetryOnException("network", request, new NetworkError());
- }
- }
- }
- }
-
- /** Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. */
- private void logSlowRequests(
- long requestLifetime, Request<?> request, byte[] responseContents, int statusCode) {
- if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
- VolleyLog.d(
- "HTTP response for request=<%s> [lifetime=%d], [size=%s], "
- + "[rc=%d], [retryCount=%s]",
- request,
- requestLifetime,
- responseContents != null ? responseContents.length : "null",
- statusCode,
- request.getRetryPolicy().getCurrentRetryCount());
- }
- }
-
- /**
- * Attempts to prepare the request for a retry. If there are no more attempts remaining in the
- * request's retry policy, a timeout exception is thrown.
- *
- * @param request The request to use.
- */
- private static void attemptRetryOnException(
- String logPrefix, Request<?> request, VolleyError exception) throws VolleyError {
- RetryPolicy retryPolicy = request.getRetryPolicy();
- int oldTimeout = request.getTimeoutMs();
-
- try {
- retryPolicy.retry(exception);
- } catch (VolleyError e) {
- request.addMarker(
- String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
- throw e;
- }
- request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
- }
-
- private Map<String, String> getCacheHeaders(Cache.Entry entry) {
- // If there's no cache entry, we're done.
- if (entry == null) {
- return Collections.emptyMap();
- }
-
- Map<String, String> headers = new HashMap<>();
-
- if (entry.etag != null) {
- headers.put("If-None-Match", entry.etag);
- }
-
- if (entry.lastModified > 0) {
- headers.put(
- "If-Modified-Since", HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified));
- }
-
- return headers;
- }
-
- protected void logError(String what, String url, long start) {
- long now = SystemClock.elapsedRealtime();
- VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url);
- }
-
- /** Reads the contents of an InputStream into a byte[]. */
- private byte[] inputStreamToBytes(InputStream in, int contentLength)
- throws IOException, ServerError {
- PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(mPool, contentLength);
- byte[] buffer = null;
- try {
- if (in == null) {
- throw new ServerError();
- }
- buffer = mPool.getBuf(1024);
- int count;
- while ((count = in.read(buffer)) != -1) {
- bytes.write(buffer, 0, count);
- }
- return bytes.toByteArray();
- } finally {
- try {
- // Close the InputStream and release the resources by "consuming the content".
- if (in != null) {
- in.close();
- }
} catch (IOException e) {
- // This can happen if there was an exception above that left the stream in
- // an invalid state.
- VolleyLog.v("Error occurred when closing InputStream");
+ // This will either throw an exception, breaking us from the loop, or will loop
+ // again and retry the request.
+ NetworkUtility.handleException(
+ request, e, requestStart, httpResponse, responseContents);
}
- mPool.returnBuf(buffer);
- bytes.close();
}
}
@@ -321,49 +160,4 @@ public class BasicNetwork implements Network {
}
return result;
}
-
- /**
- * Combine cache headers with network response headers for an HTTP 304 response.
- *
- * <p>An HTTP 304 response does not have all header fields. We have to use the header fields
- * from the cache entry plus the new ones from the response. See also:
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
- *
- * @param responseHeaders Headers from the network response.
- * @param entry The cached response.
- * @return The combined list of headers.
- */
- private static List<Header> combineHeaders(List<Header> responseHeaders, Entry entry) {
- // First, create a case-insensitive set of header names from the network
- // response.
- Set<String> headerNamesFromNetworkResponse = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
- if (!responseHeaders.isEmpty()) {
- for (Header header : responseHeaders) {
- headerNamesFromNetworkResponse.add(header.getName());
- }
- }
-
- // Second, add headers from the cache entry to the network response as long as
- // they didn't appear in the network response, which should take precedence.
- List<Header> combinedHeaders = new ArrayList<>(responseHeaders);
- if (entry.allResponseHeaders != null) {
- if (!entry.allResponseHeaders.isEmpty()) {
- for (Header header : entry.allResponseHeaders) {
- if (!headerNamesFromNetworkResponse.contains(header.getName())) {
- combinedHeaders.add(header);
- }
- }
- }
- } else {
- // Legacy caches only have entry.responseHeaders.
- if (!entry.responseHeaders.isEmpty()) {
- for (Map.Entry<String, String> header : entry.responseHeaders.entrySet()) {
- if (!headerNamesFromNetworkResponse.contains(header.getKey())) {
- combinedHeaders.add(new Header(header.getKey(), header.getValue()));
- }
- }
- }
- }
- return combinedHeaders;
- }
}
diff --git a/src/main/java/com/android/volley/toolbox/FileSupplier.java b/src/main/java/com/android/volley/toolbox/FileSupplier.java
new file mode 100644
index 0000000..70898a6
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/FileSupplier.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import java.io.File;
+
+/** Represents a supplier for {@link File}s. */
+public interface FileSupplier {
+ File get();
+}
diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
index 1b410af..0b29e80 100644
--- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
+++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
@@ -17,6 +17,8 @@
package com.android.volley.toolbox;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
import com.android.volley.Cache;
import com.android.volley.Header;
import com.android.volley.NetworkResponse;
@@ -24,17 +26,22 @@ import com.android.volley.VolleyLog;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Date;
+import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.TimeZone;
import java.util.TreeMap;
+import java.util.TreeSet;
/** Utility methods for parsing HTTP headers. */
public class HttpHeaderParser {
- static final String HEADER_CONTENT_TYPE = "Content-Type";
+ @RestrictTo({Scope.LIBRARY_GROUP})
+ public static final String HEADER_CONTENT_TYPE = "Content-Type";
private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1";
@@ -226,4 +233,69 @@ public class HttpHeaderParser {
}
return allHeaders;
}
+
+ /**
+ * Combine cache headers with network response headers for an HTTP 304 response.
+ *
+ * <p>An HTTP 304 response does not have all header fields. We have to use the header fields
+ * from the cache entry plus the new ones from the response. See also:
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+ *
+ * @param responseHeaders Headers from the network response.
+ * @param entry The cached response.
+ * @return The combined list of headers.
+ */
+ static List<Header> combineHeaders(List<Header> responseHeaders, Cache.Entry entry) {
+ // First, create a case-insensitive set of header names from the network
+ // response.
+ Set<String> headerNamesFromNetworkResponse = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+ if (!responseHeaders.isEmpty()) {
+ for (Header header : responseHeaders) {
+ headerNamesFromNetworkResponse.add(header.getName());
+ }
+ }
+
+ // Second, add headers from the cache entry to the network response as long as
+ // they didn't appear in the network response, which should take precedence.
+ List<Header> combinedHeaders = new ArrayList<>(responseHeaders);
+ if (entry.allResponseHeaders != null) {
+ if (!entry.allResponseHeaders.isEmpty()) {
+ for (Header header : entry.allResponseHeaders) {
+ if (!headerNamesFromNetworkResponse.contains(header.getName())) {
+ combinedHeaders.add(header);
+ }
+ }
+ }
+ } else {
+ // Legacy caches only have entry.responseHeaders.
+ if (!entry.responseHeaders.isEmpty()) {
+ for (Map.Entry<String, String> header : entry.responseHeaders.entrySet()) {
+ if (!headerNamesFromNetworkResponse.contains(header.getKey())) {
+ combinedHeaders.add(new Header(header.getKey(), header.getValue()));
+ }
+ }
+ }
+ }
+ return combinedHeaders;
+ }
+
+ static Map<String, String> getCacheHeaders(Cache.Entry entry) {
+ // If there's no cache entry, we're done.
+ if (entry == null) {
+ return Collections.emptyMap();
+ }
+
+ Map<String, String> headers = new HashMap<>();
+
+ if (entry.etag != null) {
+ headers.put("If-None-Match", entry.etag);
+ }
+
+ if (entry.lastModified > 0) {
+ headers.put(
+ "If-Modified-Since", HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified));
+ }
+
+ return headers;
+ }
}
diff --git a/src/main/java/com/android/volley/toolbox/HttpResponse.java b/src/main/java/com/android/volley/toolbox/HttpResponse.java
index 9a9294f..595f926 100644
--- a/src/main/java/com/android/volley/toolbox/HttpResponse.java
+++ b/src/main/java/com/android/volley/toolbox/HttpResponse.java
@@ -15,7 +15,9 @@
*/
package com.android.volley.toolbox;
+import androidx.annotation.Nullable;
import com.android.volley.Header;
+import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
@@ -26,7 +28,8 @@ public final class HttpResponse {
private final int mStatusCode;
private final List<Header> mHeaders;
private final int mContentLength;
- private final InputStream mContent;
+ @Nullable private final InputStream mContent;
+ @Nullable private final byte[] mContentBytes;
/**
* Construct a new HttpResponse for an empty response body.
@@ -53,6 +56,23 @@ public final class HttpResponse {
mHeaders = headers;
mContentLength = contentLength;
mContent = content;
+ mContentBytes = null;
+ }
+
+ /**
+ * Construct a new HttpResponse.
+ *
+ * @param statusCode the HTTP status code of the response
+ * @param headers the response headers
+ * @param contentBytes a byte[] of the response content. This is an optimization for HTTP stacks
+ * that natively support returning a byte[].
+ */
+ public HttpResponse(int statusCode, List<Header> headers, byte[] contentBytes) {
+ mStatusCode = statusCode;
+ mHeaders = headers;
+ mContentLength = contentBytes.length;
+ mContentBytes = contentBytes;
+ mContent = null;
}
/** Returns the HTTP status code of the response. */
@@ -71,10 +91,28 @@ public final class HttpResponse {
}
/**
+ * If a byte[] was already provided by an HTTP stack that natively supports returning one, this
+ * method will return that byte[] as an optimization over copying the bytes from an input
+ * stream. It may return null, even if the response has content, as long as mContent is
+ * provided.
+ */
+ @Nullable
+ public final byte[] getContentBytes() {
+ return mContentBytes;
+ }
+
+ /**
* Returns an {@link InputStream} of the response content. May be null to indicate that the
* response has no content.
*/
+ @Nullable
public final InputStream getContent() {
- return mContent;
+ if (mContent != null) {
+ return mContent;
+ } else if (mContentBytes != null) {
+ return new ByteArrayInputStream(mContentBytes);
+ } else {
+ return null;
+ }
}
}
diff --git a/src/main/java/com/android/volley/toolbox/HurlStack.java b/src/main/java/com/android/volley/toolbox/HurlStack.java
index 9c38023..35c6a72 100644
--- a/src/main/java/com/android/volley/toolbox/HurlStack.java
+++ b/src/main/java/com/android/volley/toolbox/HurlStack.java
@@ -41,13 +41,7 @@ public class HurlStack extends BaseHttpStack {
private static final int HTTP_CONTINUE = 100;
/** An interface for transforming URLs before use. */
- public interface UrlRewriter {
- /**
- * Returns a URL to use instead of the provided one, or null to indicate this URL should not
- * be used at all.
- */
- String rewriteUrl(String originalUrl);
- }
+ public interface UrlRewriter extends com.android.volley.toolbox.UrlRewriter {}
private final UrlRewriter mUrlRewriter;
private final SSLSocketFactory mSslSocketFactory;
diff --git a/src/main/java/com/android/volley/toolbox/NetworkUtility.java b/src/main/java/com/android/volley/toolbox/NetworkUtility.java
new file mode 100644
index 0000000..44d5904
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/NetworkUtility.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import android.os.SystemClock;
+import androidx.annotation.Nullable;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Cache;
+import com.android.volley.ClientError;
+import com.android.volley.Header;
+import com.android.volley.NetworkError;
+import com.android.volley.NetworkResponse;
+import com.android.volley.NoConnectionError;
+import com.android.volley.Request;
+import com.android.volley.RetryPolicy;
+import com.android.volley.ServerError;
+import com.android.volley.TimeoutError;
+import com.android.volley.VolleyError;
+import com.android.volley.VolleyLog;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.util.List;
+
+/**
+ * Utility class for methods that are shared between {@link BasicNetwork} and {@link
+ * BasicAsyncNetwork}
+ */
+public final class NetworkUtility {
+ private static final int SLOW_REQUEST_THRESHOLD_MS = 3000;
+
+ private NetworkUtility() {}
+
+ /** Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. */
+ static void logSlowRequests(
+ long requestLifetime, Request<?> request, byte[] responseContents, int statusCode) {
+ if (VolleyLog.DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
+ VolleyLog.d(
+ "HTTP response for request=<%s> [lifetime=%d], [size=%s], "
+ + "[rc=%d], [retryCount=%s]",
+ request,
+ requestLifetime,
+ responseContents != null ? responseContents.length : "null",
+ statusCode,
+ request.getRetryPolicy().getCurrentRetryCount());
+ }
+ }
+
+ static NetworkResponse getNotModifiedNetworkResponse(
+ Request<?> request, long requestDuration, List<Header> responseHeaders) {
+ Cache.Entry entry = request.getCacheEntry();
+ if (entry == null) {
+ return new NetworkResponse(
+ HttpURLConnection.HTTP_NOT_MODIFIED,
+ /* data= */ null,
+ /* notModified= */ true,
+ requestDuration,
+ responseHeaders);
+ }
+ // Combine cached and response headers so the response will be complete.
+ List<Header> combinedHeaders = HttpHeaderParser.combineHeaders(responseHeaders, entry);
+ return new NetworkResponse(
+ HttpURLConnection.HTTP_NOT_MODIFIED,
+ entry.data,
+ /* notModified= */ true,
+ requestDuration,
+ combinedHeaders);
+ }
+
+ /** Reads the contents of an InputStream into a byte[]. */
+ static byte[] inputStreamToBytes(InputStream in, int contentLength, ByteArrayPool pool)
+ throws IOException {
+ PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(pool, contentLength);
+ byte[] buffer = null;
+ try {
+ buffer = pool.getBuf(1024);
+ int count;
+ while ((count = in.read(buffer)) != -1) {
+ bytes.write(buffer, 0, count);
+ }
+ return bytes.toByteArray();
+ } finally {
+ try {
+ // Close the InputStream and release the resources by "consuming the content".
+ if (in != null) {
+ in.close();
+ }
+ } catch (IOException e) {
+ // This can happen if there was an exception above that left the stream in
+ // an invalid state.
+ VolleyLog.v("Error occurred when closing InputStream");
+ }
+ pool.returnBuf(buffer);
+ bytes.close();
+ }
+ }
+
+ /**
+ * Attempts to prepare the request for a retry. If there are no more attempts remaining in the
+ * request's retry policy, a timeout exception is thrown.
+ *
+ * @param request The request to use.
+ */
+ private static void attemptRetryOnException(
+ final String logPrefix, final Request<?> request, final VolleyError exception)
+ throws VolleyError {
+ final RetryPolicy retryPolicy = request.getRetryPolicy();
+ final int oldTimeout = request.getTimeoutMs();
+ try {
+ retryPolicy.retry(exception);
+ } catch (VolleyError e) {
+ request.addMarker(
+ String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
+ throw e;
+ }
+ request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
+ }
+
+ /**
+ * Based on the exception thrown, decides whether to attempt to retry, or to throw the error.
+ * Also handles logging.
+ */
+ static void handleException(
+ Request<?> request,
+ IOException exception,
+ long requestStartMs,
+ @Nullable HttpResponse httpResponse,
+ @Nullable byte[] responseContents)
+ throws VolleyError {
+ if (exception instanceof SocketTimeoutException) {
+ attemptRetryOnException("socket", request, new TimeoutError());
+ } else if (exception instanceof MalformedURLException) {
+ throw new RuntimeException("Bad URL " + request.getUrl(), exception);
+ } else {
+ int statusCode;
+ if (httpResponse != null) {
+ statusCode = httpResponse.getStatusCode();
+ } else {
+ if (request.shouldRetryConnectionErrors()) {
+ attemptRetryOnException("connection", request, new NoConnectionError());
+ return;
+ } else {
+ throw new NoConnectionError(exception);
+ }
+ }
+ VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
+ NetworkResponse networkResponse;
+ if (responseContents != null) {
+ List<Header> responseHeaders;
+ responseHeaders = httpResponse.getHeaders();
+ networkResponse =
+ new NetworkResponse(
+ statusCode,
+ responseContents,
+ /* notModified= */ false,
+ SystemClock.elapsedRealtime() - requestStartMs,
+ responseHeaders);
+ if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED
+ || statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
+ attemptRetryOnException("auth", request, new AuthFailureError(networkResponse));
+ } else if (statusCode >= 400 && statusCode <= 499) {
+ // Don't retry other client errors.
+ throw new ClientError(networkResponse);
+ } else if (statusCode >= 500 && statusCode <= 599) {
+ if (request.shouldRetryServerErrors()) {
+ attemptRetryOnException(
+ "server", request, new ServerError(networkResponse));
+ } else {
+ throw new ServerError(networkResponse);
+ }
+ } else {
+ // 3xx? No reason to retry.
+ throw new ServerError(networkResponse);
+ }
+ } else {
+ attemptRetryOnException("network", request, new NetworkError());
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/NoAsyncCache.java b/src/main/java/com/android/volley/toolbox/NoAsyncCache.java
new file mode 100644
index 0000000..aa4aeea
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/NoAsyncCache.java
@@ -0,0 +1,37 @@
+package com.android.volley.toolbox;
+
+import com.android.volley.AsyncCache;
+import com.android.volley.Cache;
+
+/** An AsyncCache that doesn't cache anything. */
+public class NoAsyncCache extends AsyncCache {
+ @Override
+ public void get(String key, OnGetCompleteCallback callback) {
+ callback.onGetComplete(null);
+ }
+
+ @Override
+ public void put(String key, Cache.Entry entry, OnWriteCompleteCallback callback) {
+ callback.onWriteComplete();
+ }
+
+ @Override
+ public void clear(OnWriteCompleteCallback callback) {
+ callback.onWriteComplete();
+ }
+
+ @Override
+ public void initialize(OnWriteCompleteCallback callback) {
+ callback.onWriteComplete();
+ }
+
+ @Override
+ public void invalidate(String key, boolean fullExpire, OnWriteCompleteCallback callback) {
+ callback.onWriteComplete();
+ }
+
+ @Override
+ public void remove(String key, OnWriteCompleteCallback callback) {
+ callback.onWriteComplete();
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/UrlRewriter.java b/src/main/java/com/android/volley/toolbox/UrlRewriter.java
new file mode 100644
index 0000000..8bbb770
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/UrlRewriter.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import androidx.annotation.Nullable;
+
+/** An interface for transforming URLs before use. */
+public interface UrlRewriter {
+ /**
+ * Returns a URL to use instead of the provided one, or null to indicate this URL should not be
+ * used at all.
+ */
+ @Nullable
+ String rewriteUrl(String originalUrl);
+}