diff options
Diffstat (limited to 'src/main/java/com/android/volley')
19 files changed, 2586 insertions, 346 deletions
diff --git a/src/main/java/com/android/volley/AsyncCache.java b/src/main/java/com/android/volley/AsyncCache.java new file mode 100644 index 0000000..3cddb4b --- /dev/null +++ b/src/main/java/com/android/volley/AsyncCache.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import androidx.annotation.Nullable; + +/** Asynchronous equivalent to the {@link Cache} interface. */ +public abstract class AsyncCache { + + public interface OnGetCompleteCallback { + /** + * Invoked when the read from the cache is complete. + * + * @param entry The entry read from the cache, or null if the read failed or the key did not + * exist in the cache. + */ + void onGetComplete(@Nullable Cache.Entry entry); + } + + /** + * Retrieves an entry from the cache and sends it back through the {@link + * OnGetCompleteCallback#onGetComplete} function + * + * @param key Cache key + * @param callback Callback that will be notified when the information has been retrieved + */ + public abstract void get(String key, OnGetCompleteCallback callback); + + public interface OnWriteCompleteCallback { + /** Invoked when the cache operation is complete */ + void onWriteComplete(); + } + + /** + * Writes a {@link Cache.Entry} to the cache, and calls {@link + * OnWriteCompleteCallback#onWriteComplete} after the operation is finished. + * + * @param key Cache key + * @param entry The entry to be written to the cache + * @param callback Callback that will be notified when the information has been written + */ + public abstract void put(String key, Cache.Entry entry, OnWriteCompleteCallback callback); + + /** + * Clears the cache. Deletes all cached files from disk. Calls {@link + * OnWriteCompleteCallback#onWriteComplete} after the operation is finished. + */ + public abstract void clear(OnWriteCompleteCallback callback); + + /** + * Initializes the cache and calls {@link OnWriteCompleteCallback#onWriteComplete} after the + * operation is finished. + */ + public abstract void initialize(OnWriteCompleteCallback callback); + + /** + * Invalidates an entry in the cache and calls {@link OnWriteCompleteCallback#onWriteComplete} + * after the operation is finished. + * + * @param key Cache key + * @param fullExpire True to fully expire the entry, false to soft expire + * @param callback Callback that's invoked once the entry has been invalidated + */ + public abstract void invalidate( + String key, boolean fullExpire, OnWriteCompleteCallback callback); + + /** + * Removes a {@link Cache.Entry} from the cache, and calls {@link + * OnWriteCompleteCallback#onWriteComplete} after the operation is finished. + * + * @param key Cache key + * @param callback Callback that's invoked once the entry has been removed + */ + public abstract void remove(String key, OnWriteCompleteCallback callback); +} diff --git a/src/main/java/com/android/volley/AsyncNetwork.java b/src/main/java/com/android/volley/AsyncNetwork.java new file mode 100644 index 0000000..ad19c03 --- /dev/null +++ b/src/main/java/com/android/volley/AsyncNetwork.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import androidx.annotation.RestrictTo; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +/** An asynchronous implementation of {@link Network} to perform requests. */ +public abstract class AsyncNetwork implements Network { + private ExecutorService mBlockingExecutor; + private ExecutorService mNonBlockingExecutor; + private ScheduledExecutorService mNonBlockingScheduledExecutor; + + protected AsyncNetwork() {} + + /** Interface for callback to be called after request is processed. */ + public interface OnRequestComplete { + /** Method to be called after successful network request. */ + void onSuccess(NetworkResponse networkResponse); + + /** Method to be called after unsuccessful network request. */ + void onError(VolleyError volleyError); + } + + /** + * Non-blocking method to perform the specified request. + * + * @param request Request to process + * @param callback to be called once NetworkResponse is received + */ + public abstract void performRequest(Request<?> request, OnRequestComplete callback); + + /** + * Blocking method to perform network request. + * + * @param request Request to process + * @return response retrieved from the network + * @throws VolleyError in the event of an error + */ + @Override + public NetworkResponse performRequest(Request<?> request) throws VolleyError { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference<NetworkResponse> response = new AtomicReference<>(); + final AtomicReference<VolleyError> error = new AtomicReference<>(); + performRequest( + request, + new OnRequestComplete() { + @Override + public void onSuccess(NetworkResponse networkResponse) { + response.set(networkResponse); + latch.countDown(); + } + + @Override + public void onError(VolleyError volleyError) { + error.set(volleyError); + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException e) { + VolleyLog.e(e, "while waiting for CountDownLatch"); + Thread.currentThread().interrupt(); + throw new VolleyError(e); + } + + if (response.get() != null) { + return response.get(); + } else if (error.get() != null) { + throw error.get(); + } else { + throw new VolleyError("Neither response entry was set"); + } + } + + /** + * This method sets the non blocking executor to be used by the network for non-blocking tasks. + * + * <p>This method must be called before performing any requests. + */ + @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP}) + public void setNonBlockingExecutor(ExecutorService executor) { + mNonBlockingExecutor = executor; + } + + /** + * This method sets the blocking executor to be used by the network for potentially blocking + * tasks. + * + * <p>This method must be called before performing any requests. + */ + @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP}) + public void setBlockingExecutor(ExecutorService executor) { + mBlockingExecutor = executor; + } + + /** + * This method sets the scheduled executor to be used by the network for non-blocking tasks to + * be scheduled. + * + * <p>This method must be called before performing any requests. + */ + @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP}) + public void setNonBlockingScheduledExecutor(ScheduledExecutorService executor) { + mNonBlockingScheduledExecutor = executor; + } + + /** Gets blocking executor to perform any potentially blocking tasks. */ + protected ExecutorService getBlockingExecutor() { + return mBlockingExecutor; + } + + /** Gets non-blocking executor to perform any non-blocking tasks. */ + protected ExecutorService getNonBlockingExecutor() { + return mNonBlockingExecutor; + } + + /** Gets scheduled executor to perform any non-blocking tasks that need to be scheduled. */ + protected ScheduledExecutorService getNonBlockingScheduledExecutor() { + return mNonBlockingScheduledExecutor; + } +} diff --git a/src/main/java/com/android/volley/AsyncRequestQueue.java b/src/main/java/com/android/volley/AsyncRequestQueue.java new file mode 100644 index 0000000..3754866 --- /dev/null +++ b/src/main/java/com/android/volley/AsyncRequestQueue.java @@ -0,0 +1,626 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.android.volley.AsyncCache.OnGetCompleteCallback; +import com.android.volley.AsyncNetwork.OnRequestComplete; +import com.android.volley.Cache.Entry; +import java.net.HttpURLConnection; +import java.util.Comparator; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * An asynchronous request dispatch queue. + * + * <p>Add requests to the queue with {@link #add(Request)}. Once completed, responses will be + * delivered on the main thread (unless a custom {@link ResponseDelivery} has been provided) + */ +public class AsyncRequestQueue extends RequestQueue { + /** Default number of blocking threads to start. */ + private static final int DEFAULT_BLOCKING_THREAD_POOL_SIZE = 4; + + /** + * AsyncCache used to retrieve and store responses. + * + * <p>{@code null} indicates use of blocking Cache. + */ + @Nullable private final AsyncCache mAsyncCache; + + /** AsyncNetwork used to perform nework requests. */ + private final AsyncNetwork mNetwork; + + /** Executor for non-blocking tasks. */ + private ExecutorService mNonBlockingExecutor; + + /** Executor to be used for non-blocking tasks that need to be scheduled. */ + private ScheduledExecutorService mNonBlockingScheduledExecutor; + + /** + * Executor for blocking tasks. + * + * <p>Some tasks in handling requests may not be easy to implement in a non-blocking way, such + * as reading or parsing the response data. This executor is used to run these tasks. + */ + private ExecutorService mBlockingExecutor; + + /** + * This interface may be used by advanced applications to provide custom executors according to + * their needs. Apps must create ExecutorServices dynamically given a blocking queue rather than + * providing them directly so that Volley can provide a PriorityQueue which will prioritize + * requests according to Request#getPriority. + */ + private ExecutorFactory mExecutorFactory; + + /** Manage list of waiting requests and de-duplicate requests with same cache key. */ + private final WaitingRequestManager mWaitingRequestManager = new WaitingRequestManager(this); + + /** + * Sets all the variables, but processing does not begin until {@link #start()} is called. + * + * @param cache to use for persisting responses to disk. If an AsyncCache was provided, then + * this will be a {@link ThrowingCache} + * @param network to perform HTTP requests + * @param asyncCache to use for persisting responses to disk. May be null to indicate use of + * blocking cache + * @param responseDelivery interface for posting responses and errors + * @param executorFactory Interface to be used to provide custom executors according to the + * users needs. + */ + private AsyncRequestQueue( + Cache cache, + AsyncNetwork network, + @Nullable AsyncCache asyncCache, + ResponseDelivery responseDelivery, + ExecutorFactory executorFactory) { + super(cache, network, /* threadPoolSize= */ 0, responseDelivery); + mAsyncCache = asyncCache; + mNetwork = network; + mExecutorFactory = executorFactory; + } + + /** Sets the executors and initializes the cache. */ + @Override + public void start() { + stop(); // Make sure any currently running threads are stopped + + // Create blocking / non-blocking executors and set them in the network and stack. + mNonBlockingExecutor = mExecutorFactory.createNonBlockingExecutor(getBlockingQueue()); + mBlockingExecutor = mExecutorFactory.createBlockingExecutor(getBlockingQueue()); + mNonBlockingScheduledExecutor = mExecutorFactory.createNonBlockingScheduledExecutor(); + mNetwork.setBlockingExecutor(mBlockingExecutor); + mNetwork.setNonBlockingExecutor(mNonBlockingExecutor); + mNetwork.setNonBlockingScheduledExecutor(mNonBlockingScheduledExecutor); + + mNonBlockingExecutor.execute( + new Runnable() { + @Override + public void run() { + // This is intentionally blocking, because we don't want to process any + // requests until the cache is initialized. + if (mAsyncCache != null) { + final CountDownLatch latch = new CountDownLatch(1); + mAsyncCache.initialize( + new AsyncCache.OnWriteCompleteCallback() { + @Override + public void onWriteComplete() { + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException e) { + VolleyLog.e( + e, "Thread was interrupted while initializing the cache."); + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } else { + getCache().initialize(); + } + } + }); + } + + /** Shuts down and nullifies both executors */ + @Override + public void stop() { + if (mNonBlockingExecutor != null) { + mNonBlockingExecutor.shutdownNow(); + mNonBlockingExecutor = null; + } + if (mBlockingExecutor != null) { + mBlockingExecutor.shutdownNow(); + mBlockingExecutor = null; + } + if (mNonBlockingScheduledExecutor != null) { + mNonBlockingScheduledExecutor.shutdownNow(); + mNonBlockingScheduledExecutor = null; + } + } + + /** Begins the request by sending it to the Cache or Network. */ + @Override + <T> void beginRequest(Request<T> request) { + // If the request is uncacheable, send it over the network. + if (request.shouldCache()) { + if (mAsyncCache != null) { + mNonBlockingExecutor.execute(new CacheTask<>(request)); + } else { + mBlockingExecutor.execute(new CacheTask<>(request)); + } + } else { + sendRequestOverNetwork(request); + } + } + + @Override + <T> void sendRequestOverNetwork(Request<T> request) { + mNonBlockingExecutor.execute(new NetworkTask<>(request)); + } + + /** Runnable that gets an entry from the cache. */ + private class CacheTask<T> extends RequestTask<T> { + CacheTask(Request<T> request) { + super(request); + } + + @Override + public void run() { + // If the request has been canceled, don't bother dispatching it. + if (mRequest.isCanceled()) { + mRequest.finish("cache-discard-canceled"); + return; + } + + mRequest.addMarker("cache-queue-take"); + + // Attempt to retrieve this item from cache. + if (mAsyncCache != null) { + mAsyncCache.get( + mRequest.getCacheKey(), + new OnGetCompleteCallback() { + @Override + public void onGetComplete(Entry entry) { + handleEntry(entry, mRequest); + } + }); + } else { + Entry entry = getCache().get(mRequest.getCacheKey()); + handleEntry(entry, mRequest); + } + } + } + + /** Helper method that handles the cache entry after getting it from the Cache. */ + private void handleEntry(final Entry entry, final Request<?> mRequest) { + if (entry == null) { + mRequest.addMarker("cache-miss"); + // Cache miss; send off to the network dispatcher. + if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) { + sendRequestOverNetwork(mRequest); + } + return; + } + + // If it is completely expired, just send it to the network. + if (entry.isExpired()) { + mRequest.addMarker("cache-hit-expired"); + mRequest.setCacheEntry(entry); + if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) { + sendRequestOverNetwork(mRequest); + } + return; + } + + // We have a cache hit; parse its data for delivery back to the request. + mBlockingExecutor.execute(new CacheParseTask<>(mRequest, entry)); + } + + private class CacheParseTask<T> extends RequestTask<T> { + Cache.Entry entry; + + CacheParseTask(Request<T> request, Cache.Entry entry) { + super(request); + this.entry = entry; + } + + @Override + public void run() { + mRequest.addMarker("cache-hit"); + Response<?> response = + mRequest.parseNetworkResponse( + new NetworkResponse( + HttpURLConnection.HTTP_OK, + entry.data, + /* notModified= */ false, + /* networkTimeMs= */ 0, + entry.allResponseHeaders)); + mRequest.addMarker("cache-hit-parsed"); + + if (!entry.refreshNeeded()) { + // Completely unexpired cache hit. Just deliver the response. + getResponseDelivery().postResponse(mRequest, response); + } else { + // Soft-expired cache hit. We can deliver the cached response, + // but we need to also send the request to the network for + // refreshing. + mRequest.addMarker("cache-hit-refresh-needed"); + mRequest.setCacheEntry(entry); + // Mark the response as intermediate. + response.intermediate = true; + + if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) { + // Post the intermediate response back to the user and have + // the delivery then forward the request along to the network. + getResponseDelivery() + .postResponse( + mRequest, + response, + new Runnable() { + @Override + public void run() { + sendRequestOverNetwork(mRequest); + } + }); + } else { + // request has been added to list of waiting requests + // to receive the network response from the first request once it + // returns. + getResponseDelivery().postResponse(mRequest, response); + } + } + } + } + + private class ParseErrorTask<T> extends RequestTask<T> { + VolleyError volleyError; + + ParseErrorTask(Request<T> request, VolleyError volleyError) { + super(request); + this.volleyError = volleyError; + } + + @Override + public void run() { + VolleyError parsedError = mRequest.parseNetworkError(volleyError); + getResponseDelivery().postError(mRequest, parsedError); + mRequest.notifyListenerResponseNotUsable(); + } + } + + /** Runnable that performs the network request */ + private class NetworkTask<T> extends RequestTask<T> { + NetworkTask(Request<T> request) { + super(request); + } + + @Override + public void run() { + // If the request was cancelled already, do not perform the network request. + if (mRequest.isCanceled()) { + mRequest.finish("network-discard-cancelled"); + mRequest.notifyListenerResponseNotUsable(); + return; + } + + final long startTimeMs = SystemClock.elapsedRealtime(); + mRequest.addMarker("network-queue-take"); + + // TODO: Figure out what to do with traffic stats tags. Can this be pushed to the + // HTTP stack, or is it no longer feasible to support? + + // Perform the network request. + mNetwork.performRequest( + mRequest, + new OnRequestComplete() { + @Override + public void onSuccess(final NetworkResponse networkResponse) { + mRequest.addMarker("network-http-complete"); + + // If the server returned 304 AND we delivered a response already, + // we're done -- don't deliver a second identical response. + if (networkResponse.notModified && mRequest.hasHadResponseDelivered()) { + mRequest.finish("not-modified"); + mRequest.notifyListenerResponseNotUsable(); + return; + } + + // Parse the response here on the worker thread. + mBlockingExecutor.execute( + new NetworkParseTask<>(mRequest, networkResponse)); + } + + @Override + public void onError(final VolleyError volleyError) { + volleyError.setNetworkTimeMs( + SystemClock.elapsedRealtime() - startTimeMs); + mBlockingExecutor.execute(new ParseErrorTask<>(mRequest, volleyError)); + } + }); + } + } + + /** Runnable that parses a network response. */ + private class NetworkParseTask<T> extends RequestTask<T> { + NetworkResponse networkResponse; + + NetworkParseTask(Request<T> request, NetworkResponse networkResponse) { + super(request); + this.networkResponse = networkResponse; + } + + @Override + public void run() { + final Response<?> response = mRequest.parseNetworkResponse(networkResponse); + mRequest.addMarker("network-parse-complete"); + + // Write to cache if applicable. + // TODO: Only update cache metadata instead of entire + // record for 304s. + if (mRequest.shouldCache() && response.cacheEntry != null) { + if (mAsyncCache != null) { + mNonBlockingExecutor.execute(new CachePutTask<>(mRequest, response)); + } else { + mBlockingExecutor.execute(new CachePutTask<>(mRequest, response)); + } + } else { + finishRequest(mRequest, response, /* cached= */ false); + } + } + } + + private class CachePutTask<T> extends RequestTask<T> { + Response<?> response; + + CachePutTask(Request<T> request, Response<?> response) { + super(request); + this.response = response; + } + + @Override + public void run() { + if (mAsyncCache != null) { + mAsyncCache.put( + mRequest.getCacheKey(), + response.cacheEntry, + new AsyncCache.OnWriteCompleteCallback() { + @Override + public void onWriteComplete() { + finishRequest(mRequest, response, /* cached= */ true); + } + }); + } else { + getCache().put(mRequest.getCacheKey(), response.cacheEntry); + finishRequest(mRequest, response, /* cached= */ true); + } + } + } + + /** Posts response and notifies listener */ + private void finishRequest(Request<?> mRequest, Response<?> response, boolean cached) { + if (cached) { + mRequest.addMarker("network-cache-written"); + } + // Post the response back. + mRequest.markDelivered(); + getResponseDelivery().postResponse(mRequest, response); + mRequest.notifyListenerResponseReceived(response); + } + + /** + * This class may be used by advanced applications to provide custom executors according to + * their needs. Apps must create ExecutorServices dynamically given a blocking queue rather than + * providing them directly so that Volley can provide a PriorityQueue which will prioritize + * requests according to Request#getPriority. + */ + public abstract static class ExecutorFactory { + abstract ExecutorService createNonBlockingExecutor(BlockingQueue<Runnable> taskQueue); + + abstract ExecutorService createBlockingExecutor(BlockingQueue<Runnable> taskQueue); + + abstract ScheduledExecutorService createNonBlockingScheduledExecutor(); + } + + /** Provides a BlockingQueue to be used to create executors. */ + private static PriorityBlockingQueue<Runnable> getBlockingQueue() { + return new PriorityBlockingQueue<>( + /* initialCapacity= */ 11, + new Comparator<Runnable>() { + @Override + public int compare(Runnable r1, Runnable r2) { + // Vanilla runnables are prioritized first, then RequestTasks are ordered + // by the underlying Request. + if (r1 instanceof RequestTask) { + if (r2 instanceof RequestTask) { + return ((RequestTask<?>) r1).compareTo(((RequestTask<?>) r2)); + } + return 1; + } + return r2 instanceof RequestTask ? -1 : 0; + } + }); + } + + /** + * Builder is used to build an instance of {@link AsyncRequestQueue} from values configured by + * the setters. + */ + public static class Builder { + @Nullable private AsyncCache mAsyncCache = null; + private final AsyncNetwork mNetwork; + @Nullable private Cache mCache = null; + @Nullable private ExecutorFactory mExecutorFactory = null; + @Nullable private ResponseDelivery mResponseDelivery = null; + + public Builder(AsyncNetwork asyncNetwork) { + if (asyncNetwork == null) { + throw new IllegalArgumentException("Network cannot be null"); + } + mNetwork = asyncNetwork; + } + + /** + * Sets the executor factory to be used by the AsyncRequestQueue. If this is not called, + * Volley will create suitable private thread pools. + */ + public Builder setExecutorFactory(ExecutorFactory executorFactory) { + mExecutorFactory = executorFactory; + return this; + } + + /** + * Sets the response deliver to be used by the AsyncRequestQueue. If this is not called, we + * will default to creating a new {@link ExecutorDelivery} with the application's main + * thread. + */ + public Builder setResponseDelivery(ResponseDelivery responseDelivery) { + mResponseDelivery = responseDelivery; + return this; + } + + /** Sets the AsyncCache to be used by the AsyncRequestQueue. */ + public Builder setAsyncCache(AsyncCache asyncCache) { + mAsyncCache = asyncCache; + return this; + } + + /** Sets the Cache to be used by the AsyncRequestQueue. */ + public Builder setCache(Cache cache) { + mCache = cache; + return this; + } + + /** Provides a default ExecutorFactory to use, if one is never set. */ + private ExecutorFactory getDefaultExecutorFactory() { + return new ExecutorFactory() { + @Override + public ExecutorService createNonBlockingExecutor( + BlockingQueue<Runnable> taskQueue) { + return getNewThreadPoolExecutor( + /* maximumPoolSize= */ 1, + /* threadNameSuffix= */ "Non-BlockingExecutor", + taskQueue); + } + + @Override + public ExecutorService createBlockingExecutor(BlockingQueue<Runnable> taskQueue) { + return getNewThreadPoolExecutor( + /* maximumPoolSize= */ DEFAULT_BLOCKING_THREAD_POOL_SIZE, + /* threadNameSuffix= */ "BlockingExecutor", + taskQueue); + } + + @Override + public ScheduledExecutorService createNonBlockingScheduledExecutor() { + return new ScheduledThreadPoolExecutor( + /* corePoolSize= */ 0, getThreadFactory("ScheduledExecutor")); + } + + private ThreadPoolExecutor getNewThreadPoolExecutor( + int maximumPoolSize, + final String threadNameSuffix, + BlockingQueue<Runnable> taskQueue) { + return new ThreadPoolExecutor( + /* corePoolSize= */ 0, + /* maximumPoolSize= */ maximumPoolSize, + /* keepAliveTime= */ 60, + /* unit= */ TimeUnit.SECONDS, + taskQueue, + getThreadFactory(threadNameSuffix)); + } + + private ThreadFactory getThreadFactory(final String threadNameSuffix) { + return new ThreadFactory() { + @Override + public Thread newThread(@NonNull Runnable runnable) { + Thread t = Executors.defaultThreadFactory().newThread(runnable); + t.setName("Volley-" + threadNameSuffix); + return t; + } + }; + } + }; + } + + public AsyncRequestQueue build() { + // If neither cache is set by the caller, throw an illegal argument exception. + if (mCache == null && mAsyncCache == null) { + throw new IllegalArgumentException("You must set one of the cache objects"); + } + if (mCache == null) { + // if no cache is provided, we will provide one that throws + // UnsupportedOperationExceptions to pass into the parent class. + mCache = new ThrowingCache(); + } + if (mResponseDelivery == null) { + mResponseDelivery = new ExecutorDelivery(new Handler(Looper.getMainLooper())); + } + if (mExecutorFactory == null) { + mExecutorFactory = getDefaultExecutorFactory(); + } + return new AsyncRequestQueue( + mCache, mNetwork, mAsyncCache, mResponseDelivery, mExecutorFactory); + } + } + + /** A cache that throws an error if a method is called. */ + private static class ThrowingCache implements Cache { + @Override + public Entry get(String key) { + throw new UnsupportedOperationException(); + } + + @Override + public void put(String key, Entry entry) { + throw new UnsupportedOperationException(); + } + + @Override + public void initialize() { + throw new UnsupportedOperationException(); + } + + @Override + public void invalidate(String key, boolean fullExpire) { + throw new UnsupportedOperationException(); + } + + @Override + public void remove(String key) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/main/java/com/android/volley/CacheDispatcher.java b/src/main/java/com/android/volley/CacheDispatcher.java index 12b1035..1bfc0ea 100644 --- a/src/main/java/com/android/volley/CacheDispatcher.java +++ b/src/main/java/com/android/volley/CacheDispatcher.java @@ -18,10 +18,6 @@ package com.android.volley; import android.os.Process; import androidx.annotation.VisibleForTesting; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.concurrent.BlockingQueue; /** @@ -72,7 +68,7 @@ public class CacheDispatcher extends Thread { mNetworkQueue = networkQueue; mCache = cache; mDelivery = delivery; - mWaitingRequestManager = new WaitingRequestManager(this); + mWaitingRequestManager = new WaitingRequestManager(this, networkQueue, delivery); } /** @@ -207,113 +203,4 @@ public class CacheDispatcher extends Thread { request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED); } } - - private static class WaitingRequestManager implements Request.NetworkRequestCompleteListener { - - /** - * Staging area for requests that already have a duplicate request in flight. - * - * <ul> - * <li>containsKey(cacheKey) indicates that there is a request in flight for the given - * cache key. - * <li>get(cacheKey) returns waiting requests for the given cache key. The in flight - * request is <em>not</em> contained in that list. Is null if no requests are staged. - * </ul> - */ - private final Map<String, List<Request<?>>> mWaitingRequests = new HashMap<>(); - - private final CacheDispatcher mCacheDispatcher; - - WaitingRequestManager(CacheDispatcher cacheDispatcher) { - mCacheDispatcher = cacheDispatcher; - } - - /** Request received a valid response that can be used by other waiting requests. */ - @Override - public void onResponseReceived(Request<?> request, Response<?> response) { - if (response.cacheEntry == null || response.cacheEntry.isExpired()) { - onNoUsableResponseReceived(request); - return; - } - String cacheKey = request.getCacheKey(); - List<Request<?>> waitingRequests; - synchronized (this) { - waitingRequests = mWaitingRequests.remove(cacheKey); - } - if (waitingRequests != null) { - if (VolleyLog.DEBUG) { - VolleyLog.v( - "Releasing %d waiting requests for cacheKey=%s.", - waitingRequests.size(), cacheKey); - } - // Process all queued up requests. - for (Request<?> waiting : waitingRequests) { - mCacheDispatcher.mDelivery.postResponse(waiting, response); - } - } - } - - /** No valid response received from network, release waiting requests. */ - @Override - public synchronized void onNoUsableResponseReceived(Request<?> request) { - String cacheKey = request.getCacheKey(); - List<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey); - if (waitingRequests != null && !waitingRequests.isEmpty()) { - if (VolleyLog.DEBUG) { - VolleyLog.v( - "%d waiting requests for cacheKey=%s; resend to network", - waitingRequests.size(), cacheKey); - } - Request<?> nextInLine = waitingRequests.remove(0); - mWaitingRequests.put(cacheKey, waitingRequests); - nextInLine.setNetworkRequestCompleteListener(this); - try { - mCacheDispatcher.mNetworkQueue.put(nextInLine); - } catch (InterruptedException iex) { - VolleyLog.e("Couldn't add request to queue. %s", iex.toString()); - // Restore the interrupted status of the calling thread (i.e. NetworkDispatcher) - Thread.currentThread().interrupt(); - // Quit the current CacheDispatcher thread. - mCacheDispatcher.quit(); - } - } - } - - /** - * For cacheable requests, if a request for the same cache key is already in flight, add it - * to a queue to wait for that in-flight request to finish. - * - * @return whether the request was queued. If false, we should continue issuing the request - * over the network. If true, we should put the request on hold to be processed when the - * in-flight request finishes. - */ - private synchronized boolean maybeAddToWaitingRequests(Request<?> request) { - String cacheKey = request.getCacheKey(); - // Insert request into stage if there's already a request with the same cache key - // in flight. - if (mWaitingRequests.containsKey(cacheKey)) { - // There is already a request in flight. Queue up. - List<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey); - if (stagedRequests == null) { - stagedRequests = new ArrayList<>(); - } - request.addMarker("waiting-for-response"); - stagedRequests.add(request); - mWaitingRequests.put(cacheKey, stagedRequests); - if (VolleyLog.DEBUG) { - VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey); - } - return true; - } else { - // Insert 'null' queue for this cacheKey, indicating there is now a request in - // flight. - mWaitingRequests.put(cacheKey, null); - request.setNetworkRequestCompleteListener(this); - if (VolleyLog.DEBUG) { - VolleyLog.d("new request, sending to network %s", cacheKey); - } - return false; - } - } - } } diff --git a/src/main/java/com/android/volley/Request.java b/src/main/java/com/android/volley/Request.java index 2b53f96..b60dc74 100644 --- a/src/main/java/com/android/volley/Request.java +++ b/src/main/java/com/android/volley/Request.java @@ -107,6 +107,9 @@ public abstract class Request<T> implements Comparable<Request<T>> { /** Whether the request should be retried in the event of an HTTP 5xx (server) error. */ private boolean mShouldRetryServerErrors = false; + /** Whether the request should be retried in the event of a {@link NoConnectionError}. */ + private boolean mShouldRetryConnectionErrors = false; + /** The retry policy for this request. */ private RetryPolicy mRetryPolicy; @@ -534,6 +537,25 @@ public abstract class Request<T> implements Comparable<Request<T>> { } /** + * Sets whether or not the request should be retried in the event that no connection could be + * established. + * + * @return This Request object to allow for chaining. + */ + public final Request<?> setShouldRetryConnectionErrors(boolean shouldRetryConnectionErrors) { + mShouldRetryConnectionErrors = shouldRetryConnectionErrors; + return this; + } + + /** + * Returns true if this request should be retried in the event that no connection could be + * established. + */ + public final boolean shouldRetryConnectionErrors() { + return mShouldRetryConnectionErrors; + } + + /** * Priority values. Requests will be processed from higher priorities to lower priorities, in * FIFO order. */ diff --git a/src/main/java/com/android/volley/RequestQueue.java b/src/main/java/com/android/volley/RequestQueue.java index c127c7f..6db0b1c 100644 --- a/src/main/java/com/android/volley/RequestQueue.java +++ b/src/main/java/com/android/volley/RequestQueue.java @@ -263,13 +263,17 @@ public class RequestQueue { request.addMarker("add-to-queue"); sendRequestEvent(request, RequestEvent.REQUEST_QUEUED); + beginRequest(request); + return request; + } + + <T> void beginRequest(Request<T> request) { // If the request is uncacheable, skip the cache queue and go straight to the network. if (!request.shouldCache()) { - mNetworkQueue.add(request); - return request; + sendRequestOverNetwork(request); + } else { + mCacheQueue.add(request); } - mCacheQueue.add(request); - return request; } /** @@ -327,4 +331,12 @@ public class RequestQueue { mFinishedListeners.remove(listener); } } + + public ResponseDelivery getResponseDelivery() { + return mDelivery; + } + + <T> void sendRequestOverNetwork(Request<T> request) { + mNetworkQueue.add(request); + } } diff --git a/src/main/java/com/android/volley/RequestTask.java b/src/main/java/com/android/volley/RequestTask.java new file mode 100644 index 0000000..8eeaf2c --- /dev/null +++ b/src/main/java/com/android/volley/RequestTask.java @@ -0,0 +1,15 @@ +package com.android.volley; + +/** Abstract runnable that's a task to be completed by the RequestQueue. */ +public abstract class RequestTask<T> implements Runnable { + final Request<T> mRequest; + + public RequestTask(Request<T> request) { + mRequest = request; + } + + @SuppressWarnings("unchecked") + public int compareTo(RequestTask<?> other) { + return mRequest.compareTo((Request<T>) other.mRequest); + } +} diff --git a/src/main/java/com/android/volley/WaitingRequestManager.java b/src/main/java/com/android/volley/WaitingRequestManager.java new file mode 100644 index 0000000..682e339 --- /dev/null +++ b/src/main/java/com/android/volley/WaitingRequestManager.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; + +/** + * Callback to notify the caller when the network request returns. Valid responses can be used by + * all duplicate requests. + */ +class WaitingRequestManager implements Request.NetworkRequestCompleteListener { + + /** + * Staging area for requests that already have a duplicate request in flight. + * + * <ul> + * <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache + * key. + * <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request + * is <em>not</em> contained in that list. Is null if no requests are staged. + * </ul> + */ + private final Map<String, List<Request<?>>> mWaitingRequests = new HashMap<>(); + + private final ResponseDelivery mResponseDelivery; + + /** + * RequestQueue that is passed in by the AsyncRequestQueue. This is null when this instance is + * initialized by the {@link CacheDispatcher} + */ + @Nullable private final RequestQueue mRequestQueue; + + /** + * CacheDispacter that is passed in by the CacheDispatcher. This is null when this instance is + * initialized by the {@link AsyncRequestQueue} + */ + @Nullable private final CacheDispatcher mCacheDispatcher; + + /** + * BlockingQueue that is passed in by the CacheDispatcher. This is null when this instance is + * initialized by the {@link AsyncRequestQueue} + */ + @Nullable private final BlockingQueue<Request<?>> mNetworkQueue; + + WaitingRequestManager(@NonNull RequestQueue requestQueue) { + mRequestQueue = requestQueue; + mResponseDelivery = mRequestQueue.getResponseDelivery(); + mCacheDispatcher = null; + mNetworkQueue = null; + } + + WaitingRequestManager( + @NonNull CacheDispatcher cacheDispatcher, + @NonNull BlockingQueue<Request<?>> networkQueue, + ResponseDelivery responseDelivery) { + mRequestQueue = null; + mResponseDelivery = responseDelivery; + mCacheDispatcher = cacheDispatcher; + mNetworkQueue = networkQueue; + } + + /** Request received a valid response that can be used by other waiting requests. */ + @Override + public void onResponseReceived(Request<?> request, Response<?> response) { + if (response.cacheEntry == null || response.cacheEntry.isExpired()) { + onNoUsableResponseReceived(request); + return; + } + String cacheKey = request.getCacheKey(); + List<Request<?>> waitingRequests; + synchronized (this) { + waitingRequests = mWaitingRequests.remove(cacheKey); + } + if (waitingRequests != null) { + if (VolleyLog.DEBUG) { + VolleyLog.v( + "Releasing %d waiting requests for cacheKey=%s.", + waitingRequests.size(), cacheKey); + } + // Process all queued up requests. + for (Request<?> waiting : waitingRequests) { + mResponseDelivery.postResponse(waiting, response); + } + } + } + + /** No valid response received from network, release waiting requests. */ + @Override + public synchronized void onNoUsableResponseReceived(Request<?> request) { + String cacheKey = request.getCacheKey(); + List<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey); + if (waitingRequests != null && !waitingRequests.isEmpty()) { + if (VolleyLog.DEBUG) { + VolleyLog.v( + "%d waiting requests for cacheKey=%s; resend to network", + waitingRequests.size(), cacheKey); + } + Request<?> nextInLine = waitingRequests.remove(0); + mWaitingRequests.put(cacheKey, waitingRequests); + nextInLine.setNetworkRequestCompleteListener(this); + // RequestQueue will be non-null if this instance was created in AsyncRequestQueue. + if (mRequestQueue != null) { + // Will send the network request from the RequestQueue. + mRequestQueue.sendRequestOverNetwork(nextInLine); + } else if (mCacheDispatcher != null && mNetworkQueue != null) { + // If we're not using the AsyncRequestQueue, then submit it to the network queue. + try { + mNetworkQueue.put(nextInLine); + } catch (InterruptedException iex) { + VolleyLog.e("Couldn't add request to queue. %s", iex.toString()); + // Restore the interrupted status of the calling thread (i.e. NetworkDispatcher) + Thread.currentThread().interrupt(); + // Quit the current CacheDispatcher thread. + mCacheDispatcher.quit(); + } + } + } + } + + /** + * For cacheable requests, if a request for the same cache key is already in flight, add it to a + * queue to wait for that in-flight request to finish. + * + * @return whether the request was queued. If false, we should continue issuing the request over + * the network. If true, we should put the request on hold to be processed when the + * in-flight request finishes. + */ + synchronized boolean maybeAddToWaitingRequests(Request<?> request) { + String cacheKey = request.getCacheKey(); + // Insert request into stage if there's already a request with the same cache key + // in flight. + if (mWaitingRequests.containsKey(cacheKey)) { + // There is already a request in flight. Queue up. + List<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey); + if (stagedRequests == null) { + stagedRequests = new ArrayList<>(); + } + request.addMarker("waiting-for-response"); + stagedRequests.add(request); + mWaitingRequests.put(cacheKey, stagedRequests); + if (VolleyLog.DEBUG) { + VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey); + } + return true; + } else { + // Insert 'null' queue for this cacheKey, indicating there is now a request in + // flight. + mWaitingRequests.put(cacheKey, null); + request.setNetworkRequestCompleteListener(this); + if (VolleyLog.DEBUG) { + VolleyLog.d("new request, sending to network %s", cacheKey); + } + return false; + } + } +} diff --git a/src/main/java/com/android/volley/cronet/CronetHttpStack.java b/src/main/java/com/android/volley/cronet/CronetHttpStack.java new file mode 100644 index 0000000..f3baace --- /dev/null +++ b/src/main/java/com/android/volley/cronet/CronetHttpStack.java @@ -0,0 +1,631 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.cronet; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Base64; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.android.volley.AuthFailureError; +import com.android.volley.Header; +import com.android.volley.Request; +import com.android.volley.RequestTask; +import com.android.volley.VolleyLog; +import com.android.volley.toolbox.AsyncHttpStack; +import com.android.volley.toolbox.ByteArrayPool; +import com.android.volley.toolbox.HttpHeaderParser; +import com.android.volley.toolbox.HttpResponse; +import com.android.volley.toolbox.PoolingByteArrayOutputStream; +import com.android.volley.toolbox.UrlRewriter; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetException; +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UploadDataProviders; +import org.chromium.net.UrlRequest; +import org.chromium.net.UrlRequest.Callback; +import org.chromium.net.UrlResponseInfo; + +/** + * A {@link AsyncHttpStack} that's based on Cronet's fully asynchronous API for network requests. + */ +public class CronetHttpStack extends AsyncHttpStack { + + private final CronetEngine mCronetEngine; + private final ByteArrayPool mPool; + private final UrlRewriter mUrlRewriter; + private final RequestListener mRequestListener; + + // cURL logging support + private final boolean mCurlLoggingEnabled; + private final CurlCommandLogger mCurlCommandLogger; + private final boolean mLogAuthTokensInCurlCommands; + + private CronetHttpStack( + CronetEngine cronetEngine, + ByteArrayPool pool, + UrlRewriter urlRewriter, + RequestListener requestListener, + boolean curlLoggingEnabled, + CurlCommandLogger curlCommandLogger, + boolean logAuthTokensInCurlCommands) { + mCronetEngine = cronetEngine; + mPool = pool; + mUrlRewriter = urlRewriter; + mRequestListener = requestListener; + mCurlLoggingEnabled = curlLoggingEnabled; + mCurlCommandLogger = curlCommandLogger; + mLogAuthTokensInCurlCommands = logAuthTokensInCurlCommands; + + mRequestListener.initialize(this); + } + + @Override + public void executeRequest( + final Request<?> request, + final Map<String, String> additionalHeaders, + final OnRequestComplete callback) { + if (getBlockingExecutor() == null || getNonBlockingExecutor() == null) { + throw new IllegalStateException("Must set blocking and non-blocking executors"); + } + final Callback urlCallback = + new Callback() { + PoolingByteArrayOutputStream bytesReceived = null; + WritableByteChannel receiveChannel = null; + + @Override + public void onRedirectReceived( + UrlRequest urlRequest, + UrlResponseInfo urlResponseInfo, + String newLocationUrl) { + urlRequest.followRedirect(); + } + + @Override + public void onResponseStarted( + UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) { + bytesReceived = + new PoolingByteArrayOutputStream( + mPool, getContentLength(urlResponseInfo)); + receiveChannel = Channels.newChannel(bytesReceived); + urlRequest.read(ByteBuffer.allocateDirect(1024)); + } + + @Override + public void onReadCompleted( + UrlRequest urlRequest, + UrlResponseInfo urlResponseInfo, + ByteBuffer byteBuffer) { + byteBuffer.flip(); + try { + receiveChannel.write(byteBuffer); + byteBuffer.clear(); + urlRequest.read(byteBuffer); + } catch (IOException e) { + urlRequest.cancel(); + callback.onError(e); + } + } + + @Override + public void onSucceeded( + UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) { + List<Header> headers = getHeaders(urlResponseInfo.getAllHeadersAsList()); + HttpResponse response = + new HttpResponse( + urlResponseInfo.getHttpStatusCode(), + headers, + bytesReceived.toByteArray()); + callback.onSuccess(response); + } + + @Override + public void onFailed( + UrlRequest urlRequest, + UrlResponseInfo urlResponseInfo, + CronetException e) { + callback.onError(e); + } + }; + + String url = request.getUrl(); + String rewritten = mUrlRewriter.rewriteUrl(url); + if (rewritten == null) { + callback.onError(new IOException("URL blocked by rewriter: " + url)); + return; + } + url = rewritten; + + // We can call allowDirectExecutor here and run directly on the network thread, since all + // the callbacks are non-blocking. + final UrlRequest.Builder builder = + mCronetEngine + .newUrlRequestBuilder(url, urlCallback, getNonBlockingExecutor()) + .allowDirectExecutor() + .disableCache() + .setPriority(getPriority(request)); + // request.getHeaders() may be blocking, so submit it to the blocking executor. + getBlockingExecutor() + .execute( + new SetUpRequestTask<>(request, url, builder, additionalHeaders, callback)); + } + + private class SetUpRequestTask<T> extends RequestTask<T> { + UrlRequest.Builder builder; + String url; + Map<String, String> additionalHeaders; + OnRequestComplete callback; + Request<T> request; + + SetUpRequestTask( + Request<T> request, + String url, + UrlRequest.Builder builder, + Map<String, String> additionalHeaders, + OnRequestComplete callback) { + super(request); + // Note that this URL may be different from Request#getUrl() due to the UrlRewriter. + this.url = url; + this.builder = builder; + this.additionalHeaders = additionalHeaders; + this.callback = callback; + this.request = request; + } + + @Override + public void run() { + try { + mRequestListener.onRequestPrepared(request, builder); + CurlLoggedRequestParameters requestParameters = new CurlLoggedRequestParameters(); + setHttpMethod(requestParameters, request); + setRequestHeaders(requestParameters, request, additionalHeaders); + requestParameters.applyToRequest(builder, getNonBlockingExecutor()); + UrlRequest urlRequest = builder.build(); + if (mCurlLoggingEnabled) { + mCurlCommandLogger.logCurlCommand(generateCurlCommand(url, requestParameters)); + } + urlRequest.start(); + } catch (AuthFailureError authFailureError) { + callback.onAuthError(authFailureError); + } + } + } + + @VisibleForTesting + public static List<Header> getHeaders(List<Map.Entry<String, String>> headersList) { + List<Header> headers = new ArrayList<>(); + for (Map.Entry<String, String> header : headersList) { + headers.add(new Header(header.getKey(), header.getValue())); + } + return headers; + } + + /** Sets the connection parameters for the UrlRequest */ + private void setHttpMethod(CurlLoggedRequestParameters requestParameters, Request<?> request) + throws AuthFailureError { + switch (request.getMethod()) { + case Request.Method.DEPRECATED_GET_OR_POST: + // This is the deprecated way that needs to be handled for backwards compatibility. + // If the request's post body is null, then the assumption is that the request is + // GET. Otherwise, it is assumed that the request is a POST. + byte[] postBody = request.getPostBody(); + if (postBody != null) { + requestParameters.setHttpMethod("POST"); + addBodyIfExists(requestParameters, request.getPostBodyContentType(), postBody); + } else { + requestParameters.setHttpMethod("GET"); + } + break; + case Request.Method.GET: + // Not necessary to set the request method because connection defaults to GET but + // being explicit here. + requestParameters.setHttpMethod("GET"); + break; + case Request.Method.DELETE: + requestParameters.setHttpMethod("DELETE"); + break; + case Request.Method.POST: + requestParameters.setHttpMethod("POST"); + addBodyIfExists(requestParameters, request.getBodyContentType(), request.getBody()); + break; + case Request.Method.PUT: + requestParameters.setHttpMethod("PUT"); + addBodyIfExists(requestParameters, request.getBodyContentType(), request.getBody()); + break; + case Request.Method.HEAD: + requestParameters.setHttpMethod("HEAD"); + break; + case Request.Method.OPTIONS: + requestParameters.setHttpMethod("OPTIONS"); + break; + case Request.Method.TRACE: + requestParameters.setHttpMethod("TRACE"); + break; + case Request.Method.PATCH: + requestParameters.setHttpMethod("PATCH"); + addBodyIfExists(requestParameters, request.getBodyContentType(), request.getBody()); + break; + default: + throw new IllegalStateException("Unknown method type."); + } + } + + /** + * Sets the request headers for the UrlRequest. + * + * @param requestParameters parameters that we are adding the request headers to + * @param request to get the headers from + * @param additionalHeaders for the UrlRequest + * @throws AuthFailureError is thrown if Request#getHeaders throws ones + */ + private void setRequestHeaders( + CurlLoggedRequestParameters requestParameters, + Request<?> request, + Map<String, String> additionalHeaders) + throws AuthFailureError { + requestParameters.putAllHeaders(additionalHeaders); + // Request.getHeaders() takes precedence over the given additional (cache) headers). + requestParameters.putAllHeaders(request.getHeaders()); + } + + /** Sets the UploadDataProvider of the UrlRequest.Builder */ + private void addBodyIfExists( + CurlLoggedRequestParameters requestParameters, + String contentType, + @Nullable byte[] body) { + requestParameters.setBody(contentType, body); + } + + /** Helper method that maps Volley's request priority to Cronet's */ + private int getPriority(Request<?> request) { + switch (request.getPriority()) { + case LOW: + return UrlRequest.Builder.REQUEST_PRIORITY_LOW; + case HIGH: + case IMMEDIATE: + return UrlRequest.Builder.REQUEST_PRIORITY_HIGHEST; + case NORMAL: + default: + return UrlRequest.Builder.REQUEST_PRIORITY_MEDIUM; + } + } + + private int getContentLength(UrlResponseInfo urlResponseInfo) { + List<String> content = urlResponseInfo.getAllHeaders().get("Content-Length"); + if (content == null) { + return 1024; + } else { + return Integer.parseInt(content.get(0)); + } + } + + private String generateCurlCommand(String url, CurlLoggedRequestParameters requestParameters) { + StringBuilder builder = new StringBuilder("curl "); + + // HTTP method + builder.append("-X ").append(requestParameters.getHttpMethod()).append(" "); + + // Request headers + for (Map.Entry<String, String> header : requestParameters.getHeaders().entrySet()) { + builder.append("--header \"").append(header.getKey()).append(": "); + if (!mLogAuthTokensInCurlCommands + && ("Authorization".equals(header.getKey()) + || "Cookie".equals(header.getKey()))) { + builder.append("[REDACTED]"); + } else { + builder.append(header.getValue()); + } + builder.append("\" "); + } + + // URL + builder.append("\"").append(url).append("\""); + + // Request body (if any) + if (requestParameters.getBody() != null) { + if (requestParameters.getBody().length >= 1024) { + builder.append(" [REQUEST BODY TOO LARGE TO INCLUDE]"); + } else if (isBinaryContentForLogging(requestParameters)) { + String base64 = Base64.encodeToString(requestParameters.getBody(), Base64.NO_WRAP); + builder.insert(0, "echo '" + base64 + "' | base64 -d > /tmp/$$.bin; ") + .append(" --data-binary @/tmp/$$.bin"); + } else { + // Just assume the request body is UTF-8 since this is for debugging. + try { + builder.append(" --data-ascii \"") + .append(new String(requestParameters.getBody(), "UTF-8")) + .append("\""); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Could not encode to UTF-8", e); + } + } + } + + return builder.toString(); + } + + /** Rough heuristic to determine whether the request body is binary, for logging purposes. */ + private boolean isBinaryContentForLogging(CurlLoggedRequestParameters requestParameters) { + // Check to see if the content is gzip compressed - this means it should be treated as + // binary content regardless of the content type. + String contentEncoding = requestParameters.getHeaders().get("Content-Encoding"); + if (contentEncoding != null) { + String[] encodings = TextUtils.split(contentEncoding, ","); + for (String encoding : encodings) { + if ("gzip".equals(encoding.trim())) { + return true; + } + } + } + + // If the content type is a known text type, treat it as text content. + String contentType = requestParameters.getHeaders().get("Content-Type"); + if (contentType != null) { + return !contentType.startsWith("text/") + && !contentType.startsWith("application/xml") + && !contentType.startsWith("application/json"); + } + + // Otherwise, assume it is binary content. + return true; + } + + /** + * Builder is used to build an instance of {@link CronetHttpStack} from values configured by the + * setters. + */ + public static class Builder { + private static final int DEFAULT_POOL_SIZE = 4096; + private CronetEngine mCronetEngine; + private final Context context; + private ByteArrayPool mPool; + private UrlRewriter mUrlRewriter; + private RequestListener mRequestListener; + private boolean mCurlLoggingEnabled; + private CurlCommandLogger mCurlCommandLogger; + private boolean mLogAuthTokensInCurlCommands; + + public Builder(Context context) { + this.context = context; + } + + /** Sets the CronetEngine to be used. Defaults to a vanialla CronetEngine. */ + public Builder setCronetEngine(CronetEngine engine) { + mCronetEngine = engine; + return this; + } + + /** Sets the ByteArrayPool to be used. Defaults to a new pool with 4096 bytes. */ + public Builder setPool(ByteArrayPool pool) { + mPool = pool; + return this; + } + + /** Sets the UrlRewriter to be used. Default is to return the original string. */ + public Builder setUrlRewriter(UrlRewriter urlRewriter) { + mUrlRewriter = urlRewriter; + return this; + } + + /** Set the optional RequestListener to be used. */ + public Builder setRequestListener(RequestListener requestListener) { + mRequestListener = requestListener; + return this; + } + + /** + * Sets whether cURL logging should be enabled for debugging purposes. + * + * <p>When enabled, for each request dispatched to the network, a roughly-equivalent cURL + * command will be logged to logcat. + * + * <p>The command may be missing some headers that are added by Cronet automatically, and + * the full request body may not be included if it is too large. To inspect the full + * requests and responses, see {@code CronetEngine#startNetLogToFile}. + * + * <p>WARNING: This is only intended for debugging purposes and should never be enabled on + * production devices. + * + * @see #setCurlCommandLogger(CurlCommandLogger) + * @see #setLogAuthTokensInCurlCommands(boolean) + */ + public Builder setCurlLoggingEnabled(boolean curlLoggingEnabled) { + mCurlLoggingEnabled = curlLoggingEnabled; + return this; + } + + /** + * Sets the function used to log cURL commands. + * + * <p>Allows customization of the logging performed when cURL logging is enabled. + * + * <p>By default, when cURL logging is enabled, cURL commands are logged using {@link + * VolleyLog#v}, e.g. at the verbose log level with the same log tag used by the rest of + * Volley. This function may optionally be invoked to provide a custom logger. + * + * @see #setCurlLoggingEnabled(boolean) + */ + public Builder setCurlCommandLogger(CurlCommandLogger curlCommandLogger) { + mCurlCommandLogger = curlCommandLogger; + return this; + } + + /** + * Sets whether to log known auth tokens in cURL commands, or redact them. + * + * <p>By default, headers which may contain auth tokens (e.g. Authorization or Cookie) will + * have their values redacted. Passing true to this method will disable this redaction and + * log the values of these headers. + * + * <p>This heuristic is not perfect; tokens that are logged in unknown headers, or in the + * request body itself, will not be redacted as they cannot be detected generically. + * + * @see #setCurlLoggingEnabled(boolean) + */ + public Builder setLogAuthTokensInCurlCommands(boolean logAuthTokensInCurlCommands) { + mLogAuthTokensInCurlCommands = logAuthTokensInCurlCommands; + return this; + } + + public CronetHttpStack build() { + if (mCronetEngine == null) { + mCronetEngine = new CronetEngine.Builder(context).build(); + } + if (mUrlRewriter == null) { + mUrlRewriter = + new UrlRewriter() { + @Override + public String rewriteUrl(String originalUrl) { + return originalUrl; + } + }; + } + if (mRequestListener == null) { + mRequestListener = new RequestListener() {}; + } + if (mPool == null) { + mPool = new ByteArrayPool(DEFAULT_POOL_SIZE); + } + if (mCurlCommandLogger == null) { + mCurlCommandLogger = + new CurlCommandLogger() { + @Override + public void logCurlCommand(String curlCommand) { + VolleyLog.v(curlCommand); + } + }; + } + return new CronetHttpStack( + mCronetEngine, + mPool, + mUrlRewriter, + mRequestListener, + mCurlLoggingEnabled, + mCurlCommandLogger, + mLogAuthTokensInCurlCommands); + } + } + + /** Callback interface allowing clients to intercept different parts of the request flow. */ + public abstract static class RequestListener { + private CronetHttpStack mStack; + + void initialize(CronetHttpStack stack) { + mStack = stack; + } + + /** + * Called when a request is prepared and about to be sent over the network. + * + * <p>Clients may use this callback to customize UrlRequests before they are dispatched, + * e.g. to enable socket tagging or request finished listeners. + */ + public void onRequestPrepared(Request<?> request, UrlRequest.Builder requestBuilder) {} + + /** @see AsyncHttpStack#getNonBlockingExecutor() */ + protected Executor getNonBlockingExecutor() { + return mStack.getNonBlockingExecutor(); + } + + /** @see AsyncHttpStack#getBlockingExecutor() */ + protected Executor getBlockingExecutor() { + return mStack.getBlockingExecutor(); + } + } + + /** + * Interface for logging cURL commands for requests. + * + * @see Builder#setCurlCommandLogger(CurlCommandLogger) + */ + public interface CurlCommandLogger { + /** Log the given cURL command. */ + void logCurlCommand(String curlCommand); + } + + /** + * Internal container class for request parameters that impact logged cURL commands. + * + * <p>When cURL logging is enabled, an equivalent cURL command to a given request must be + * generated and logged. However, the Cronet UrlRequest object is write-only. So, we write any + * relevant parameters into this read-write container so they can be referenced when generating + * the cURL command (if needed) and then merged into the UrlRequest. + */ + private static class CurlLoggedRequestParameters { + private final TreeMap<String, String> mHeaders = + new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + private String mHttpMethod; + @Nullable private byte[] mBody; + + /** + * Return the headers to be used for the request. + * + * <p>The returned map is case-insensitive. + */ + TreeMap<String, String> getHeaders() { + return mHeaders; + } + + /** Apply all the headers in the given map to the request. */ + void putAllHeaders(Map<String, String> headers) { + mHeaders.putAll(headers); + } + + String getHttpMethod() { + return mHttpMethod; + } + + void setHttpMethod(String httpMethod) { + mHttpMethod = httpMethod; + } + + @Nullable + byte[] getBody() { + return mBody; + } + + void setBody(String contentType, @Nullable byte[] body) { + mBody = body; + if (body != null && !mHeaders.containsKey(HttpHeaderParser.HEADER_CONTENT_TYPE)) { + // Set the content-type unless it was already set (by Request#getHeaders). + mHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, contentType); + } + } + + void applyToRequest(UrlRequest.Builder builder, ExecutorService nonBlockingExecutor) { + for (Map.Entry<String, String> header : mHeaders.entrySet()) { + builder.addHeader(header.getKey(), header.getValue()); + } + builder.setHttpMethod(mHttpMethod); + if (mBody != null) { + UploadDataProvider dataProvider = UploadDataProviders.create(mBody); + builder.setUploadDataProvider(dataProvider, nonBlockingExecutor); + } + } + } +} diff --git a/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java b/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java new file mode 100644 index 0000000..bafab8c --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.VolleyLog; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +/** Asynchronous extension of the {@link BaseHttpStack} class. */ +public abstract class AsyncHttpStack extends BaseHttpStack { + private ExecutorService mBlockingExecutor; + private ExecutorService mNonBlockingExecutor; + + public interface OnRequestComplete { + /** Invoked when the stack successfully completes a request. */ + void onSuccess(HttpResponse httpResponse); + + /** Invoked when the stack throws an {@link AuthFailureError} during a request. */ + void onAuthError(AuthFailureError authFailureError); + + /** Invoked when the stack throws an {@link IOException} during a request. */ + void onError(IOException ioException); + } + + /** + * Makes an HTTP request with the given parameters, and calls the {@link OnRequestComplete} + * callback, with either the {@link HttpResponse} or error that was thrown. + * + * @param request to perform + * @param additionalHeaders to be sent together with {@link Request#getHeaders()} + * @param callback to be called after retrieving the {@link HttpResponse} or throwing an error. + */ + public abstract void executeRequest( + Request<?> request, Map<String, String> additionalHeaders, OnRequestComplete callback); + + /** + * This method sets the non blocking executor to be used by the stack for non-blocking tasks. + * This method must be called before executing any requests. + */ + @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP}) + public void setNonBlockingExecutor(ExecutorService executor) { + mNonBlockingExecutor = executor; + } + + /** + * This method sets the blocking executor to be used by the stack for potentially blocking + * tasks. This method must be called before executing any requests. + */ + @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP}) + public void setBlockingExecutor(ExecutorService executor) { + mBlockingExecutor = executor; + } + + /** Gets blocking executor to perform any potentially blocking tasks. */ + protected ExecutorService getBlockingExecutor() { + return mBlockingExecutor; + } + + /** Gets non-blocking executor to perform any non-blocking tasks. */ + protected ExecutorService getNonBlockingExecutor() { + return mNonBlockingExecutor; + } + + /** + * Performs an HTTP request with the given parameters. + * + * @param request the request to perform + * @param additionalHeaders additional headers to be sent together with {@link + * Request#getHeaders()} + * @return the {@link HttpResponse} + * @throws IOException if an I/O error occurs during the request + * @throws AuthFailureError if an authentication failure occurs during the request + */ + @Override + public final HttpResponse executeRequest( + Request<?> request, Map<String, String> additionalHeaders) + throws IOException, AuthFailureError { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference<Response> entry = new AtomicReference<>(); + executeRequest( + request, + additionalHeaders, + new OnRequestComplete() { + @Override + public void onSuccess(HttpResponse httpResponse) { + Response response = + new Response( + httpResponse, + /* ioException= */ null, + /* authFailureError= */ null); + entry.set(response); + latch.countDown(); + } + + @Override + public void onAuthError(AuthFailureError authFailureError) { + Response response = + new Response( + /* httpResponse= */ null, + /* ioException= */ null, + authFailureError); + entry.set(response); + latch.countDown(); + } + + @Override + public void onError(IOException ioException) { + Response response = + new Response( + /* httpResponse= */ null, + ioException, + /* authFailureError= */ null); + entry.set(response); + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException e) { + VolleyLog.e(e, "while waiting for CountDownLatch"); + Thread.currentThread().interrupt(); + throw new InterruptedIOException(e.toString()); + } + Response response = entry.get(); + if (response.httpResponse != null) { + return response.httpResponse; + } else if (response.ioException != null) { + throw response.ioException; + } else { + throw response.authFailureError; + } + } + + private static class Response { + HttpResponse httpResponse; + IOException ioException; + AuthFailureError authFailureError; + + private Response( + @Nullable HttpResponse httpResponse, + @Nullable IOException ioException, + @Nullable AuthFailureError authFailureError) { + this.httpResponse = httpResponse; + this.ioException = ioException; + this.authFailureError = authFailureError; + } + } +} diff --git a/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java b/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java new file mode 100644 index 0000000..55892a0 --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static com.android.volley.toolbox.NetworkUtility.logSlowRequests; + +import android.os.SystemClock; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import com.android.volley.AsyncNetwork; +import com.android.volley.AuthFailureError; +import com.android.volley.Header; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.RequestTask; +import com.android.volley.VolleyError; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; + +/** A network performing Volley requests over an {@link HttpStack}. */ +public class BasicAsyncNetwork extends AsyncNetwork { + + private final AsyncHttpStack mAsyncStack; + private final ByteArrayPool mPool; + + /** + * @param httpStack HTTP stack to be used + * @param pool a buffer pool that improves GC performance in copy operations + */ + private BasicAsyncNetwork(AsyncHttpStack httpStack, ByteArrayPool pool) { + mAsyncStack = httpStack; + mPool = pool; + } + + @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP}) + @Override + public void setBlockingExecutor(ExecutorService executor) { + super.setBlockingExecutor(executor); + mAsyncStack.setBlockingExecutor(executor); + } + + @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP}) + @Override + public void setNonBlockingExecutor(ExecutorService executor) { + super.setNonBlockingExecutor(executor); + mAsyncStack.setNonBlockingExecutor(executor); + } + + /* Method to be called after a successful network request */ + private void onRequestSucceeded( + final Request<?> request, + final long requestStartMs, + final HttpResponse httpResponse, + final OnRequestComplete callback) { + final int statusCode = httpResponse.getStatusCode(); + final List<Header> responseHeaders = httpResponse.getHeaders(); + // Handle cache validation. + if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + long requestDuration = SystemClock.elapsedRealtime() - requestStartMs; + callback.onSuccess( + NetworkUtility.getNotModifiedNetworkResponse( + request, requestDuration, responseHeaders)); + return; + } + + byte[] responseContents = httpResponse.getContentBytes(); + if (responseContents == null && httpResponse.getContent() == null) { + // Add 0 byte response as a way of honestly representing a + // no-content request. + responseContents = new byte[0]; + } + + if (responseContents != null) { + onResponseRead( + requestStartMs, + statusCode, + httpResponse, + request, + callback, + responseHeaders, + responseContents); + return; + } + + // The underlying AsyncHttpStack does not support asynchronous reading of the response into + // a byte array, so we need to submit a blocking task to copy the response from the + // InputStream instead. + final InputStream inputStream = httpResponse.getContent(); + getBlockingExecutor() + .execute( + new ResponseParsingTask<>( + inputStream, + httpResponse, + request, + callback, + requestStartMs, + responseHeaders, + statusCode)); + } + + /* Method to be called after a failed network request */ + private void onRequestFailed( + Request<?> request, + OnRequestComplete callback, + IOException exception, + long requestStartMs, + @Nullable HttpResponse httpResponse, + @Nullable byte[] responseContents) { + try { + NetworkUtility.handleException( + request, exception, requestStartMs, httpResponse, responseContents); + } catch (VolleyError volleyError) { + callback.onError(volleyError); + return; + } + performRequest(request, callback); + } + + @Override + public void performRequest(final Request<?> request, final OnRequestComplete callback) { + if (getBlockingExecutor() == null) { + throw new IllegalStateException( + "mBlockingExecuter must be set before making a request"); + } + final long requestStartMs = SystemClock.elapsedRealtime(); + // Gather headers. + final Map<String, String> additionalRequestHeaders = + HttpHeaderParser.getCacheHeaders(request.getCacheEntry()); + mAsyncStack.executeRequest( + request, + additionalRequestHeaders, + new AsyncHttpStack.OnRequestComplete() { + @Override + public void onSuccess(HttpResponse httpResponse) { + onRequestSucceeded(request, requestStartMs, httpResponse, callback); + } + + @Override + public void onAuthError(AuthFailureError authFailureError) { + callback.onError(authFailureError); + } + + @Override + public void onError(IOException ioException) { + onRequestFailed( + request, + callback, + ioException, + requestStartMs, + /* httpResponse= */ null, + /* responseContents= */ null); + } + }); + } + + /* Helper method that determines what to do after byte[] is received */ + private void onResponseRead( + long requestStartMs, + int statusCode, + HttpResponse httpResponse, + Request<?> request, + OnRequestComplete callback, + List<Header> responseHeaders, + byte[] responseContents) { + // if the request is slow, log it. + long requestLifetime = SystemClock.elapsedRealtime() - requestStartMs; + logSlowRequests(requestLifetime, request, responseContents, statusCode); + + if (statusCode < 200 || statusCode > 299) { + onRequestFailed( + request, + callback, + new IOException(), + requestStartMs, + httpResponse, + responseContents); + return; + } + + callback.onSuccess( + new NetworkResponse( + statusCode, + responseContents, + /* notModified= */ false, + SystemClock.elapsedRealtime() - requestStartMs, + responseHeaders)); + } + + private class ResponseParsingTask<T> extends RequestTask<T> { + InputStream inputStream; + HttpResponse httpResponse; + Request<T> request; + OnRequestComplete callback; + long requestStartMs; + List<Header> responseHeaders; + int statusCode; + + ResponseParsingTask( + InputStream inputStream, + HttpResponse httpResponse, + Request<T> request, + OnRequestComplete callback, + long requestStartMs, + List<Header> responseHeaders, + int statusCode) { + super(request); + this.inputStream = inputStream; + this.httpResponse = httpResponse; + this.request = request; + this.callback = callback; + this.requestStartMs = requestStartMs; + this.responseHeaders = responseHeaders; + this.statusCode = statusCode; + } + + @Override + public void run() { + byte[] finalResponseContents; + try { + finalResponseContents = + NetworkUtility.inputStreamToBytes( + inputStream, httpResponse.getContentLength(), mPool); + } catch (IOException e) { + onRequestFailed(request, callback, e, requestStartMs, httpResponse, null); + return; + } + onResponseRead( + requestStartMs, + statusCode, + httpResponse, + request, + callback, + responseHeaders, + finalResponseContents); + } + } + + /** + * Builder is used to build an instance of {@link BasicAsyncNetwork} from values configured by + * the setters. + */ + public static class Builder { + private static final int DEFAULT_POOL_SIZE = 4096; + @NonNull private AsyncHttpStack mAsyncStack; + private ByteArrayPool mPool; + + public Builder(@NonNull AsyncHttpStack httpStack) { + mAsyncStack = httpStack; + mPool = null; + } + + /** + * Sets the ByteArrayPool to be used. If not set, it will default to a pool with the default + * pool size. + */ + public Builder setPool(ByteArrayPool pool) { + mPool = pool; + return this; + } + + /** Builds the {@link com.android.volley.toolbox.BasicAsyncNetwork} */ + public BasicAsyncNetwork build() { + if (mPool == null) { + mPool = new ByteArrayPool(DEFAULT_POOL_SIZE); + } + return new BasicAsyncNetwork(mAsyncStack, mPool); + } + } +} diff --git a/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/src/main/java/com/android/volley/toolbox/BasicNetwork.java index b527cb9..06427fe 100644 --- a/src/main/java/com/android/volley/toolbox/BasicNetwork.java +++ b/src/main/java/com/android/volley/toolbox/BasicNetwork.java @@ -17,41 +17,21 @@ package com.android.volley.toolbox; import android.os.SystemClock; -import com.android.volley.AuthFailureError; -import com.android.volley.Cache; -import com.android.volley.Cache.Entry; -import com.android.volley.ClientError; import com.android.volley.Header; import com.android.volley.Network; -import com.android.volley.NetworkError; import com.android.volley.NetworkResponse; -import com.android.volley.NoConnectionError; import com.android.volley.Request; -import com.android.volley.RetryPolicy; -import com.android.volley.ServerError; -import com.android.volley.TimeoutError; import com.android.volley.VolleyError; -import com.android.volley.VolleyLog; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.SocketTimeoutException; -import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.TreeMap; -import java.util.TreeSet; /** A network performing Volley requests over an {@link HttpStack}. */ public class BasicNetwork implements Network { - protected static final boolean DEBUG = VolleyLog.DEBUG; - - private static final int SLOW_REQUEST_THRESHOLD_MS = 3000; - private static final int DEFAULT_POOL_SIZE = 4096; /** @@ -119,37 +99,24 @@ public class BasicNetwork implements Network { try { // Gather headers. Map<String, String> additionalRequestHeaders = - getCacheHeaders(request.getCacheEntry()); + HttpHeaderParser.getCacheHeaders(request.getCacheEntry()); httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders); int statusCode = httpResponse.getStatusCode(); responseHeaders = httpResponse.getHeaders(); // Handle cache validation. if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) { - Entry entry = request.getCacheEntry(); - if (entry == null) { - return new NetworkResponse( - HttpURLConnection.HTTP_NOT_MODIFIED, - /* data= */ null, - /* notModified= */ true, - SystemClock.elapsedRealtime() - requestStart, - responseHeaders); - } - // Combine cached and response headers so the response will be complete. - List<Header> combinedHeaders = combineHeaders(responseHeaders, entry); - return new NetworkResponse( - HttpURLConnection.HTTP_NOT_MODIFIED, - entry.data, - /* notModified= */ true, - SystemClock.elapsedRealtime() - requestStart, - combinedHeaders); + long requestDuration = SystemClock.elapsedRealtime() - requestStart; + return NetworkUtility.getNotModifiedNetworkResponse( + request, requestDuration, responseHeaders); } // Some responses such as 204s do not have content. We must check. InputStream inputStream = httpResponse.getContent(); if (inputStream != null) { responseContents = - inputStreamToBytes(inputStream, httpResponse.getContentLength()); + NetworkUtility.inputStreamToBytes( + inputStream, httpResponse.getContentLength(), mPool); } else { // Add 0 byte response as a way of honestly representing a // no-content request. @@ -158,7 +125,8 @@ public class BasicNetwork implements Network { // if the request is slow, log it. long requestLifetime = SystemClock.elapsedRealtime() - requestStart; - logSlowRequests(requestLifetime, request, responseContents, statusCode); + NetworkUtility.logSlowRequests( + requestLifetime, request, responseContents, statusCode); if (statusCode < 200 || statusCode > 299) { throw new IOException(); @@ -169,141 +137,12 @@ public class BasicNetwork implements Network { /* notModified= */ false, SystemClock.elapsedRealtime() - requestStart, responseHeaders); - } catch (SocketTimeoutException e) { - attemptRetryOnException("socket", request, new TimeoutError()); - } catch (MalformedURLException e) { - throw new RuntimeException("Bad URL " + request.getUrl(), e); - } catch (IOException e) { - int statusCode; - if (httpResponse != null) { - statusCode = httpResponse.getStatusCode(); - } else { - throw new NoConnectionError(e); - } - VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); - NetworkResponse networkResponse; - if (responseContents != null) { - networkResponse = - new NetworkResponse( - statusCode, - responseContents, - /* notModified= */ false, - SystemClock.elapsedRealtime() - requestStart, - responseHeaders); - if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED - || statusCode == HttpURLConnection.HTTP_FORBIDDEN) { - attemptRetryOnException( - "auth", request, new AuthFailureError(networkResponse)); - } else if (statusCode >= 400 && statusCode <= 499) { - // Don't retry other client errors. - throw new ClientError(networkResponse); - } else if (statusCode >= 500 && statusCode <= 599) { - if (request.shouldRetryServerErrors()) { - attemptRetryOnException( - "server", request, new ServerError(networkResponse)); - } else { - throw new ServerError(networkResponse); - } - } else { - // 3xx? No reason to retry. - throw new ServerError(networkResponse); - } - } else { - attemptRetryOnException("network", request, new NetworkError()); - } - } - } - } - - /** Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. */ - private void logSlowRequests( - long requestLifetime, Request<?> request, byte[] responseContents, int statusCode) { - if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) { - VolleyLog.d( - "HTTP response for request=<%s> [lifetime=%d], [size=%s], " - + "[rc=%d], [retryCount=%s]", - request, - requestLifetime, - responseContents != null ? responseContents.length : "null", - statusCode, - request.getRetryPolicy().getCurrentRetryCount()); - } - } - - /** - * Attempts to prepare the request for a retry. If there are no more attempts remaining in the - * request's retry policy, a timeout exception is thrown. - * - * @param request The request to use. - */ - private static void attemptRetryOnException( - String logPrefix, Request<?> request, VolleyError exception) throws VolleyError { - RetryPolicy retryPolicy = request.getRetryPolicy(); - int oldTimeout = request.getTimeoutMs(); - - try { - retryPolicy.retry(exception); - } catch (VolleyError e) { - request.addMarker( - String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout)); - throw e; - } - request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout)); - } - - private Map<String, String> getCacheHeaders(Cache.Entry entry) { - // If there's no cache entry, we're done. - if (entry == null) { - return Collections.emptyMap(); - } - - Map<String, String> headers = new HashMap<>(); - - if (entry.etag != null) { - headers.put("If-None-Match", entry.etag); - } - - if (entry.lastModified > 0) { - headers.put( - "If-Modified-Since", HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified)); - } - - return headers; - } - - protected void logError(String what, String url, long start) { - long now = SystemClock.elapsedRealtime(); - VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url); - } - - /** Reads the contents of an InputStream into a byte[]. */ - private byte[] inputStreamToBytes(InputStream in, int contentLength) - throws IOException, ServerError { - PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(mPool, contentLength); - byte[] buffer = null; - try { - if (in == null) { - throw new ServerError(); - } - buffer = mPool.getBuf(1024); - int count; - while ((count = in.read(buffer)) != -1) { - bytes.write(buffer, 0, count); - } - return bytes.toByteArray(); - } finally { - try { - // Close the InputStream and release the resources by "consuming the content". - if (in != null) { - in.close(); - } } catch (IOException e) { - // This can happen if there was an exception above that left the stream in - // an invalid state. - VolleyLog.v("Error occurred when closing InputStream"); + // This will either throw an exception, breaking us from the loop, or will loop + // again and retry the request. + NetworkUtility.handleException( + request, e, requestStart, httpResponse, responseContents); } - mPool.returnBuf(buffer); - bytes.close(); } } @@ -321,49 +160,4 @@ public class BasicNetwork implements Network { } return result; } - - /** - * Combine cache headers with network response headers for an HTTP 304 response. - * - * <p>An HTTP 304 response does not have all header fields. We have to use the header fields - * from the cache entry plus the new ones from the response. See also: - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 - * - * @param responseHeaders Headers from the network response. - * @param entry The cached response. - * @return The combined list of headers. - */ - private static List<Header> combineHeaders(List<Header> responseHeaders, Entry entry) { - // First, create a case-insensitive set of header names from the network - // response. - Set<String> headerNamesFromNetworkResponse = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - if (!responseHeaders.isEmpty()) { - for (Header header : responseHeaders) { - headerNamesFromNetworkResponse.add(header.getName()); - } - } - - // Second, add headers from the cache entry to the network response as long as - // they didn't appear in the network response, which should take precedence. - List<Header> combinedHeaders = new ArrayList<>(responseHeaders); - if (entry.allResponseHeaders != null) { - if (!entry.allResponseHeaders.isEmpty()) { - for (Header header : entry.allResponseHeaders) { - if (!headerNamesFromNetworkResponse.contains(header.getName())) { - combinedHeaders.add(header); - } - } - } - } else { - // Legacy caches only have entry.responseHeaders. - if (!entry.responseHeaders.isEmpty()) { - for (Map.Entry<String, String> header : entry.responseHeaders.entrySet()) { - if (!headerNamesFromNetworkResponse.contains(header.getKey())) { - combinedHeaders.add(new Header(header.getKey(), header.getValue())); - } - } - } - } - return combinedHeaders; - } } diff --git a/src/main/java/com/android/volley/toolbox/FileSupplier.java b/src/main/java/com/android/volley/toolbox/FileSupplier.java new file mode 100644 index 0000000..70898a6 --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/FileSupplier.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import java.io.File; + +/** Represents a supplier for {@link File}s. */ +public interface FileSupplier { + File get(); +} diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java index 1b410af..0b29e80 100644 --- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java +++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java @@ -17,6 +17,8 @@ package com.android.volley.toolbox; import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; import com.android.volley.Cache; import com.android.volley.Header; import com.android.volley.NetworkResponse; @@ -24,17 +26,22 @@ import com.android.volley.VolleyLog; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.TimeZone; import java.util.TreeMap; +import java.util.TreeSet; /** Utility methods for parsing HTTP headers. */ public class HttpHeaderParser { - static final String HEADER_CONTENT_TYPE = "Content-Type"; + @RestrictTo({Scope.LIBRARY_GROUP}) + public static final String HEADER_CONTENT_TYPE = "Content-Type"; private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1"; @@ -226,4 +233,69 @@ public class HttpHeaderParser { } return allHeaders; } + + /** + * Combine cache headers with network response headers for an HTTP 304 response. + * + * <p>An HTTP 304 response does not have all header fields. We have to use the header fields + * from the cache entry plus the new ones from the response. See also: + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 + * + * @param responseHeaders Headers from the network response. + * @param entry The cached response. + * @return The combined list of headers. + */ + static List<Header> combineHeaders(List<Header> responseHeaders, Cache.Entry entry) { + // First, create a case-insensitive set of header names from the network + // response. + Set<String> headerNamesFromNetworkResponse = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + if (!responseHeaders.isEmpty()) { + for (Header header : responseHeaders) { + headerNamesFromNetworkResponse.add(header.getName()); + } + } + + // Second, add headers from the cache entry to the network response as long as + // they didn't appear in the network response, which should take precedence. + List<Header> combinedHeaders = new ArrayList<>(responseHeaders); + if (entry.allResponseHeaders != null) { + if (!entry.allResponseHeaders.isEmpty()) { + for (Header header : entry.allResponseHeaders) { + if (!headerNamesFromNetworkResponse.contains(header.getName())) { + combinedHeaders.add(header); + } + } + } + } else { + // Legacy caches only have entry.responseHeaders. + if (!entry.responseHeaders.isEmpty()) { + for (Map.Entry<String, String> header : entry.responseHeaders.entrySet()) { + if (!headerNamesFromNetworkResponse.contains(header.getKey())) { + combinedHeaders.add(new Header(header.getKey(), header.getValue())); + } + } + } + } + return combinedHeaders; + } + + static Map<String, String> getCacheHeaders(Cache.Entry entry) { + // If there's no cache entry, we're done. + if (entry == null) { + return Collections.emptyMap(); + } + + Map<String, String> headers = new HashMap<>(); + + if (entry.etag != null) { + headers.put("If-None-Match", entry.etag); + } + + if (entry.lastModified > 0) { + headers.put( + "If-Modified-Since", HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified)); + } + + return headers; + } } diff --git a/src/main/java/com/android/volley/toolbox/HttpResponse.java b/src/main/java/com/android/volley/toolbox/HttpResponse.java index 9a9294f..595f926 100644 --- a/src/main/java/com/android/volley/toolbox/HttpResponse.java +++ b/src/main/java/com/android/volley/toolbox/HttpResponse.java @@ -15,7 +15,9 @@ */ package com.android.volley.toolbox; +import androidx.annotation.Nullable; import com.android.volley.Header; +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Collections; import java.util.List; @@ -26,7 +28,8 @@ public final class HttpResponse { private final int mStatusCode; private final List<Header> mHeaders; private final int mContentLength; - private final InputStream mContent; + @Nullable private final InputStream mContent; + @Nullable private final byte[] mContentBytes; /** * Construct a new HttpResponse for an empty response body. @@ -53,6 +56,23 @@ public final class HttpResponse { mHeaders = headers; mContentLength = contentLength; mContent = content; + mContentBytes = null; + } + + /** + * Construct a new HttpResponse. + * + * @param statusCode the HTTP status code of the response + * @param headers the response headers + * @param contentBytes a byte[] of the response content. This is an optimization for HTTP stacks + * that natively support returning a byte[]. + */ + public HttpResponse(int statusCode, List<Header> headers, byte[] contentBytes) { + mStatusCode = statusCode; + mHeaders = headers; + mContentLength = contentBytes.length; + mContentBytes = contentBytes; + mContent = null; } /** Returns the HTTP status code of the response. */ @@ -71,10 +91,28 @@ public final class HttpResponse { } /** + * If a byte[] was already provided by an HTTP stack that natively supports returning one, this + * method will return that byte[] as an optimization over copying the bytes from an input + * stream. It may return null, even if the response has content, as long as mContent is + * provided. + */ + @Nullable + public final byte[] getContentBytes() { + return mContentBytes; + } + + /** * Returns an {@link InputStream} of the response content. May be null to indicate that the * response has no content. */ + @Nullable public final InputStream getContent() { - return mContent; + if (mContent != null) { + return mContent; + } else if (mContentBytes != null) { + return new ByteArrayInputStream(mContentBytes); + } else { + return null; + } } } diff --git a/src/main/java/com/android/volley/toolbox/HurlStack.java b/src/main/java/com/android/volley/toolbox/HurlStack.java index 9c38023..35c6a72 100644 --- a/src/main/java/com/android/volley/toolbox/HurlStack.java +++ b/src/main/java/com/android/volley/toolbox/HurlStack.java @@ -41,13 +41,7 @@ public class HurlStack extends BaseHttpStack { private static final int HTTP_CONTINUE = 100; /** An interface for transforming URLs before use. */ - public interface UrlRewriter { - /** - * Returns a URL to use instead of the provided one, or null to indicate this URL should not - * be used at all. - */ - String rewriteUrl(String originalUrl); - } + public interface UrlRewriter extends com.android.volley.toolbox.UrlRewriter {} private final UrlRewriter mUrlRewriter; private final SSLSocketFactory mSslSocketFactory; diff --git a/src/main/java/com/android/volley/toolbox/NetworkUtility.java b/src/main/java/com/android/volley/toolbox/NetworkUtility.java new file mode 100644 index 0000000..44d5904 --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/NetworkUtility.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.os.SystemClock; +import androidx.annotation.Nullable; +import com.android.volley.AuthFailureError; +import com.android.volley.Cache; +import com.android.volley.ClientError; +import com.android.volley.Header; +import com.android.volley.NetworkError; +import com.android.volley.NetworkResponse; +import com.android.volley.NoConnectionError; +import com.android.volley.Request; +import com.android.volley.RetryPolicy; +import com.android.volley.ServerError; +import com.android.volley.TimeoutError; +import com.android.volley.VolleyError; +import com.android.volley.VolleyLog; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.util.List; + +/** + * Utility class for methods that are shared between {@link BasicNetwork} and {@link + * BasicAsyncNetwork} + */ +public final class NetworkUtility { + private static final int SLOW_REQUEST_THRESHOLD_MS = 3000; + + private NetworkUtility() {} + + /** Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. */ + static void logSlowRequests( + long requestLifetime, Request<?> request, byte[] responseContents, int statusCode) { + if (VolleyLog.DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) { + VolleyLog.d( + "HTTP response for request=<%s> [lifetime=%d], [size=%s], " + + "[rc=%d], [retryCount=%s]", + request, + requestLifetime, + responseContents != null ? responseContents.length : "null", + statusCode, + request.getRetryPolicy().getCurrentRetryCount()); + } + } + + static NetworkResponse getNotModifiedNetworkResponse( + Request<?> request, long requestDuration, List<Header> responseHeaders) { + Cache.Entry entry = request.getCacheEntry(); + if (entry == null) { + return new NetworkResponse( + HttpURLConnection.HTTP_NOT_MODIFIED, + /* data= */ null, + /* notModified= */ true, + requestDuration, + responseHeaders); + } + // Combine cached and response headers so the response will be complete. + List<Header> combinedHeaders = HttpHeaderParser.combineHeaders(responseHeaders, entry); + return new NetworkResponse( + HttpURLConnection.HTTP_NOT_MODIFIED, + entry.data, + /* notModified= */ true, + requestDuration, + combinedHeaders); + } + + /** Reads the contents of an InputStream into a byte[]. */ + static byte[] inputStreamToBytes(InputStream in, int contentLength, ByteArrayPool pool) + throws IOException { + PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(pool, contentLength); + byte[] buffer = null; + try { + buffer = pool.getBuf(1024); + int count; + while ((count = in.read(buffer)) != -1) { + bytes.write(buffer, 0, count); + } + return bytes.toByteArray(); + } finally { + try { + // Close the InputStream and release the resources by "consuming the content". + if (in != null) { + in.close(); + } + } catch (IOException e) { + // This can happen if there was an exception above that left the stream in + // an invalid state. + VolleyLog.v("Error occurred when closing InputStream"); + } + pool.returnBuf(buffer); + bytes.close(); + } + } + + /** + * Attempts to prepare the request for a retry. If there are no more attempts remaining in the + * request's retry policy, a timeout exception is thrown. + * + * @param request The request to use. + */ + private static void attemptRetryOnException( + final String logPrefix, final Request<?> request, final VolleyError exception) + throws VolleyError { + final RetryPolicy retryPolicy = request.getRetryPolicy(); + final int oldTimeout = request.getTimeoutMs(); + try { + retryPolicy.retry(exception); + } catch (VolleyError e) { + request.addMarker( + String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout)); + throw e; + } + request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout)); + } + + /** + * Based on the exception thrown, decides whether to attempt to retry, or to throw the error. + * Also handles logging. + */ + static void handleException( + Request<?> request, + IOException exception, + long requestStartMs, + @Nullable HttpResponse httpResponse, + @Nullable byte[] responseContents) + throws VolleyError { + if (exception instanceof SocketTimeoutException) { + attemptRetryOnException("socket", request, new TimeoutError()); + } else if (exception instanceof MalformedURLException) { + throw new RuntimeException("Bad URL " + request.getUrl(), exception); + } else { + int statusCode; + if (httpResponse != null) { + statusCode = httpResponse.getStatusCode(); + } else { + if (request.shouldRetryConnectionErrors()) { + attemptRetryOnException("connection", request, new NoConnectionError()); + return; + } else { + throw new NoConnectionError(exception); + } + } + VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); + NetworkResponse networkResponse; + if (responseContents != null) { + List<Header> responseHeaders; + responseHeaders = httpResponse.getHeaders(); + networkResponse = + new NetworkResponse( + statusCode, + responseContents, + /* notModified= */ false, + SystemClock.elapsedRealtime() - requestStartMs, + responseHeaders); + if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED + || statusCode == HttpURLConnection.HTTP_FORBIDDEN) { + attemptRetryOnException("auth", request, new AuthFailureError(networkResponse)); + } else if (statusCode >= 400 && statusCode <= 499) { + // Don't retry other client errors. + throw new ClientError(networkResponse); + } else if (statusCode >= 500 && statusCode <= 599) { + if (request.shouldRetryServerErrors()) { + attemptRetryOnException( + "server", request, new ServerError(networkResponse)); + } else { + throw new ServerError(networkResponse); + } + } else { + // 3xx? No reason to retry. + throw new ServerError(networkResponse); + } + } else { + attemptRetryOnException("network", request, new NetworkError()); + } + } + } +} diff --git a/src/main/java/com/android/volley/toolbox/NoAsyncCache.java b/src/main/java/com/android/volley/toolbox/NoAsyncCache.java new file mode 100644 index 0000000..aa4aeea --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/NoAsyncCache.java @@ -0,0 +1,37 @@ +package com.android.volley.toolbox; + +import com.android.volley.AsyncCache; +import com.android.volley.Cache; + +/** An AsyncCache that doesn't cache anything. */ +public class NoAsyncCache extends AsyncCache { + @Override + public void get(String key, OnGetCompleteCallback callback) { + callback.onGetComplete(null); + } + + @Override + public void put(String key, Cache.Entry entry, OnWriteCompleteCallback callback) { + callback.onWriteComplete(); + } + + @Override + public void clear(OnWriteCompleteCallback callback) { + callback.onWriteComplete(); + } + + @Override + public void initialize(OnWriteCompleteCallback callback) { + callback.onWriteComplete(); + } + + @Override + public void invalidate(String key, boolean fullExpire, OnWriteCompleteCallback callback) { + callback.onWriteComplete(); + } + + @Override + public void remove(String key, OnWriteCompleteCallback callback) { + callback.onWriteComplete(); + } +} diff --git a/src/main/java/com/android/volley/toolbox/UrlRewriter.java b/src/main/java/com/android/volley/toolbox/UrlRewriter.java new file mode 100644 index 0000000..8bbb770 --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/UrlRewriter.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import androidx.annotation.Nullable; + +/** An interface for transforming URLs before use. */ +public interface UrlRewriter { + /** + * Returns a URL to use instead of the provided one, or null to indicate this URL should not be + * used at all. + */ + @Nullable + String rewriteUrl(String originalUrl); +} |