From 212e7004acfdce76c900fd97070e2e5e8476be20 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 7 Jul 2021 14:01:06 -0700 Subject: Import of Volley from GitHub to AOSP. Android.bp has been updated to account for the new source directory structure. - 0dc50bcfd021c204a9e6c9e7e6befbdfa1027247 Refactor Volley into a multi-module project. (#418) by Jeff Davidson - 763c86b0bc9f66a8bb499f6a8b7fd3bdc87621a8 Remove new constructors from JsonRequests which are break... by Jeff Davidson - 8d1b1a59e7cd1b1d3c6d8686f8831cea08f80d1f Add @NonNull annotations to Volley (#413) by Kamal Faraj - 5ba41f8670413973f587e435598f9f1724fa26e9 Allow sending any JSON with JsonArrayRequest & JsonObject... by Paul Smith - 784cdd755392a6080e5eb0bf94bd7bf4ea31cf17 Update SNAPSHOT version after 1.2.0 release by Jeff Davidson - 0d6497bab417a5f78b3c8e03ea157ada0fbfbc5d Add developers stanza to Volley POM. (#400) by Jeff Davidson - 36274bf515a699ae5a7fe3d321206d1b803226d8 API cleanup for Async Volley stack ahead of 1.2.0 release... by Jeff Davidson - 03f0144843fcf9ebafe512647c1c588975429452 Update environment variable name for snapshot pushes. (#3... by Jeff Davidson - 3bd1975652687d2baa1b11a7f02b135edede8523 Publish SNAPSHOT builds to OSSRH instead of OJO. (#397) by Jeff Davidson - 0e0c3d9cfa694f8f1400a9e9abc4bc11761fdb52 Invoke RetryPolicy#retry in the blocking executor. (#393) by Jeff Davidson - b51831a48f06ad28f627c3624e5edb41598a2bf8 Use a consistent timebase when evaluating soft/hard TTLs.... by Jeff Davidson - cd0839113b100f163df1ebd04ce6d5b9e36e9863 Migrate from Travis CI to GitHub Actions. (#381) by Jeff Davidson - bdc0e393199ebf9e67c4e29e665252818eed4639 Clean up cache initialization in AsyncRequestQueue. (#380) by Jeff Davidson - 1c0ade36edde15d02844b40351ab6f80c63b71b3 Actually allow applications to provide custom executors. by Jeff Davidson GitOrigin-RevId: 0dc50bcfd021c204a9e6c9e7e6befbdfa1027247 Change-Id: I4b8e4098ad5c349cb83efc867273fac1d3582a34 --- .github/workflows/gradle-build.yaml | 28 + .travis.yml | 41 -- Android.bp | 3 +- bintray.gradle | 78 --- build.gradle | 104 ++- consumer-proguard-rules.pro | 9 - core/build.gradle | 28 + core/consumer-proguard-rules.pro | 9 + core/src/main/AndroidManifest.xml | 2 + .../main/java/com/android/volley/AsyncCache.java | 94 +++ .../main/java/com/android/volley/AsyncNetwork.java | 145 +++++ .../java/com/android/volley/AsyncRequestQueue.java | 684 +++++++++++++++++++ .../java/com/android/volley/AuthFailureError.java | 56 ++ core/src/main/java/com/android/volley/Cache.java | 121 ++++ .../java/com/android/volley/CacheDispatcher.java | 212 ++++++ .../main/java/com/android/volley/ClientError.java | 34 + .../com/android/volley/DefaultRetryPolicy.java | 95 +++ .../java/com/android/volley/ExecutorDelivery.java | 121 ++++ core/src/main/java/com/android/volley/Header.java | 59 ++ core/src/main/java/com/android/volley/Network.java | 29 + .../java/com/android/volley/NetworkDispatcher.java | 177 +++++ .../main/java/com/android/volley/NetworkError.java | 33 + .../java/com/android/volley/NetworkResponse.java | 198 ++++++ .../java/com/android/volley/NoConnectionError.java | 29 + .../main/java/com/android/volley/ParseError.java | 31 + core/src/main/java/com/android/volley/Request.java | 723 +++++++++++++++++++++ .../main/java/com/android/volley/RequestQueue.java | 342 ++++++++++ .../main/java/com/android/volley/RequestTask.java | 20 + .../src/main/java/com/android/volley/Response.java | 84 +++ .../java/com/android/volley/ResponseDelivery.java | 31 + .../main/java/com/android/volley/RetryPolicy.java | 56 ++ .../main/java/com/android/volley/ServerError.java | 29 + .../main/java/com/android/volley/TimeoutError.java | 21 + .../main/java/com/android/volley/VolleyError.java | 55 ++ .../main/java/com/android/volley/VolleyLog.java | 182 ++++++ .../com/android/volley/WaitingRequestManager.java | 176 +++++ .../android/volley/toolbox/AdaptedHttpStack.java | 78 +++ .../volley/toolbox/AndroidAuthenticator.java | 123 ++++ .../com/android/volley/toolbox/AsyncHttpStack.java | 175 +++++ .../com/android/volley/toolbox/Authenticator.java | 32 + .../com/android/volley/toolbox/BaseHttpStack.java | 92 +++ .../android/volley/toolbox/BasicAsyncNetwork.java | 320 +++++++++ .../com/android/volley/toolbox/BasicNetwork.java | 167 +++++ .../com/android/volley/toolbox/ByteArrayPool.java | 130 ++++ .../android/volley/toolbox/ClearCacheRequest.java | 66 ++ .../com/android/volley/toolbox/DiskBasedCache.java | 677 +++++++++++++++++++ .../com/android/volley/toolbox/FileSupplier.java | 24 + .../android/volley/toolbox/HttpClientStack.java | 201 ++++++ .../android/volley/toolbox/HttpHeaderParser.java | 301 +++++++++ .../com/android/volley/toolbox/HttpResponse.java | 118 ++++ .../java/com/android/volley/toolbox/HttpStack.java | 47 ++ .../java/com/android/volley/toolbox/HurlStack.java | 321 +++++++++ .../com/android/volley/toolbox/ImageLoader.java | 541 +++++++++++++++ .../com/android/volley/toolbox/ImageRequest.java | 283 ++++++++ .../android/volley/toolbox/JsonArrayRequest.java | 86 +++ .../android/volley/toolbox/JsonObjectRequest.java | 106 +++ .../com/android/volley/toolbox/JsonRequest.java | 137 ++++ .../android/volley/toolbox/NetworkImageView.java | 332 ++++++++++ .../com/android/volley/toolbox/NetworkUtility.java | 206 ++++++ .../com/android/volley/toolbox/NoAsyncCache.java | 42 ++ .../java/com/android/volley/toolbox/NoCache.java | 42 ++ .../toolbox/PoolingByteArrayOutputStream.java | 92 +++ .../com/android/volley/toolbox/RequestFuture.java | 159 +++++ .../com/android/volley/toolbox/StringRequest.java | 100 +++ .../java/com/android/volley/toolbox/Threads.java | 13 + .../com/android/volley/toolbox/UrlRewriter.java | 29 + .../java/com/android/volley/toolbox/Volley.java | 123 ++++ .../com/android/volley/AsyncRequestQueueTest.java | 200 ++++++ .../com/android/volley/CacheDispatcherTest.java | 276 ++++++++ .../com/android/volley/NetworkDispatcherTest.java | 146 +++++ .../com/android/volley/NetworkResponseTest.java | 61 ++ .../volley/RequestQueueIntegrationTest.java | 197 ++++++ .../java/com/android/volley/RequestQueueTest.java | 129 ++++ .../test/java/com/android/volley/RequestTest.java | 232 +++++++ .../com/android/volley/ResponseDeliveryTest.java | 71 ++ .../com/android/volley/mock/MockAsyncStack.java | 86 +++ .../com/android/volley/mock/MockHttpStack.java | 80 +++ .../java/com/android/volley/mock/MockRequest.java | 99 +++ .../com/android/volley/mock/ShadowSystemClock.java | 27 + .../volley/toolbox/AdaptedHttpStackTest.java | 128 ++++ .../volley/toolbox/AndroidAuthenticatorTest.java | 111 ++++ .../android/volley/toolbox/BaseHttpStackTest.java | 104 +++ .../volley/toolbox/BasicAsyncNetworkTest.java | 508 +++++++++++++++ .../android/volley/toolbox/BasicNetworkTest.java | 384 +++++++++++ .../android/volley/toolbox/ByteArrayPoolTest.java | 78 +++ .../java/com/android/volley/toolbox/CacheTest.java | 39 ++ .../android/volley/toolbox/DiskBasedCacheTest.java | 646 ++++++++++++++++++ .../volley/toolbox/HttpClientStackTest.java | 156 +++++ .../volley/toolbox/HttpHeaderParserTest.java | 317 +++++++++ .../volley/toolbox/HttpStackConformanceTest.java | 192 ++++++ .../com/android/volley/toolbox/HurlStackTest.java | 337 ++++++++++ .../android/volley/toolbox/ImageLoaderTest.java | 121 ++++ .../android/volley/toolbox/ImageRequestTest.java | 194 ++++++ .../volley/toolbox/JsonRequestCharsetTest.java | 119 ++++ .../android/volley/toolbox/JsonRequestTest.java | 73 +++ .../volley/toolbox/NetworkImageViewTest.java | 101 +++ .../toolbox/PoolingByteArrayOutputStreamTest.java | 81 +++ .../android/volley/toolbox/RequestFutureTest.java | 35 + .../android/volley/toolbox/RequestQueueTest.java | 51 ++ .../com/android/volley/toolbox/RequestTest.java | 77 +++ .../com/android/volley/toolbox/ResponseTest.java | 55 ++ .../android/volley/toolbox/StringRequestTest.java | 42 ++ .../com/android/volley/utils/CacheTestUtils.java | 89 +++ .../volley/utils/ImmediateResponseDelivery.java | 37 ++ .../resources/org.robolectric.Config.properties | 1 + cronet/build.gradle | 24 + cronet/src/main/AndroidManifest.xml | 2 + .../com/android/volley/cronet/CronetHttpStack.java | 634 ++++++++++++++++++ .../android/volley/cronet/CronetHttpStackTest.java | 381 +++++++++++ publish-snapshot-on-commit.sh | 16 +- publish.gradle | 72 ++ rules.gradle | 36 - settings.gradle | 4 + src/main/AndroidManifest.xml | 2 - src/main/java/com/android/volley/AsyncCache.java | 89 --- src/main/java/com/android/volley/AsyncNetwork.java | 140 ---- .../java/com/android/volley/AsyncRequestQueue.java | 626 ------------------ .../java/com/android/volley/AuthFailureError.java | 56 -- src/main/java/com/android/volley/Cache.java | 113 ---- .../java/com/android/volley/CacheDispatcher.java | 206 ------ src/main/java/com/android/volley/ClientError.java | 34 - .../com/android/volley/DefaultRetryPolicy.java | 95 --- .../java/com/android/volley/ExecutorDelivery.java | 121 ---- src/main/java/com/android/volley/Header.java | 59 -- src/main/java/com/android/volley/Network.java | 29 - .../java/com/android/volley/NetworkDispatcher.java | 177 ----- src/main/java/com/android/volley/NetworkError.java | 33 - .../java/com/android/volley/NetworkResponse.java | 198 ------ .../java/com/android/volley/NoConnectionError.java | 29 - src/main/java/com/android/volley/ParseError.java | 31 - src/main/java/com/android/volley/Request.java | 719 -------------------- src/main/java/com/android/volley/RequestQueue.java | 342 ---------- src/main/java/com/android/volley/RequestTask.java | 15 - src/main/java/com/android/volley/Response.java | 84 --- .../java/com/android/volley/ResponseDelivery.java | 31 - src/main/java/com/android/volley/RetryPolicy.java | 56 -- src/main/java/com/android/volley/ServerError.java | 29 - src/main/java/com/android/volley/TimeoutError.java | 21 - src/main/java/com/android/volley/VolleyError.java | 55 -- src/main/java/com/android/volley/VolleyLog.java | 182 ------ .../com/android/volley/WaitingRequestManager.java | 176 ----- .../com/android/volley/cronet/CronetHttpStack.java | 631 ------------------ .../android/volley/toolbox/AdaptedHttpStack.java | 78 --- .../volley/toolbox/AndroidAuthenticator.java | 123 ---- .../com/android/volley/toolbox/AsyncHttpStack.java | 170 ----- .../com/android/volley/toolbox/Authenticator.java | 32 - .../com/android/volley/toolbox/BaseHttpStack.java | 92 --- .../android/volley/toolbox/BasicAsyncNetwork.java | 288 -------- .../com/android/volley/toolbox/BasicNetwork.java | 163 ----- .../com/android/volley/toolbox/ByteArrayPool.java | 130 ---- .../android/volley/toolbox/ClearCacheRequest.java | 66 -- .../com/android/volley/toolbox/DiskBasedCache.java | 677 ------------------- .../com/android/volley/toolbox/FileSupplier.java | 24 - .../android/volley/toolbox/HttpClientStack.java | 201 ------ .../android/volley/toolbox/HttpHeaderParser.java | 301 --------- .../com/android/volley/toolbox/HttpResponse.java | 118 ---- .../java/com/android/volley/toolbox/HttpStack.java | 47 -- .../java/com/android/volley/toolbox/HurlStack.java | 321 --------- .../com/android/volley/toolbox/ImageLoader.java | 541 --------------- .../com/android/volley/toolbox/ImageRequest.java | 283 -------- .../android/volley/toolbox/JsonArrayRequest.java | 83 --- .../android/volley/toolbox/JsonObjectRequest.java | 93 --- .../com/android/volley/toolbox/JsonRequest.java | 127 ---- .../android/volley/toolbox/NetworkImageView.java | 332 ---------- .../com/android/volley/toolbox/NetworkUtility.java | 196 ------ .../com/android/volley/toolbox/NoAsyncCache.java | 37 -- .../java/com/android/volley/toolbox/NoCache.java | 42 -- .../toolbox/PoolingByteArrayOutputStream.java | 92 --- .../com/android/volley/toolbox/RequestFuture.java | 159 ----- .../com/android/volley/toolbox/StringRequest.java | 100 --- .../java/com/android/volley/toolbox/Threads.java | 13 - .../com/android/volley/toolbox/UrlRewriter.java | 29 - .../java/com/android/volley/toolbox/Volley.java | 118 ---- .../com/android/volley/AsyncRequestQueueTest.java | 164 ----- .../com/android/volley/CacheDispatcherTest.java | 276 -------- .../com/android/volley/NetworkDispatcherTest.java | 146 ----- .../com/android/volley/NetworkResponseTest.java | 61 -- .../volley/RequestQueueIntegrationTest.java | 197 ------ .../java/com/android/volley/RequestQueueTest.java | 129 ---- src/test/java/com/android/volley/RequestTest.java | 232 ------- .../com/android/volley/ResponseDeliveryTest.java | 71 -- .../android/volley/cronet/CronetHttpStackTest.java | 381 ----------- .../com/android/volley/mock/MockAsyncStack.java | 86 --- .../com/android/volley/mock/MockHttpStack.java | 80 --- .../java/com/android/volley/mock/MockRequest.java | 99 --- .../com/android/volley/mock/ShadowSystemClock.java | 27 - .../java/com/android/volley/mock/TestRequest.java | 177 ----- .../volley/toolbox/AdaptedHttpStackTest.java | 128 ---- .../volley/toolbox/AndroidAuthenticatorTest.java | 111 ---- .../android/volley/toolbox/BaseHttpStackTest.java | 104 --- .../volley/toolbox/BasicAsyncNetworkTest.java | 508 --------------- .../android/volley/toolbox/BasicNetworkTest.java | 384 ----------- .../android/volley/toolbox/ByteArrayPoolTest.java | 78 --- .../java/com/android/volley/toolbox/CacheTest.java | 39 -- .../android/volley/toolbox/DiskBasedCacheTest.java | 646 ------------------ .../volley/toolbox/HttpClientStackTest.java | 156 ----- .../volley/toolbox/HttpHeaderParserTest.java | 317 --------- .../volley/toolbox/HttpStackConformanceTest.java | 192 ------ .../com/android/volley/toolbox/HurlStackTest.java | 337 ---------- .../android/volley/toolbox/ImageLoaderTest.java | 121 ---- .../android/volley/toolbox/ImageRequestTest.java | 194 ------ .../volley/toolbox/JsonRequestCharsetTest.java | 119 ---- .../android/volley/toolbox/JsonRequestTest.java | 73 --- .../volley/toolbox/NetworkImageViewTest.java | 101 --- .../toolbox/PoolingByteArrayOutputStreamTest.java | 81 --- .../android/volley/toolbox/RequestFutureTest.java | 35 - .../android/volley/toolbox/RequestQueueTest.java | 51 -- .../com/android/volley/toolbox/RequestTest.java | 77 --- .../com/android/volley/toolbox/ResponseTest.java | 55 -- .../android/volley/toolbox/StringRequestTest.java | 42 -- .../com/android/volley/utils/CacheTestUtils.java | 89 --- .../volley/utils/ImmediateResponseDelivery.java | 37 -- .../resources/org.robolectric.Config.properties | 1 - testing/build.gradle | 4 + testing/src/main/AndroidManifest.xml | 2 + .../java/com/android/volley/mock/TestRequest.java | 177 +++++ 216 files changed, 16107 insertions(+), 15884 deletions(-) create mode 100644 .github/workflows/gradle-build.yaml delete mode 100644 .travis.yml delete mode 100644 bintray.gradle delete mode 100644 consumer-proguard-rules.pro create mode 100644 core/build.gradle create mode 100644 core/consumer-proguard-rules.pro create mode 100644 core/src/main/AndroidManifest.xml create mode 100644 core/src/main/java/com/android/volley/AsyncCache.java create mode 100644 core/src/main/java/com/android/volley/AsyncNetwork.java create mode 100644 core/src/main/java/com/android/volley/AsyncRequestQueue.java create mode 100644 core/src/main/java/com/android/volley/AuthFailureError.java create mode 100644 core/src/main/java/com/android/volley/Cache.java create mode 100644 core/src/main/java/com/android/volley/CacheDispatcher.java create mode 100644 core/src/main/java/com/android/volley/ClientError.java create mode 100644 core/src/main/java/com/android/volley/DefaultRetryPolicy.java create mode 100644 core/src/main/java/com/android/volley/ExecutorDelivery.java create mode 100644 core/src/main/java/com/android/volley/Header.java create mode 100644 core/src/main/java/com/android/volley/Network.java create mode 100644 core/src/main/java/com/android/volley/NetworkDispatcher.java create mode 100644 core/src/main/java/com/android/volley/NetworkError.java create mode 100644 core/src/main/java/com/android/volley/NetworkResponse.java create mode 100644 core/src/main/java/com/android/volley/NoConnectionError.java create mode 100644 core/src/main/java/com/android/volley/ParseError.java create mode 100644 core/src/main/java/com/android/volley/Request.java create mode 100644 core/src/main/java/com/android/volley/RequestQueue.java create mode 100644 core/src/main/java/com/android/volley/RequestTask.java create mode 100644 core/src/main/java/com/android/volley/Response.java create mode 100644 core/src/main/java/com/android/volley/ResponseDelivery.java create mode 100644 core/src/main/java/com/android/volley/RetryPolicy.java create mode 100644 core/src/main/java/com/android/volley/ServerError.java create mode 100644 core/src/main/java/com/android/volley/TimeoutError.java create mode 100644 core/src/main/java/com/android/volley/VolleyError.java create mode 100644 core/src/main/java/com/android/volley/VolleyLog.java create mode 100644 core/src/main/java/com/android/volley/WaitingRequestManager.java create mode 100644 core/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java create mode 100644 core/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java create mode 100644 core/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java create mode 100644 core/src/main/java/com/android/volley/toolbox/Authenticator.java create mode 100644 core/src/main/java/com/android/volley/toolbox/BaseHttpStack.java create mode 100644 core/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java create mode 100644 core/src/main/java/com/android/volley/toolbox/BasicNetwork.java create mode 100644 core/src/main/java/com/android/volley/toolbox/ByteArrayPool.java create mode 100644 core/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java create mode 100644 core/src/main/java/com/android/volley/toolbox/DiskBasedCache.java create mode 100644 core/src/main/java/com/android/volley/toolbox/FileSupplier.java create mode 100644 core/src/main/java/com/android/volley/toolbox/HttpClientStack.java create mode 100644 core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java create mode 100644 core/src/main/java/com/android/volley/toolbox/HttpResponse.java create mode 100644 core/src/main/java/com/android/volley/toolbox/HttpStack.java create mode 100644 core/src/main/java/com/android/volley/toolbox/HurlStack.java create mode 100644 core/src/main/java/com/android/volley/toolbox/ImageLoader.java create mode 100644 core/src/main/java/com/android/volley/toolbox/ImageRequest.java create mode 100644 core/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java create mode 100644 core/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java create mode 100644 core/src/main/java/com/android/volley/toolbox/JsonRequest.java create mode 100644 core/src/main/java/com/android/volley/toolbox/NetworkImageView.java create mode 100644 core/src/main/java/com/android/volley/toolbox/NetworkUtility.java create mode 100644 core/src/main/java/com/android/volley/toolbox/NoAsyncCache.java create mode 100644 core/src/main/java/com/android/volley/toolbox/NoCache.java create mode 100644 core/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java create mode 100644 core/src/main/java/com/android/volley/toolbox/RequestFuture.java create mode 100644 core/src/main/java/com/android/volley/toolbox/StringRequest.java create mode 100644 core/src/main/java/com/android/volley/toolbox/Threads.java create mode 100644 core/src/main/java/com/android/volley/toolbox/UrlRewriter.java create mode 100644 core/src/main/java/com/android/volley/toolbox/Volley.java create mode 100644 core/src/test/java/com/android/volley/AsyncRequestQueueTest.java create mode 100644 core/src/test/java/com/android/volley/CacheDispatcherTest.java create mode 100644 core/src/test/java/com/android/volley/NetworkDispatcherTest.java create mode 100644 core/src/test/java/com/android/volley/NetworkResponseTest.java create mode 100644 core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java create mode 100644 core/src/test/java/com/android/volley/RequestQueueTest.java create mode 100644 core/src/test/java/com/android/volley/RequestTest.java create mode 100644 core/src/test/java/com/android/volley/ResponseDeliveryTest.java create mode 100644 core/src/test/java/com/android/volley/mock/MockAsyncStack.java create mode 100644 core/src/test/java/com/android/volley/mock/MockHttpStack.java create mode 100644 core/src/test/java/com/android/volley/mock/MockRequest.java create mode 100644 core/src/test/java/com/android/volley/mock/ShadowSystemClock.java create mode 100644 core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/CacheTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/HurlStackTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/RequestTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/ResponseTest.java create mode 100644 core/src/test/java/com/android/volley/toolbox/StringRequestTest.java create mode 100644 core/src/test/java/com/android/volley/utils/CacheTestUtils.java create mode 100644 core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java create mode 100644 core/src/test/resources/org.robolectric.Config.properties create mode 100644 cronet/build.gradle create mode 100644 cronet/src/main/AndroidManifest.xml create mode 100644 cronet/src/main/java/com/android/volley/cronet/CronetHttpStack.java create mode 100644 cronet/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java create mode 100644 publish.gradle delete mode 100644 rules.gradle create mode 100644 settings.gradle delete mode 100644 src/main/AndroidManifest.xml delete mode 100644 src/main/java/com/android/volley/AsyncCache.java delete mode 100644 src/main/java/com/android/volley/AsyncNetwork.java delete mode 100644 src/main/java/com/android/volley/AsyncRequestQueue.java delete mode 100644 src/main/java/com/android/volley/AuthFailureError.java delete mode 100644 src/main/java/com/android/volley/Cache.java delete mode 100644 src/main/java/com/android/volley/CacheDispatcher.java delete mode 100644 src/main/java/com/android/volley/ClientError.java delete mode 100644 src/main/java/com/android/volley/DefaultRetryPolicy.java delete mode 100644 src/main/java/com/android/volley/ExecutorDelivery.java delete mode 100644 src/main/java/com/android/volley/Header.java delete mode 100644 src/main/java/com/android/volley/Network.java delete mode 100644 src/main/java/com/android/volley/NetworkDispatcher.java delete mode 100644 src/main/java/com/android/volley/NetworkError.java delete mode 100644 src/main/java/com/android/volley/NetworkResponse.java delete mode 100644 src/main/java/com/android/volley/NoConnectionError.java delete mode 100644 src/main/java/com/android/volley/ParseError.java delete mode 100644 src/main/java/com/android/volley/Request.java delete mode 100644 src/main/java/com/android/volley/RequestQueue.java delete mode 100644 src/main/java/com/android/volley/RequestTask.java delete mode 100644 src/main/java/com/android/volley/Response.java delete mode 100644 src/main/java/com/android/volley/ResponseDelivery.java delete mode 100644 src/main/java/com/android/volley/RetryPolicy.java delete mode 100644 src/main/java/com/android/volley/ServerError.java delete mode 100644 src/main/java/com/android/volley/TimeoutError.java delete mode 100644 src/main/java/com/android/volley/VolleyError.java delete mode 100644 src/main/java/com/android/volley/VolleyLog.java delete mode 100644 src/main/java/com/android/volley/WaitingRequestManager.java delete mode 100644 src/main/java/com/android/volley/cronet/CronetHttpStack.java delete mode 100644 src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java delete mode 100644 src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java delete mode 100644 src/main/java/com/android/volley/toolbox/AsyncHttpStack.java delete mode 100644 src/main/java/com/android/volley/toolbox/Authenticator.java delete mode 100644 src/main/java/com/android/volley/toolbox/BaseHttpStack.java delete mode 100644 src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java delete mode 100644 src/main/java/com/android/volley/toolbox/BasicNetwork.java delete mode 100644 src/main/java/com/android/volley/toolbox/ByteArrayPool.java delete mode 100644 src/main/java/com/android/volley/toolbox/ClearCacheRequest.java delete mode 100644 src/main/java/com/android/volley/toolbox/DiskBasedCache.java delete mode 100644 src/main/java/com/android/volley/toolbox/FileSupplier.java delete mode 100644 src/main/java/com/android/volley/toolbox/HttpClientStack.java delete mode 100644 src/main/java/com/android/volley/toolbox/HttpHeaderParser.java delete mode 100644 src/main/java/com/android/volley/toolbox/HttpResponse.java delete mode 100644 src/main/java/com/android/volley/toolbox/HttpStack.java delete mode 100644 src/main/java/com/android/volley/toolbox/HurlStack.java delete mode 100644 src/main/java/com/android/volley/toolbox/ImageLoader.java delete mode 100644 src/main/java/com/android/volley/toolbox/ImageRequest.java delete mode 100644 src/main/java/com/android/volley/toolbox/JsonArrayRequest.java delete mode 100644 src/main/java/com/android/volley/toolbox/JsonObjectRequest.java delete mode 100644 src/main/java/com/android/volley/toolbox/JsonRequest.java delete mode 100644 src/main/java/com/android/volley/toolbox/NetworkImageView.java delete mode 100644 src/main/java/com/android/volley/toolbox/NetworkUtility.java delete mode 100644 src/main/java/com/android/volley/toolbox/NoAsyncCache.java delete mode 100644 src/main/java/com/android/volley/toolbox/NoCache.java delete mode 100644 src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java delete mode 100644 src/main/java/com/android/volley/toolbox/RequestFuture.java delete mode 100644 src/main/java/com/android/volley/toolbox/StringRequest.java delete mode 100644 src/main/java/com/android/volley/toolbox/Threads.java delete mode 100644 src/main/java/com/android/volley/toolbox/UrlRewriter.java delete mode 100644 src/main/java/com/android/volley/toolbox/Volley.java delete mode 100644 src/test/java/com/android/volley/AsyncRequestQueueTest.java delete mode 100644 src/test/java/com/android/volley/CacheDispatcherTest.java delete mode 100644 src/test/java/com/android/volley/NetworkDispatcherTest.java delete mode 100644 src/test/java/com/android/volley/NetworkResponseTest.java delete mode 100644 src/test/java/com/android/volley/RequestQueueIntegrationTest.java delete mode 100644 src/test/java/com/android/volley/RequestQueueTest.java delete mode 100644 src/test/java/com/android/volley/RequestTest.java delete mode 100644 src/test/java/com/android/volley/ResponseDeliveryTest.java delete mode 100644 src/test/java/com/android/volley/cronet/CronetHttpStackTest.java delete mode 100644 src/test/java/com/android/volley/mock/MockAsyncStack.java delete mode 100644 src/test/java/com/android/volley/mock/MockHttpStack.java delete mode 100644 src/test/java/com/android/volley/mock/MockRequest.java delete mode 100644 src/test/java/com/android/volley/mock/ShadowSystemClock.java delete mode 100644 src/test/java/com/android/volley/mock/TestRequest.java delete mode 100644 src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/BasicNetworkTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/CacheTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/HttpClientStackTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/HurlStackTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/ImageLoaderTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/ImageRequestTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/JsonRequestTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/RequestFutureTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/RequestQueueTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/RequestTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/ResponseTest.java delete mode 100644 src/test/java/com/android/volley/toolbox/StringRequestTest.java delete mode 100644 src/test/java/com/android/volley/utils/CacheTestUtils.java delete mode 100644 src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java delete mode 100644 src/test/resources/org.robolectric.Config.properties create mode 100644 testing/build.gradle create mode 100644 testing/src/main/AndroidManifest.xml create mode 100644 testing/src/main/java/com/android/volley/mock/TestRequest.java diff --git a/.github/workflows/gradle-build.yaml b/.github/workflows/gradle-build.yaml new file mode 100644 index 0000000..c42648c --- /dev/null +++ b/.github/workflows/gradle-build.yaml @@ -0,0 +1,28 @@ +name: Gradle + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Cache Gradle packages + uses: actions/cache@v2 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Build with Gradle + run: ./gradlew --continue verifyGoogleJavaFormat build connectedCheck + - name: Publish snapshot + if: github.event_name == 'push' + env: + OSSRH_DEPLOY_USERNAME: ${{ secrets.OSSRH_DEPLOY_USERNAME }} + OSSRH_DEPLOY_PASSWORD: ${{ secrets.OSSRH_DEPLOY_PASSWORD }} + run: ./publish-snapshot-on-commit.sh diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fb6481a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,41 +0,0 @@ -language: android -android: - licenses: - - 'android-sdk-preview-license-.+' - - 'android-sdk-license-.+' - - 'google-gdk-license-.+' - - components: - # Workaround to be able to install v28 SDK and build tools. - # See https://github.com/travis-ci/travis-ci/issues/6040 - - tools # to update the repository XML files - - tools # to update the SDK tools themselves - - - platform-tools - - build-tools-28.0.3 - - android-28 - -jdk: - - oraclejdk8 - -# Avoid uploading the cache after every build -# See https://docs.travis-ci.com/user/languages/android/ -before_cache: - - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ - - rm -fr $HOME/.gradle/caches/*/classAnalysis/ - - rm -fr $HOME/.gradle/caches/*/workerMain/ -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ - - $HOME/.android/build-cache - -env: - global: - - secure: "ObTSgg1H/RoQwhf9735Cr0OEs7r296aQTKiVOmQYSIxZPzc7o2KveSVV8IVeQ+VQtDxPFij6Odk4gaQSKVytliTlgtSsrEaYt67yWWYLWBLWbnzLNYoWMMy+36O5BK78RNtVJ3Xr1mdMcZ2+SRj2TcEvQBqMWlUkJfEcmJttreq0Wd7jiNitv5MbyYRcd2AvKaqNilx5rEm1ihbE0wT3lH20EdAjjjckaBT04r+VXU9e0dg6tmZjqG8dxGzxYHLRTz7nmKXlUnOJ3steNPrmR/AsGqKW6Ppowi31t3iLpL3zdT0+mvzfvnQwBodqHWSU/JhVDGlePv+4a5aU80s+5nN1IKL7tTLGYWoKdoIuQLovRkdcdkuj8UNyftPj6qOO2C9Tk8j64WXwUIDRnmNfjXbFzqN51oiT94G6hPcEDQSxLwuqlmgNm9I9WxZidb5YotIN2BcIKthAvdL4ecxE5INJvss8DVdYUZ53000GqSoMv8WET6jYkSJPKfvmTgpqqYIW0sgMiDfO/ta/MTFG5kSqECL+sAFZNmugwmTc7NIdy29myCyLH/A6oM4n3QeFDDHhOl2cRYlsX3juzw2goRppR9sEosFN0D1T7Fije5RmJsPgVLGwYMxSGukAHufcatKfhccUVl+haSJ3PLQ1z+25Ug2kpJwMAHbUYWCBnew=" - - secure: "Ac95rbJd5dgNdK8ZlaeXpkKYXHGNj8pm4eNv1Oe8YtsByshHtBAK1m55H4Ex55oRXaLXntvQwnnJfqeTDPuqvd7QP1fjvp4Yjdaqa8MkC6qdVtm6LaqsGuE38uSiU6oxJKfnlCywNe2LfFlbzBtPd3ejNI7tfJcO3s1mD2aBT46vmwUy0t5ESxNdP2zUs6DFcbaOUWeJhQn8iNdRm2VbHEMdevzvvXXIUB9YUdBZQIuAZ5E8NRP5/dzPE8P0CY6/yXqQ/6bkXRV3Pf9QsRzJ+oEQVJAIFfC4JAGasgaBIVpaJ2C2At39jrNFpGYUNbHxvkBEal+WiaPk5TfmVbpyWJOTPRaaY3tIRdBbYf6kklnQk2jRJB6GCi4/yvT6oNjTQQEwsuYlaivkibwNehQqtqyjj12CcTI7lwbgNXeLvIWE6LNLIxrY4pnNy3bKjA0oLFoG/FuP3Wi9WldBtVXwvUBVFFXeOgzP6lCDkzGYTwYZi20lRmSgma3Q5e3/BbPtos3BZ9dSY3lUjttGxvHEDCJ48U1aw6usR91ZKD78Thb5OxWLkvs4rjWEDU2I649wiYSyqFldNEnv+2SJSRB+097XEcnCopGXorlMhBMAlhwHyiRY1u0D+9qrpfIl9Z07j0ZiG5uIscDXQZZjMivqe/+u8NJ3kN0zrDO5BQEgNls=" - -# Publish a SNAPSHOT build for all commits to master. -script: - - ./gradlew --continue verifyGoogleJavaFormat build connectedCheck && ./publish-snapshot-on-commit.sh diff --git a/Android.bp b/Android.bp index 803b4f2..917048f 100644 --- a/Android.bp +++ b/Android.bp @@ -36,11 +36,10 @@ java_library { name: "volley", sdk_version: "28", min_sdk_version: "8", - srcs: ["src/main/java/**/*.java"], + srcs: ["core/src/main/java/**/*.java"], // Exclude Cronet support for now. Can be enabled later if/when Cronet is made available as a // compilation dependency for Volley clients. - exclude_srcs: ["src/main/java/com/android/volley/cronet/**/*"], libs: [ // Only needed at compile-time. diff --git a/bintray.gradle b/bintray.gradle deleted file mode 100644 index b642b41..0000000 --- a/bintray.gradle +++ /dev/null @@ -1,78 +0,0 @@ -buildscript { - repositories { - jcenter() - } - dependencies { - classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.8.1" - } -} - -// apply the plugin with its class name rather than its Id to work around gradle limitation of -// not being able to find the plugin by Id despite the dependencies being added right above. Gradle -// is currently not capable of loading plugins by Id if the dependency is anywhere else than -// in the main project build.gradle. This file is "imported" into the project's build.gradle -// through a "apply from:". -apply plugin: org.jfrog.gradle.plugin.artifactory.ArtifactoryPlugin -apply plugin: 'maven-publish' - -task sourcesJar(type: Jar) { - classifier = 'sources' - from android.sourceSets.main.java.srcDirs -} - -task javadoc(type: Javadoc) { - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir -} - -artifacts { - archives javadocJar - archives sourcesJar -} - -publishing { - publications { - library(MavenPublication) { - groupId 'com.android.volley' - artifactId 'volley' - version project.version - pom { - packaging 'aar' - licenses { - license { - name = "The Apache License, Version 2.0" - url = "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - } - - // Release AAR, Sources, and JavaDoc - artifact "$buildDir/outputs/aar/volley-release.aar" - artifact sourcesJar - artifact javadocJar - } - } -} - -artifactory { - contextUrl = "https://oss.jfrog.org" - publish { - repository { - repoKey = 'oss-snapshot-local' - username = System.env.CI_DEPLOY_USERNAME - password = System.env.CI_DEPLOY_PASSWORD - } - defaults { - publications('library') - publishArtifacts = true - } - } - resolve { - repoKey = 'jcenter' - } -} diff --git a/build.gradle b/build.gradle index 544771c..b8db952 100644 --- a/build.gradle +++ b/build.gradle @@ -1,76 +1,72 @@ -// NOTE: The only changes that belong in this file are the definitions -// of tool versions (gradle plugin, compile SDK, build tools), so that -// Volley can be built via gradle as a standalone project. -// -// Any other changes to the build config belong in rules.gradle, which -// is used by projects that depend on Volley but define their own -// tools versions across all dependencies to ensure a consistent build. -// -// Most users should just add this line to settings.gradle: -// include(":volley") -// -// If you have a more complicated Gradle setup you can choose to use -// this instead: -// include(":volley") -// project(':volley').buildFileName = 'rules.gradle' - -import net.ltgt.gradle.errorprone.CheckSeverity - buildscript { repositories { + gradlePluginPortal() jcenter() google() } dependencies { classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'gradle.plugin.com.github.sherter.google-java-format:google-java-format-gradle-plugin:0.6' + // NOTE: 0.7 or newer will require upgrading to a newer Android gradle plugin: + // https://github.com/tbroyer/gradle-errorprone-plugin/commit/65b1026ebeae1b7ed8c28578c7f6eea512c16bea + classpath 'net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:0.6.1' } } -plugins { - id "com.github.sherter.google-java-format" version "0.6" - // NOTE: 0.7 or newer will require upgrading to a newer Android gradle plugin: - // https://github.com/tbroyer/gradle-errorprone-plugin/commit/65b1026ebeae1b7ed8c28578c7f6eea512c16bea - id "net.ltgt.errorprone" version "0.6.1" +allprojects { + repositories { + jcenter() + google() + } } -googleJavaFormat { - toolVersion = '1.5' - options style: 'AOSP' -} +subprojects { + apply plugin: 'com.github.sherter.google-java-format' + apply plugin: 'net.ltgt.errorprone' -apply plugin: 'com.android.library' + googleJavaFormat { + toolVersion = '1.5' + options style: 'AOSP' + } -repositories { - jcenter() - google() -} + apply plugin: 'com.android.library' -dependencies { - // NOTE: Updating ErrorProne introduces new checks that may cause the build to fail. Pin to a - // specific version to control these updates. - errorprone("com.google.errorprone:error_prone_core:2.3.2") - // ErrorProne requires a JDK 9 compiler, so pull one in as a dependency since we use Java 8: - // https://github.com/tbroyer/gradle-errorprone-plugin#jdk-8-support - errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") -} + dependencies { + // NOTE: Updating ErrorProne introduces new checks that may cause the build to fail. Pin to a + // specific version to control these updates. + errorprone("com.google.errorprone:error_prone_core:2.3.2") + // ErrorProne requires a JDK 9 compiler, so pull one in as a dependency since we use Java 8: + // https://github.com/tbroyer/gradle-errorprone-plugin#jdk-8-support + errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") + } + + group = 'com.android.volley' + version = '1.2.1-SNAPSHOT' + + android { + useLibrary 'org.apache.http.legacy' -group = 'com.android.volley' -version = '1.2.0-SNAPSHOT' + compileSdkVersion 28 + buildToolsVersion = '28.0.3' -android { - compileSdkVersion 28 - buildToolsVersion = '28.0.3' + defaultConfig { + minSdkVersion 8 + } - defaultConfig { - minSdkVersion 8 + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } } -} -tasks.withType(JavaCompile) { - options.errorprone { - check("ParameterComment", CheckSeverity.ERROR) + tasks.withType(JavaCompile) { + options.errorprone { + check("ParameterComment", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + } + options.compilerArgs << "-Xlint:unchecked" << "-Werror" } -} -apply from: 'rules.gradle' -apply from: 'bintray.gradle' + if (it.name != 'testing') { + apply from: '../publish.gradle' + } +} diff --git a/consumer-proguard-rules.pro b/consumer-proguard-rules.pro deleted file mode 100644 index 38d2cf1..0000000 --- a/consumer-proguard-rules.pro +++ /dev/null @@ -1,9 +0,0 @@ -# Prevent Proguard from inlining methods that are intentionally extracted to ensure locals have a -# constrained liveness scope by the GC. This is needed to avoid keeping previous request references -# alive for an indeterminate amount of time. See also https://github.com/google/volley/issues/114 --keepclassmembers,allowshrinking,allowobfuscation class com.android.volley.NetworkDispatcher { - void processRequest(); -} --keepclassmembers,allowshrinking,allowobfuscation class com.android.volley.CacheDispatcher { - void processRequest(); -} diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 0000000..812968c --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,28 @@ +android { + defaultConfig { + consumerProguardFiles 'consumer-proguard-rules.pro' + } +} + +dependencies { + implementation "androidx.annotation:annotation:1.0.1" + + testImplementation project(":testing") + testImplementation "junit:junit:4.12" + testImplementation "org.hamcrest:hamcrest-library:1.3" + testImplementation "org.mockito:mockito-core:2.19.0" + testImplementation "org.robolectric:robolectric:3.4.2" +} + +publishing { + publications { + library(MavenPublication) { + artifactId 'volley' + pom { + name = 'Volley' + description = 'An HTTP library that makes networking for Android apps easier and, most importantly, faster.' + } + artifact "$buildDir/outputs/aar/core-release.aar" + } + } +} diff --git a/core/consumer-proguard-rules.pro b/core/consumer-proguard-rules.pro new file mode 100644 index 0000000..38d2cf1 --- /dev/null +++ b/core/consumer-proguard-rules.pro @@ -0,0 +1,9 @@ +# Prevent Proguard from inlining methods that are intentionally extracted to ensure locals have a +# constrained liveness scope by the GC. This is needed to avoid keeping previous request references +# alive for an indeterminate amount of time. See also https://github.com/google/volley/issues/114 +-keepclassmembers,allowshrinking,allowobfuscation class com.android.volley.NetworkDispatcher { + void processRequest(); +} +-keepclassmembers,allowshrinking,allowobfuscation class com.android.volley.CacheDispatcher { + void processRequest(); +} diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ba3a2a7 --- /dev/null +++ b/core/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/core/src/main/java/com/android/volley/AsyncCache.java b/core/src/main/java/com/android/volley/AsyncCache.java new file mode 100644 index 0000000..8b2dbcc --- /dev/null +++ b/core/src/main/java/com/android/volley/AsyncCache.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import androidx.annotation.Nullable; + +/** + * Asynchronous equivalent to the {@link Cache} interface. + * + *

WARNING: This API is experimental and subject to breaking changes. Please see + * https://github.com/google/volley/wiki/Asynchronous-Volley for more details. + */ +public abstract class AsyncCache { + + public interface OnGetCompleteCallback { + /** + * Invoked when the read from the cache is complete. + * + * @param entry The entry read from the cache, or null if the read failed or the key did not + * exist in the cache. + */ + void onGetComplete(@Nullable Cache.Entry entry); + } + + /** + * Retrieves an entry from the cache and sends it back through the {@link + * OnGetCompleteCallback#onGetComplete} function + * + * @param key Cache key + * @param callback Callback that will be notified when the information has been retrieved + */ + public abstract void get(String key, OnGetCompleteCallback callback); + + public interface OnWriteCompleteCallback { + /** Invoked when the cache operation is complete */ + void onWriteComplete(); + } + + /** + * Writes a {@link Cache.Entry} to the cache, and calls {@link + * OnWriteCompleteCallback#onWriteComplete} after the operation is finished. + * + * @param key Cache key + * @param entry The entry to be written to the cache + * @param callback Callback that will be notified when the information has been written + */ + public abstract void put(String key, Cache.Entry entry, OnWriteCompleteCallback callback); + + /** + * Clears the cache. Deletes all cached files from disk. Calls {@link + * OnWriteCompleteCallback#onWriteComplete} after the operation is finished. + */ + public abstract void clear(OnWriteCompleteCallback callback); + + /** + * Initializes the cache and calls {@link OnWriteCompleteCallback#onWriteComplete} after the + * operation is finished. + */ + public abstract void initialize(OnWriteCompleteCallback callback); + + /** + * Invalidates an entry in the cache and calls {@link OnWriteCompleteCallback#onWriteComplete} + * after the operation is finished. + * + * @param key Cache key + * @param fullExpire True to fully expire the entry, false to soft expire + * @param callback Callback that's invoked once the entry has been invalidated + */ + public abstract void invalidate( + String key, boolean fullExpire, OnWriteCompleteCallback callback); + + /** + * Removes a {@link Cache.Entry} from the cache, and calls {@link + * OnWriteCompleteCallback#onWriteComplete} after the operation is finished. + * + * @param key Cache key + * @param callback Callback that's invoked once the entry has been removed + */ + public abstract void remove(String key, OnWriteCompleteCallback callback); +} diff --git a/core/src/main/java/com/android/volley/AsyncNetwork.java b/core/src/main/java/com/android/volley/AsyncNetwork.java new file mode 100644 index 0000000..47f35ea --- /dev/null +++ b/core/src/main/java/com/android/volley/AsyncNetwork.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import androidx.annotation.RestrictTo; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +/** + * An asynchronous implementation of {@link Network} to perform requests. + * + *

WARNING: This API is experimental and subject to breaking changes. Please see + * https://github.com/google/volley/wiki/Asynchronous-Volley for more details. + */ +public abstract class AsyncNetwork implements Network { + private ExecutorService mBlockingExecutor; + private ExecutorService mNonBlockingExecutor; + private ScheduledExecutorService mNonBlockingScheduledExecutor; + + protected AsyncNetwork() {} + + /** Interface for callback to be called after request is processed. */ + public interface OnRequestComplete { + /** Method to be called after successful network request. */ + void onSuccess(NetworkResponse networkResponse); + + /** Method to be called after unsuccessful network request. */ + void onError(VolleyError volleyError); + } + + /** + * Non-blocking method to perform the specified request. + * + * @param request Request to process + * @param callback to be called once NetworkResponse is received + */ + public abstract void performRequest(Request request, OnRequestComplete callback); + + /** + * Blocking method to perform network request. + * + * @param request Request to process + * @return response retrieved from the network + * @throws VolleyError in the event of an error + */ + @Override + public NetworkResponse performRequest(Request request) throws VolleyError { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference response = new AtomicReference<>(); + final AtomicReference 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. + * + *

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. + * + *

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. + * + *

This method must be called before performing any requests. + */ + @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP}) + public void setNonBlockingScheduledExecutor(ScheduledExecutorService executor) { + mNonBlockingScheduledExecutor = executor; + } + + /** Gets blocking executor to perform any potentially blocking tasks. */ + protected ExecutorService getBlockingExecutor() { + return mBlockingExecutor; + } + + /** Gets non-blocking executor to perform any non-blocking tasks. */ + protected ExecutorService getNonBlockingExecutor() { + return mNonBlockingExecutor; + } + + /** Gets scheduled executor to perform any non-blocking tasks that need to be scheduled. */ + protected ScheduledExecutorService getNonBlockingScheduledExecutor() { + return mNonBlockingScheduledExecutor; + } +} diff --git a/core/src/main/java/com/android/volley/AsyncRequestQueue.java b/core/src/main/java/com/android/volley/AsyncRequestQueue.java new file mode 100644 index 0000000..7bf8c21 --- /dev/null +++ b/core/src/main/java/com/android/volley/AsyncRequestQueue.java @@ -0,0 +1,684 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.android.volley.AsyncCache.OnGetCompleteCallback; +import com.android.volley.AsyncNetwork.OnRequestComplete; +import com.android.volley.Cache.Entry; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * An asynchronous request dispatch queue. + * + *

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). + * + *

WARNING: This API is experimental and subject to breaking changes. Please see + * https://github.com/google/volley/wiki/Asynchronous-Volley for more details. + */ +public class AsyncRequestQueue extends RequestQueue { + /** Default number of blocking threads to start. */ + private static final int DEFAULT_BLOCKING_THREAD_POOL_SIZE = 4; + + /** + * AsyncCache used to retrieve and store responses. + * + *

{@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. + * + *

Some tasks in handling requests may not be easy to implement in a non-blocking way, such + * as reading or parsing the response data. This executor is used to run these tasks. + */ + private ExecutorService mBlockingExecutor; + + /** + * This interface may be used by advanced applications to provide custom executors according to + * their needs. Apps must create ExecutorServices dynamically given a blocking queue rather than + * providing them directly so that Volley can provide a PriorityQueue which will prioritize + * requests according to Request#getPriority. + */ + private ExecutorFactory mExecutorFactory; + + /** Manage list of waiting requests and de-duplicate requests with same cache key. */ + private final WaitingRequestManager mWaitingRequestManager = new WaitingRequestManager(this); + + /** + * Requests which have been queued before cache initialization has completed. + * + *

These requests are kicked off once cache initialization finishes. We avoid enqueuing them + * sooner as the cache may not yet be ready. + */ + private final List> mRequestsAwaitingCacheInitialization = new ArrayList<>(); + + private volatile boolean mIsCacheInitialized = false; + private final Object mCacheInitializationLock = new Object[0]; + + /** + * Sets all the variables, but processing does not begin until {@link #start()} is called. + * + * @param cache to use for persisting responses to disk. If an AsyncCache was provided, then + * this will be a {@link ThrowingCache} + * @param network to perform HTTP requests + * @param asyncCache to use for persisting responses to disk. May be null to indicate use of + * blocking cache + * @param responseDelivery interface for posting responses and errors + * @param executorFactory Interface to be used to provide custom executors according to the + * users needs. + */ + private AsyncRequestQueue( + Cache cache, + AsyncNetwork network, + @Nullable AsyncCache asyncCache, + ResponseDelivery responseDelivery, + ExecutorFactory executorFactory) { + super(cache, network, /* threadPoolSize= */ 0, responseDelivery); + mAsyncCache = asyncCache; + mNetwork = network; + mExecutorFactory = executorFactory; + } + + /** Sets the executors and initializes the cache. */ + @Override + public void start() { + stop(); // Make sure any currently running threads are stopped + + // Create blocking / non-blocking executors and set them in the network and stack. + mNonBlockingExecutor = mExecutorFactory.createNonBlockingExecutor(getBlockingQueue()); + mBlockingExecutor = mExecutorFactory.createBlockingExecutor(getBlockingQueue()); + mNonBlockingScheduledExecutor = mExecutorFactory.createNonBlockingScheduledExecutor(); + mNetwork.setBlockingExecutor(mBlockingExecutor); + mNetwork.setNonBlockingExecutor(mNonBlockingExecutor); + mNetwork.setNonBlockingScheduledExecutor(mNonBlockingScheduledExecutor); + + // Kick off cache initialization, which must complete before any requests can be processed. + if (mAsyncCache != null) { + mNonBlockingExecutor.execute( + new Runnable() { + @Override + public void run() { + mAsyncCache.initialize( + new AsyncCache.OnWriteCompleteCallback() { + @Override + public void onWriteComplete() { + onCacheInitializationComplete(); + } + }); + } + }); + } else { + mBlockingExecutor.execute( + new Runnable() { + @Override + public void run() { + getCache().initialize(); + mNonBlockingExecutor.execute( + new Runnable() { + @Override + public void run() { + onCacheInitializationComplete(); + } + }); + } + }); + } + } + + /** Shuts down and nullifies both executors */ + @Override + public void stop() { + if (mNonBlockingExecutor != null) { + mNonBlockingExecutor.shutdownNow(); + mNonBlockingExecutor = null; + } + if (mBlockingExecutor != null) { + mBlockingExecutor.shutdownNow(); + mBlockingExecutor = null; + } + if (mNonBlockingScheduledExecutor != null) { + mNonBlockingScheduledExecutor.shutdownNow(); + mNonBlockingScheduledExecutor = null; + } + } + + /** Begins the request by sending it to the Cache or Network. */ + @Override + void beginRequest(Request request) { + // If the cache hasn't been initialized yet, add the request to a temporary queue to be + // flushed once initialization completes. + if (!mIsCacheInitialized) { + synchronized (mCacheInitializationLock) { + if (!mIsCacheInitialized) { + mRequestsAwaitingCacheInitialization.add(request); + return; + } + } + } + + // If the request is uncacheable, send it over the network. + if (request.shouldCache()) { + if (mAsyncCache != null) { + mNonBlockingExecutor.execute(new CacheTask<>(request)); + } else { + mBlockingExecutor.execute(new CacheTask<>(request)); + } + } else { + sendRequestOverNetwork(request); + } + } + + private void onCacheInitializationComplete() { + List> requestsToDispatch; + synchronized (mCacheInitializationLock) { + requestsToDispatch = new ArrayList<>(mRequestsAwaitingCacheInitialization); + mRequestsAwaitingCacheInitialization.clear(); + mIsCacheInitialized = true; + } + + // Kick off any requests that were queued while waiting for cache initialization. + for (Request request : requestsToDispatch) { + beginRequest(request); + } + } + + @Override + void sendRequestOverNetwork(Request request) { + mNonBlockingExecutor.execute(new NetworkTask<>(request)); + } + + /** Runnable that gets an entry from the cache. */ + private class CacheTask extends RequestTask { + CacheTask(Request request) { + super(request); + } + + @Override + public void run() { + // If the request has been canceled, don't bother dispatching it. + if (mRequest.isCanceled()) { + mRequest.finish("cache-discard-canceled"); + return; + } + + mRequest.addMarker("cache-queue-take"); + + // Attempt to retrieve this item from cache. + if (mAsyncCache != null) { + mAsyncCache.get( + mRequest.getCacheKey(), + new OnGetCompleteCallback() { + @Override + public void onGetComplete(Entry entry) { + handleEntry(entry, mRequest); + } + }); + } else { + Entry entry = getCache().get(mRequest.getCacheKey()); + handleEntry(entry, mRequest); + } + } + } + + /** Helper method that handles the cache entry after getting it from the Cache. */ + private void handleEntry(final Entry entry, final Request mRequest) { + if (entry == null) { + mRequest.addMarker("cache-miss"); + // Cache miss; send off to the network dispatcher. + if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) { + sendRequestOverNetwork(mRequest); + } + return; + } + + // Use a single instant to evaluate cache expiration. Otherwise, a cache entry with + // identical soft and hard TTL times may appear to be valid when checking isExpired but + // invalid upon checking refreshNeeded(), triggering a soft TTL refresh which should be + // impossible. + long currentTimeMillis = System.currentTimeMillis(); + + // If it is completely expired, just send it to the network. + if (entry.isExpired(currentTimeMillis)) { + mRequest.addMarker("cache-hit-expired"); + mRequest.setCacheEntry(entry); + if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) { + sendRequestOverNetwork(mRequest); + } + return; + } + + // We have a cache hit; parse its data for delivery back to the request. + mBlockingExecutor.execute(new CacheParseTask<>(mRequest, entry, currentTimeMillis)); + } + + private class CacheParseTask extends RequestTask { + Cache.Entry entry; + long startTimeMillis; + + CacheParseTask(Request request, Cache.Entry entry, long startTimeMillis) { + super(request); + this.entry = entry; + this.startTimeMillis = startTimeMillis; + } + + @Override + public void run() { + mRequest.addMarker("cache-hit"); + Response response = + mRequest.parseNetworkResponse( + new NetworkResponse( + HttpURLConnection.HTTP_OK, + entry.data, + /* notModified= */ false, + /* networkTimeMs= */ 0, + entry.allResponseHeaders)); + mRequest.addMarker("cache-hit-parsed"); + + if (!entry.refreshNeeded(startTimeMillis)) { + // Completely unexpired cache hit. Just deliver the response. + getResponseDelivery().postResponse(mRequest, response); + } else { + // Soft-expired cache hit. We can deliver the cached response, + // but we need to also send the request to the network for + // refreshing. + mRequest.addMarker("cache-hit-refresh-needed"); + mRequest.setCacheEntry(entry); + // Mark the response as intermediate. + response.intermediate = true; + + if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) { + // Post the intermediate response back to the user and have + // the delivery then forward the request along to the network. + getResponseDelivery() + .postResponse( + mRequest, + response, + new Runnable() { + @Override + public void run() { + sendRequestOverNetwork(mRequest); + } + }); + } else { + // request has been added to list of waiting requests + // to receive the network response from the first request once it + // returns. + getResponseDelivery().postResponse(mRequest, response); + } + } + } + } + + private class ParseErrorTask extends RequestTask { + VolleyError volleyError; + + ParseErrorTask(Request 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 extends RequestTask { + NetworkTask(Request 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 extends RequestTask { + NetworkResponse networkResponse; + + NetworkParseTask(Request 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 extends RequestTask { + Response response; + + CachePutTask(Request request, Response response) { + super(request); + this.response = response; + } + + @Override + public void run() { + if (mAsyncCache != null) { + mAsyncCache.put( + mRequest.getCacheKey(), + response.cacheEntry, + new AsyncCache.OnWriteCompleteCallback() { + @Override + public void onWriteComplete() { + finishRequest(mRequest, response, /* cached= */ true); + } + }); + } else { + getCache().put(mRequest.getCacheKey(), response.cacheEntry); + finishRequest(mRequest, response, /* cached= */ true); + } + } + } + + /** Posts response and notifies listener */ + private void finishRequest(Request mRequest, Response response, boolean cached) { + if (cached) { + mRequest.addMarker("network-cache-written"); + } + // Post the response back. + mRequest.markDelivered(); + getResponseDelivery().postResponse(mRequest, response); + mRequest.notifyListenerResponseReceived(response); + } + + /** + * Factory to create/provide the executors which Volley will use. + * + *

This class may be used by advanced applications to provide custom executors according to + * their needs. + * + *

For applications which rely on setting request priority via {@link Request#getPriority}, a + * task queue is provided which will prioritize requests of higher priority should the thread + * pool itself be exhausted. If a shared pool is provided which does not make use of the given + * queue, then lower-priority requests may have tasks executed before higher-priority requests + * when enough tasks are in flight to fully saturate the shared pool. + */ + public abstract static class ExecutorFactory { + public abstract ExecutorService createNonBlockingExecutor( + BlockingQueue taskQueue); + + public abstract ExecutorService createBlockingExecutor(BlockingQueue taskQueue); + + public abstract ScheduledExecutorService createNonBlockingScheduledExecutor(); + } + + /** Provides a BlockingQueue to be used to create executors. */ + private static PriorityBlockingQueue getBlockingQueue() { + return new PriorityBlockingQueue<>( + /* initialCapacity= */ 11, + new Comparator() { + @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 taskQueue) { + return getNewThreadPoolExecutor( + /* maximumPoolSize= */ 1, + /* threadNameSuffix= */ "Non-BlockingExecutor", + taskQueue); + } + + @Override + public ExecutorService createBlockingExecutor(BlockingQueue 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 taskQueue) { + return new ThreadPoolExecutor( + /* corePoolSize= */ 0, + /* maximumPoolSize= */ maximumPoolSize, + /* keepAliveTime= */ 60, + /* unit= */ TimeUnit.SECONDS, + taskQueue, + getThreadFactory(threadNameSuffix)); + } + + private ThreadFactory getThreadFactory(final String threadNameSuffix) { + return new ThreadFactory() { + @Override + public Thread newThread(@NonNull Runnable runnable) { + Thread t = Executors.defaultThreadFactory().newThread(runnable); + t.setName("Volley-" + threadNameSuffix); + return t; + } + }; + } + }; + } + + public AsyncRequestQueue build() { + // If neither cache is set by the caller, throw an illegal argument exception. + if (mCache == null && mAsyncCache == null) { + throw new IllegalArgumentException("You must set one of the cache objects"); + } + if (mCache == null) { + // if no cache is provided, we will provide one that throws + // UnsupportedOperationExceptions to pass into the parent class. + mCache = new ThrowingCache(); + } + if (mResponseDelivery == null) { + mResponseDelivery = new ExecutorDelivery(new Handler(Looper.getMainLooper())); + } + if (mExecutorFactory == null) { + mExecutorFactory = getDefaultExecutorFactory(); + } + return new AsyncRequestQueue( + mCache, mNetwork, mAsyncCache, mResponseDelivery, mExecutorFactory); + } + } + + /** A cache that throws an error if a method is called. */ + private static class ThrowingCache implements Cache { + @Override + public Entry get(String key) { + throw new UnsupportedOperationException(); + } + + @Override + public void put(String key, Entry entry) { + throw new UnsupportedOperationException(); + } + + @Override + public void initialize() { + throw new UnsupportedOperationException(); + } + + @Override + public void invalidate(String key, boolean fullExpire) { + throw new UnsupportedOperationException(); + } + + @Override + public void remove(String key) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/core/src/main/java/com/android/volley/AuthFailureError.java b/core/src/main/java/com/android/volley/AuthFailureError.java new file mode 100644 index 0000000..fc6417e --- /dev/null +++ b/core/src/main/java/com/android/volley/AuthFailureError.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.content.Intent; + +/** Error indicating that there was an authentication failure when performing a Request. */ +@SuppressWarnings("serial") +public class AuthFailureError extends VolleyError { + /** An intent that can be used to resolve this exception. (Brings up the password dialog.) */ + private Intent mResolutionIntent; + + public AuthFailureError() {} + + public AuthFailureError(Intent intent) { + mResolutionIntent = intent; + } + + public AuthFailureError(NetworkResponse response) { + super(response); + } + + public AuthFailureError(String message) { + super(message); + } + + public AuthFailureError(String message, Exception reason) { + super(message, reason); + } + + public Intent getResolutionIntent() { + return mResolutionIntent; + } + + @Override + public String getMessage() { + if (mResolutionIntent != null) { + return "User needs to (re)enter credentials."; + } + return super.getMessage(); + } +} diff --git a/core/src/main/java/com/android/volley/Cache.java b/core/src/main/java/com/android/volley/Cache.java new file mode 100644 index 0000000..7348d0f --- /dev/null +++ b/core/src/main/java/com/android/volley/Cache.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import androidx.annotation.Nullable; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** An interface for a cache keyed by a String with a byte array as data. */ +public interface Cache { + /** + * Retrieves an entry from the cache. + * + * @param key Cache key + * @return An {@link Entry} or null in the event of a cache miss + */ + @Nullable + Entry get(String key); + + /** + * Adds or replaces an entry to the cache. + * + * @param key Cache key + * @param entry Data to store and metadata for cache coherency, TTL, etc. + */ + void put(String key, Entry entry); + + /** + * Performs any potentially long-running actions needed to initialize the cache; will be called + * from a worker thread. + */ + void initialize(); + + /** + * Invalidates an entry in the cache. + * + * @param key Cache key + * @param fullExpire True to fully expire the entry, false to soft expire + */ + void invalidate(String key, boolean fullExpire); + + /** + * Removes an entry from the cache. + * + * @param key Cache key + */ + void remove(String key); + + /** Empties the cache. */ + void clear(); + + /** Data and metadata for an entry returned by the cache. */ + class Entry { + /** The data returned from cache. */ + public byte[] data; + + /** ETag for cache coherency. */ + public String etag; + + /** Date of this response as reported by the server. */ + public long serverDate; + + /** The last modified date for the requested object. */ + public long lastModified; + + /** TTL for this record. */ + public long ttl; + + /** Soft TTL for this record. */ + public long softTtl; + + /** + * Response headers as received from server; must be non-null. Should not be mutated + * directly. + * + *

Note that if the server returns two headers with the same (case-insensitive) name, + * this map will only contain the one of them. {@link #allResponseHeaders} may contain all + * headers if the {@link Cache} implementation supports it. + */ + public Map responseHeaders = Collections.emptyMap(); + + /** + * All response headers. May be null depending on the {@link Cache} implementation. Should + * not be mutated directly. + */ + public List

allResponseHeaders; + + /** True if the entry is expired. */ + public boolean isExpired() { + return isExpired(System.currentTimeMillis()); + } + + boolean isExpired(long currentTimeMillis) { + return this.ttl < currentTimeMillis; + } + + /** True if a refresh is needed from the original data source. */ + public boolean refreshNeeded() { + return refreshNeeded(System.currentTimeMillis()); + } + + boolean refreshNeeded(long currentTimeMillis) { + return this.softTtl < currentTimeMillis; + } + } +} diff --git a/core/src/main/java/com/android/volley/CacheDispatcher.java b/core/src/main/java/com/android/volley/CacheDispatcher.java new file mode 100644 index 0000000..4443143 --- /dev/null +++ b/core/src/main/java/com/android/volley/CacheDispatcher.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.os.Process; +import androidx.annotation.VisibleForTesting; +import java.util.concurrent.BlockingQueue; + +/** + * Provides a thread for performing cache triage on a queue of requests. + * + *

Requests added to the specified cache queue are resolved from cache. Any deliverable response + * is posted back to the caller via a {@link ResponseDelivery}. Cache misses and responses that + * require refresh are enqueued on the specified network queue for processing by a {@link + * NetworkDispatcher}. + */ +public class CacheDispatcher extends Thread { + + private static final boolean DEBUG = VolleyLog.DEBUG; + + /** The queue of requests coming in for triage. */ + private final BlockingQueue> mCacheQueue; + + /** The queue of requests going out to the network. */ + private final BlockingQueue> mNetworkQueue; + + /** The cache to read from. */ + private final Cache mCache; + + /** For posting responses. */ + private final ResponseDelivery mDelivery; + + /** Used for telling us to die. */ + private volatile boolean mQuit = false; + + /** Manage list of waiting requests and de-duplicate requests with same cache key. */ + private final WaitingRequestManager mWaitingRequestManager; + + /** + * Creates a new cache triage dispatcher thread. You must call {@link #start()} in order to + * begin processing. + * + * @param cacheQueue Queue of incoming requests for triage + * @param networkQueue Queue to post requests that require network to + * @param cache Cache interface to use for resolution + * @param delivery Delivery interface to use for posting responses + */ + public CacheDispatcher( + BlockingQueue> cacheQueue, + BlockingQueue> networkQueue, + Cache cache, + ResponseDelivery delivery) { + mCacheQueue = cacheQueue; + mNetworkQueue = networkQueue; + mCache = cache; + mDelivery = delivery; + mWaitingRequestManager = new WaitingRequestManager(this, networkQueue, delivery); + } + + /** + * Forces this dispatcher to quit immediately. If any requests are still in the queue, they are + * not guaranteed to be processed. + */ + public void quit() { + mQuit = true; + interrupt(); + } + + @Override + public void run() { + if (DEBUG) VolleyLog.v("start new dispatcher"); + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + // Make a blocking call to initialize the cache. + mCache.initialize(); + + while (true) { + try { + processRequest(); + } catch (InterruptedException e) { + // We may have been interrupted because it was time to quit. + if (mQuit) { + Thread.currentThread().interrupt(); + return; + } + VolleyLog.e( + "Ignoring spurious interrupt of CacheDispatcher thread; " + + "use quit() to terminate it"); + } + } + } + + // Extracted to its own method to ensure locals have a constrained liveness scope by the GC. + // This is needed to avoid keeping previous request references alive for an indeterminate amount + // of time. Update consumer-proguard-rules.pro when modifying this. See also + // https://github.com/google/volley/issues/114 + private void processRequest() throws InterruptedException { + // Get a request from the cache triage queue, blocking until + // at least one is available. + final Request request = mCacheQueue.take(); + processRequest(request); + } + + @VisibleForTesting + void processRequest(final Request request) throws InterruptedException { + request.addMarker("cache-queue-take"); + request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED); + + try { + // If the request has been canceled, don't bother dispatching it. + if (request.isCanceled()) { + request.finish("cache-discard-canceled"); + return; + } + + // Attempt to retrieve this item from cache. + Cache.Entry entry = mCache.get(request.getCacheKey()); + if (entry == null) { + request.addMarker("cache-miss"); + // Cache miss; send off to the network dispatcher. + if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { + mNetworkQueue.put(request); + } + return; + } + + // Use a single instant to evaluate cache expiration. Otherwise, a cache entry with + // identical soft and hard TTL times may appear to be valid when checking isExpired but + // invalid upon checking refreshNeeded(), triggering a soft TTL refresh which should be + // impossible. + long currentTimeMillis = System.currentTimeMillis(); + + // If it is completely expired, just send it to the network. + if (entry.isExpired(currentTimeMillis)) { + request.addMarker("cache-hit-expired"); + request.setCacheEntry(entry); + if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { + mNetworkQueue.put(request); + } + return; + } + + // We have a cache hit; parse its data for delivery back to the request. + request.addMarker("cache-hit"); + Response response = + request.parseNetworkResponse( + new NetworkResponse(entry.data, entry.responseHeaders)); + request.addMarker("cache-hit-parsed"); + + if (!response.isSuccess()) { + request.addMarker("cache-parsing-failed"); + mCache.invalidate(request.getCacheKey(), true); + request.setCacheEntry(null); + if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { + mNetworkQueue.put(request); + } + return; + } + if (!entry.refreshNeeded(currentTimeMillis)) { + // Completely unexpired cache hit. Just deliver the response. + mDelivery.postResponse(request, response); + } else { + // Soft-expired cache hit. We can deliver the cached response, + // but we need to also send the request to the network for + // refreshing. + request.addMarker("cache-hit-refresh-needed"); + request.setCacheEntry(entry); + // Mark the response as intermediate. + response.intermediate = true; + + if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { + // Post the intermediate response back to the user and have + // the delivery then forward the request along to the network. + mDelivery.postResponse( + request, + response, + new Runnable() { + @Override + public void run() { + try { + mNetworkQueue.put(request); + } catch (InterruptedException e) { + // Restore the interrupted status + Thread.currentThread().interrupt(); + } + } + }); + } else { + // request has been added to list of waiting requests + // to receive the network response from the first request once it returns. + mDelivery.postResponse(request, response); + } + } + } finally { + request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED); + } + } +} diff --git a/core/src/main/java/com/android/volley/ClientError.java b/core/src/main/java/com/android/volley/ClientError.java new file mode 100644 index 0000000..521b76f --- /dev/null +++ b/core/src/main/java/com/android/volley/ClientError.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * Indicates that the server responded with an error response indicating that the client has erred. + * + *

For backwards compatibility, extends ServerError which used to be thrown for all server + * errors, including 4xx error codes indicating a client error. + */ +@SuppressWarnings("serial") +public class ClientError extends ServerError { + public ClientError(NetworkResponse networkResponse) { + super(networkResponse); + } + + public ClientError() { + super(); + } +} diff --git a/core/src/main/java/com/android/volley/DefaultRetryPolicy.java b/core/src/main/java/com/android/volley/DefaultRetryPolicy.java new file mode 100644 index 0000000..4be6b50 --- /dev/null +++ b/core/src/main/java/com/android/volley/DefaultRetryPolicy.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** Default retry policy for requests. */ +public class DefaultRetryPolicy implements RetryPolicy { + /** The current timeout in milliseconds. */ + private int mCurrentTimeoutMs; + + /** The current retry count. */ + private int mCurrentRetryCount; + + /** The maximum number of attempts. */ + private final int mMaxNumRetries; + + /** The backoff multiplier for the policy. */ + private final float mBackoffMultiplier; + + /** The default socket timeout in milliseconds */ + public static final int DEFAULT_TIMEOUT_MS = 2500; + + /** The default number of retries */ + public static final int DEFAULT_MAX_RETRIES = 1; + + /** The default backoff multiplier */ + public static final float DEFAULT_BACKOFF_MULT = 1f; + + /** Constructs a new retry policy using the default timeouts. */ + public DefaultRetryPolicy() { + this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT); + } + + /** + * Constructs a new retry policy. + * + * @param initialTimeoutMs The initial timeout for the policy. + * @param maxNumRetries The maximum number of retries. + * @param backoffMultiplier Backoff multiplier for the policy. + */ + public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) { + mCurrentTimeoutMs = initialTimeoutMs; + mMaxNumRetries = maxNumRetries; + mBackoffMultiplier = backoffMultiplier; + } + + /** Returns the current timeout. */ + @Override + public int getCurrentTimeout() { + return mCurrentTimeoutMs; + } + + /** Returns the current retry count. */ + @Override + public int getCurrentRetryCount() { + return mCurrentRetryCount; + } + + /** Returns the backoff multiplier for the policy. */ + public float getBackoffMultiplier() { + return mBackoffMultiplier; + } + + /** + * Prepares for the next retry by applying a backoff to the timeout. + * + * @param error The error code of the last attempt. + */ + @Override + public void retry(VolleyError error) throws VolleyError { + mCurrentRetryCount++; + mCurrentTimeoutMs += (int) (mCurrentTimeoutMs * mBackoffMultiplier); + if (!hasAttemptRemaining()) { + throw error; + } + } + + /** Returns true if this policy has attempts remaining, false otherwise. */ + protected boolean hasAttemptRemaining() { + return mCurrentRetryCount <= mMaxNumRetries; + } +} diff --git a/core/src/main/java/com/android/volley/ExecutorDelivery.java b/core/src/main/java/com/android/volley/ExecutorDelivery.java new file mode 100644 index 0000000..fd992f9 --- /dev/null +++ b/core/src/main/java/com/android/volley/ExecutorDelivery.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.os.Handler; +import java.util.concurrent.Executor; + +/** Delivers responses and errors. */ +public class ExecutorDelivery implements ResponseDelivery { + /** Used for posting responses, typically to the main thread. */ + private final Executor mResponsePoster; + + /** + * Creates a new response delivery interface. + * + * @param handler {@link Handler} to post responses on + */ + public ExecutorDelivery(final Handler handler) { + // Make an Executor that just wraps the handler. + mResponsePoster = + new Executor() { + @Override + public void execute(Runnable command) { + handler.post(command); + } + }; + } + + /** + * Creates a new response delivery interface, mockable version for testing. + * + * @param executor For running delivery tasks + */ + public ExecutorDelivery(Executor executor) { + mResponsePoster = executor; + } + + @Override + public void postResponse(Request request, Response response) { + postResponse(request, response, null); + } + + @Override + public void postResponse(Request request, Response response, Runnable runnable) { + request.markDelivered(); + request.addMarker("post-response"); + mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable)); + } + + @Override + public void postError(Request request, VolleyError error) { + request.addMarker("post-error"); + Response response = Response.error(error); + mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null)); + } + + /** A Runnable used for delivering network responses to a listener on the main thread. */ + @SuppressWarnings("rawtypes") + private static class ResponseDeliveryRunnable implements Runnable { + private final Request mRequest; + private final Response mResponse; + private final Runnable mRunnable; + + public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) { + mRequest = request; + mResponse = response; + mRunnable = runnable; + } + + @SuppressWarnings("unchecked") + @Override + public void run() { + // NOTE: If cancel() is called off the thread that we're currently running in (by + // default, the main thread), we cannot guarantee that deliverResponse()/deliverError() + // won't be called, since it may be canceled after we check isCanceled() but before we + // deliver the response. Apps concerned about this guarantee must either call cancel() + // from the same thread or implement their own guarantee about not invoking their + // listener after cancel() has been called. + + // If this request has canceled, finish it and don't deliver. + if (mRequest.isCanceled()) { + mRequest.finish("canceled-at-delivery"); + return; + } + + // Deliver a normal response or error, depending. + if (mResponse.isSuccess()) { + mRequest.deliverResponse(mResponse.result); + } else { + mRequest.deliverError(mResponse.error); + } + + // If this is an intermediate response, add a marker, otherwise we're done + // and the request can be finished. + if (mResponse.intermediate) { + mRequest.addMarker("intermediate-response"); + } else { + mRequest.finish("done"); + } + + // If we have been provided a post-delivery runnable, run it. + if (mRunnable != null) { + mRunnable.run(); + } + } + } +} diff --git a/core/src/main/java/com/android/volley/Header.java b/core/src/main/java/com/android/volley/Header.java new file mode 100644 index 0000000..cd9c6ec --- /dev/null +++ b/core/src/main/java/com/android/volley/Header.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.volley; + +import android.text.TextUtils; + +/** An HTTP header. */ +public final class Header { + private final String mName; + private final String mValue; + + public Header(String name, String value) { + mName = name; + mValue = value; + } + + public final String getName() { + return mName; + } + + public final String getValue() { + return mValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Header header = (Header) o; + + return TextUtils.equals(mName, header.mName) && TextUtils.equals(mValue, header.mValue); + } + + @Override + public int hashCode() { + int result = mName.hashCode(); + result = 31 * result + mValue.hashCode(); + return result; + } + + @Override + public String toString() { + return "Header[name=" + mName + ",value=" + mValue + "]"; + } +} diff --git a/core/src/main/java/com/android/volley/Network.java b/core/src/main/java/com/android/volley/Network.java new file mode 100644 index 0000000..16d5858 --- /dev/null +++ b/core/src/main/java/com/android/volley/Network.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** An interface for performing requests. */ +public interface Network { + /** + * Performs the specified request. + * + * @param request Request to process + * @return A {@link NetworkResponse} with data and caching metadata; will never be null + * @throws VolleyError on errors + */ + NetworkResponse performRequest(Request request) throws VolleyError; +} diff --git a/core/src/main/java/com/android/volley/NetworkDispatcher.java b/core/src/main/java/com/android/volley/NetworkDispatcher.java new file mode 100644 index 0000000..06057c3 --- /dev/null +++ b/core/src/main/java/com/android/volley/NetworkDispatcher.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.annotation.TargetApi; +import android.net.TrafficStats; +import android.os.Build; +import android.os.Process; +import android.os.SystemClock; +import androidx.annotation.VisibleForTesting; +import java.util.concurrent.BlockingQueue; + +/** + * Provides a thread for performing network dispatch from a queue of requests. + * + *

Requests added to the specified queue are processed from the network via a specified {@link + * Network} interface. Responses are committed to cache, if eligible, using a specified {@link + * Cache} interface. Valid responses and errors are posted back to the caller via a {@link + * ResponseDelivery}. + */ +public class NetworkDispatcher extends Thread { + + /** The queue of requests to service. */ + private final BlockingQueue> mQueue; + /** The network interface for processing requests. */ + private final Network mNetwork; + /** The cache to write to. */ + private final Cache mCache; + /** For posting responses and errors. */ + private final ResponseDelivery mDelivery; + /** Used for telling us to die. */ + private volatile boolean mQuit = false; + + /** + * Creates a new network dispatcher thread. You must call {@link #start()} in order to begin + * processing. + * + * @param queue Queue of incoming requests for triage + * @param network Network interface to use for performing requests + * @param cache Cache interface to use for writing responses to cache + * @param delivery Delivery interface to use for posting responses + */ + public NetworkDispatcher( + BlockingQueue> queue, + Network network, + Cache cache, + ResponseDelivery delivery) { + mQueue = queue; + mNetwork = network; + mCache = cache; + mDelivery = delivery; + } + + /** + * Forces this dispatcher to quit immediately. If any requests are still in the queue, they are + * not guaranteed to be processed. + */ + public void quit() { + mQuit = true; + interrupt(); + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + private void addTrafficStatsTag(Request request) { + // Tag the request (if API >= 14) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + TrafficStats.setThreadStatsTag(request.getTrafficStatsTag()); + } + } + + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + while (true) { + try { + processRequest(); + } catch (InterruptedException e) { + // We may have been interrupted because it was time to quit. + if (mQuit) { + Thread.currentThread().interrupt(); + return; + } + VolleyLog.e( + "Ignoring spurious interrupt of NetworkDispatcher thread; " + + "use quit() to terminate it"); + } + } + } + + // Extracted to its own method to ensure locals have a constrained liveness scope by the GC. + // This is needed to avoid keeping previous request references alive for an indeterminate amount + // of time. Update consumer-proguard-rules.pro when modifying this. See also + // https://github.com/google/volley/issues/114 + private void processRequest() throws InterruptedException { + // Take a request from the queue. + Request request = mQueue.take(); + processRequest(request); + } + + @VisibleForTesting + void processRequest(Request request) { + long startTimeMs = SystemClock.elapsedRealtime(); + request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + try { + request.addMarker("network-queue-take"); + + // If the request was cancelled already, do not perform the + // network request. + if (request.isCanceled()) { + request.finish("network-discard-cancelled"); + request.notifyListenerResponseNotUsable(); + return; + } + + addTrafficStatsTag(request); + + // Perform the network request. + NetworkResponse networkResponse = mNetwork.performRequest(request); + request.addMarker("network-http-complete"); + + // If the server returned 304 AND we delivered a response already, + // we're done -- don't deliver a second identical response. + if (networkResponse.notModified && request.hasHadResponseDelivered()) { + request.finish("not-modified"); + request.notifyListenerResponseNotUsable(); + return; + } + + // Parse the response here on the worker thread. + Response response = request.parseNetworkResponse(networkResponse); + request.addMarker("network-parse-complete"); + + // Write to cache if applicable. + // TODO: Only update cache metadata instead of entire record for 304s. + if (request.shouldCache() && response.cacheEntry != null) { + mCache.put(request.getCacheKey(), response.cacheEntry); + request.addMarker("network-cache-written"); + } + + // Post the response back. + request.markDelivered(); + mDelivery.postResponse(request, response); + request.notifyListenerResponseReceived(response); + } catch (VolleyError volleyError) { + volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); + parseAndDeliverNetworkError(request, volleyError); + request.notifyListenerResponseNotUsable(); + } catch (Exception e) { + VolleyLog.e(e, "Unhandled exception %s", e.toString()); + VolleyError volleyError = new VolleyError(e); + volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); + mDelivery.postError(request, volleyError); + request.notifyListenerResponseNotUsable(); + } finally { + request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED); + } + } + + private void parseAndDeliverNetworkError(Request request, VolleyError error) { + error = request.parseNetworkError(error); + mDelivery.postError(request, error); + } +} diff --git a/core/src/main/java/com/android/volley/NetworkError.java b/core/src/main/java/com/android/volley/NetworkError.java new file mode 100644 index 0000000..6b2b19f --- /dev/null +++ b/core/src/main/java/com/android/volley/NetworkError.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** Indicates that there was a network error when performing a Volley request. */ +@SuppressWarnings("serial") +public class NetworkError extends VolleyError { + public NetworkError() { + super(); + } + + public NetworkError(Throwable cause) { + super(cause); + } + + public NetworkError(NetworkResponse networkResponse) { + super(networkResponse); + } +} diff --git a/core/src/main/java/com/android/volley/NetworkResponse.java b/core/src/main/java/com/android/volley/NetworkResponse.java new file mode 100644 index 0000000..cfbc371 --- /dev/null +++ b/core/src/main/java/com/android/volley/NetworkResponse.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import androidx.annotation.Nullable; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** Data and headers returned from {@link Network#performRequest(Request)}. */ +public class NetworkResponse { + + /** + * Creates a new network response. + * + * @param statusCode the HTTP status code + * @param data Response body + * @param headers Headers returned with this response, or null for none + * @param notModified True if the server returned a 304 and the data was already in cache + * @param networkTimeMs Round-trip network time to receive network response + * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor + * cannot handle server responses containing multiple headers with the same name. This + * constructor may be removed in a future release of Volley. + */ + @Deprecated + public NetworkResponse( + int statusCode, + byte[] data, + @Nullable Map headers, + boolean notModified, + long networkTimeMs) { + this(statusCode, data, headers, toAllHeaderList(headers), notModified, networkTimeMs); + } + + /** + * Creates a new network response. + * + * @param statusCode the HTTP status code + * @param data Response body + * @param notModified True if the server returned a 304 and the data was already in cache + * @param networkTimeMs Round-trip network time to receive network response + * @param allHeaders All headers returned with this response, or null for none + */ + public NetworkResponse( + int statusCode, + byte[] data, + boolean notModified, + long networkTimeMs, + @Nullable List

allHeaders) { + this(statusCode, data, toHeaderMap(allHeaders), allHeaders, notModified, networkTimeMs); + } + + /** + * Creates a new network response. + * + * @param statusCode the HTTP status code + * @param data Response body + * @param headers Headers returned with this response, or null for none + * @param notModified True if the server returned a 304 and the data was already in cache + * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor + * cannot handle server responses containing multiple headers with the same name. This + * constructor may be removed in a future release of Volley. + */ + @Deprecated + public NetworkResponse( + int statusCode, + byte[] data, + @Nullable Map headers, + boolean notModified) { + this(statusCode, data, headers, notModified, /* networkTimeMs= */ 0); + } + + /** + * Creates a new network response for an OK response with no headers. + * + * @param data Response body + */ + public NetworkResponse(byte[] data) { + this( + HttpURLConnection.HTTP_OK, + data, + /* notModified= */ false, + /* networkTimeMs= */ 0, + Collections.
emptyList()); + } + + /** + * Creates a new network response for an OK response. + * + * @param data Response body + * @param headers Headers returned with this response, or null for none + * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor + * cannot handle server responses containing multiple headers with the same name. This + * constructor may be removed in a future release of Volley. + */ + @Deprecated + public NetworkResponse(byte[] data, @Nullable Map headers) { + this( + HttpURLConnection.HTTP_OK, + data, + headers, + /* notModified= */ false, + /* networkTimeMs= */ 0); + } + + private NetworkResponse( + int statusCode, + byte[] data, + @Nullable Map headers, + @Nullable List
allHeaders, + boolean notModified, + long networkTimeMs) { + this.statusCode = statusCode; + this.data = data; + this.headers = headers; + if (allHeaders == null) { + this.allHeaders = null; + } else { + this.allHeaders = Collections.unmodifiableList(allHeaders); + } + this.notModified = notModified; + this.networkTimeMs = networkTimeMs; + } + + /** The HTTP status code. */ + public final int statusCode; + + /** Raw data from this response. */ + public final byte[] data; + + /** + * Response headers. + * + *

This map is case-insensitive. It should not be mutated directly. + * + *

Note that if the server returns two headers with the same (case-insensitive) name, this + * map will only contain the last one. Use {@link #allHeaders} to inspect all headers returned + * by the server. + */ + @Nullable public final Map headers; + + /** All response headers. Must not be mutated directly. */ + @Nullable public final List

allHeaders; + + /** True if the server returned a 304 (Not Modified). */ + public final boolean notModified; + + /** Network roundtrip time in milliseconds. */ + public final long networkTimeMs; + + @Nullable + private static Map toHeaderMap(@Nullable List
allHeaders) { + if (allHeaders == null) { + return null; + } + if (allHeaders.isEmpty()) { + return Collections.emptyMap(); + } + Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + // Later elements in the list take precedence. + for (Header header : allHeaders) { + headers.put(header.getName(), header.getValue()); + } + return headers; + } + + @Nullable + private static List
toAllHeaderList(@Nullable Map headers) { + if (headers == null) { + return null; + } + if (headers.isEmpty()) { + return Collections.emptyList(); + } + List
allHeaders = new ArrayList<>(headers.size()); + for (Map.Entry header : headers.entrySet()) { + allHeaders.add(new Header(header.getKey(), header.getValue())); + } + return allHeaders; + } +} diff --git a/core/src/main/java/com/android/volley/NoConnectionError.java b/core/src/main/java/com/android/volley/NoConnectionError.java new file mode 100644 index 0000000..185eb35 --- /dev/null +++ b/core/src/main/java/com/android/volley/NoConnectionError.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** Error indicating that no connection could be established when performing a Volley request. */ +@SuppressWarnings("serial") +public class NoConnectionError extends NetworkError { + public NoConnectionError() { + super(); + } + + public NoConnectionError(Throwable reason) { + super(reason); + } +} diff --git a/core/src/main/java/com/android/volley/ParseError.java b/core/src/main/java/com/android/volley/ParseError.java new file mode 100644 index 0000000..04a9d58 --- /dev/null +++ b/core/src/main/java/com/android/volley/ParseError.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** Indicates that the server's response could not be parsed. */ +@SuppressWarnings("serial") +public class ParseError extends VolleyError { + public ParseError() {} + + public ParseError(NetworkResponse networkResponse) { + super(networkResponse); + } + + public ParseError(Throwable cause) { + super(cause); + } +} diff --git a/core/src/main/java/com/android/volley/Request.java b/core/src/main/java/com/android/volley/Request.java new file mode 100644 index 0000000..df0d18f --- /dev/null +++ b/core/src/main/java/com/android/volley/Request.java @@ -0,0 +1,723 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.net.TrafficStats; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import androidx.annotation.CallSuper; +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; +import com.android.volley.VolleyLog.MarkerLog; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.Map; + +/** + * Base class for all network requests. + * + * @param The type of parsed response this request expects. + */ +public abstract class Request implements Comparable> { + + /** Default encoding for POST or PUT parameters. See {@link #getParamsEncoding()}. */ + private static final String DEFAULT_PARAMS_ENCODING = "UTF-8"; + + /** Supported request methods. */ + public interface Method { + int DEPRECATED_GET_OR_POST = -1; + int GET = 0; + int POST = 1; + int PUT = 2; + int DELETE = 3; + int HEAD = 4; + int OPTIONS = 5; + int TRACE = 6; + int PATCH = 7; + } + + /** Callback to notify when the network request returns. */ + /* package */ interface NetworkRequestCompleteListener { + + /** Callback when a network response has been received. */ + void onResponseReceived(Request request, Response response); + + /** Callback when request returns from network without valid response. */ + void onNoUsableResponseReceived(Request request); + } + + /** An event log tracing the lifetime of this request; for debugging. */ + private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null; + + /** + * Request method of this request. Currently supports GET, POST, PUT, DELETE, HEAD, OPTIONS, + * TRACE, and PATCH. + */ + private final int mMethod; + + /** URL of this request. */ + private final String mUrl; + + /** Default tag for {@link TrafficStats}. */ + private final int mDefaultTrafficStatsTag; + + /** Lock to guard state which can be mutated after a request is added to the queue. */ + private final Object mLock = new Object(); + + /** Listener interface for errors. */ + @Nullable + @GuardedBy("mLock") + private Response.ErrorListener mErrorListener; + + /** Sequence number of this request, used to enforce FIFO ordering. */ + private Integer mSequence; + + /** The request queue this request is associated with. */ + private RequestQueue mRequestQueue; + + /** Whether or not responses to this request should be cached. */ + // TODO(#190): Turn this off by default for anything other than GET requests. + private boolean mShouldCache = true; + + /** Whether or not this request has been canceled. */ + @GuardedBy("mLock") + private boolean mCanceled = false; + + /** Whether or not a response has been delivered for this request yet. */ + @GuardedBy("mLock") + private boolean mResponseDelivered = false; + + /** Whether the request should be retried in the event of an HTTP 5xx (server) error. */ + private boolean mShouldRetryServerErrors = false; + + /** Whether the request should be retried in the event of a {@link NoConnectionError}. */ + private boolean mShouldRetryConnectionErrors = false; + + /** The retry policy for this request. */ + private RetryPolicy mRetryPolicy; + + /** + * When a request can be retrieved from cache but must be refreshed from the network, the cache + * entry will be stored here so that in the event of a "Not Modified" response, we can be sure + * it hasn't been evicted from cache. + */ + @Nullable private Cache.Entry mCacheEntry = null; + + /** An opaque token tagging this request; used for bulk cancellation. */ + private Object mTag; + + /** Listener that will be notified when a response has been delivered. */ + @GuardedBy("mLock") + private NetworkRequestCompleteListener mRequestCompleteListener; + + /** + * Creates a new request with the given URL and error listener. Note that the normal response + * listener is not provided here as delivery of responses is provided by subclasses, who have a + * better idea of how to deliver an already-parsed response. + * + * @deprecated Use {@link #Request(int, String, com.android.volley.Response.ErrorListener)}. + */ + @Deprecated + public Request(String url, Response.ErrorListener errorListener) { + this(Method.DEPRECATED_GET_OR_POST, url, errorListener); + } + + /** + * Creates a new request with the given method (one of the values from {@link Method}), URL, and + * error listener. Note that the normal response listener is not provided here as delivery of + * responses is provided by subclasses, who have a better idea of how to deliver an + * already-parsed response. + * + * @param method the HTTP method to use + * @param url URL to fetch the response from + * @param errorListener Error listener, or null to ignore errors. + */ + public Request(int method, String url, @Nullable Response.ErrorListener errorListener) { + mMethod = method; + mUrl = url; + mErrorListener = errorListener; + setRetryPolicy(new DefaultRetryPolicy()); + + mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url); + } + + /** Return the method for this request. Can be one of the values in {@link Method}. */ + public int getMethod() { + return mMethod; + } + + /** + * Set a tag on this request. Can be used to cancel all requests with this tag by {@link + * RequestQueue#cancelAll(Object)}. + * + * @return This Request object to allow for chaining. + */ + public Request setTag(Object tag) { + mTag = tag; + return this; + } + + /** + * Returns this request's tag. + * + * @see Request#setTag(Object) + */ + public Object getTag() { + return mTag; + } + + /** @return this request's {@link com.android.volley.Response.ErrorListener}. */ + @Nullable + public Response.ErrorListener getErrorListener() { + synchronized (mLock) { + return mErrorListener; + } + } + + /** @return A tag for use with {@link TrafficStats#setThreadStatsTag(int)} */ + public int getTrafficStatsTag() { + return mDefaultTrafficStatsTag; + } + + /** @return The hashcode of the URL's host component, or 0 if there is none. */ + private static int findDefaultTrafficStatsTag(String url) { + if (!TextUtils.isEmpty(url)) { + Uri uri = Uri.parse(url); + if (uri != null) { + String host = uri.getHost(); + if (host != null) { + return host.hashCode(); + } + } + } + return 0; + } + + /** + * Sets the retry policy for this request. + * + * @return This Request object to allow for chaining. + */ + public Request setRetryPolicy(RetryPolicy retryPolicy) { + mRetryPolicy = retryPolicy; + return this; + } + + /** Adds an event to this request's event log; for debugging. */ + public void addMarker(String tag) { + if (MarkerLog.ENABLED) { + mEventLog.add(tag, Thread.currentThread().getId()); + } + } + + /** + * Notifies the request queue that this request has finished (successfully or with error). + * + *

Also dumps all events from this request's event log; for debugging. + */ + void finish(final String tag) { + if (mRequestQueue != null) { + mRequestQueue.finish(this); + } + if (MarkerLog.ENABLED) { + final long threadId = Thread.currentThread().getId(); + if (Looper.myLooper() != Looper.getMainLooper()) { + // If we finish marking off of the main thread, we need to + // actually do it on the main thread to ensure correct ordering. + Handler mainThread = new Handler(Looper.getMainLooper()); + mainThread.post( + new Runnable() { + @Override + public void run() { + mEventLog.add(tag, threadId); + mEventLog.finish(Request.this.toString()); + } + }); + return; + } + + mEventLog.add(tag, threadId); + mEventLog.finish(this.toString()); + } + } + + void sendEvent(@RequestQueue.RequestEvent int event) { + if (mRequestQueue != null) { + mRequestQueue.sendRequestEvent(this, event); + } + } + + /** + * Associates this request with the given queue. The request queue will be notified when this + * request has finished. + * + * @return This Request object to allow for chaining. + */ + public Request setRequestQueue(RequestQueue requestQueue) { + mRequestQueue = requestQueue; + return this; + } + + /** + * Sets the sequence number of this request. Used by {@link RequestQueue}. + * + * @return This Request object to allow for chaining. + */ + public final Request setSequence(int sequence) { + mSequence = sequence; + return this; + } + + /** Returns the sequence number of this request. */ + public final int getSequence() { + if (mSequence == null) { + throw new IllegalStateException("getSequence called before setSequence"); + } + return mSequence; + } + + /** Returns the URL of this request. */ + public String getUrl() { + return mUrl; + } + + /** Returns the cache key for this request. By default, this is the URL. */ + public String getCacheKey() { + String url = getUrl(); + // If this is a GET request, just use the URL as the key. + // For callers using DEPRECATED_GET_OR_POST, we assume the method is GET, which matches + // legacy behavior where all methods had the same cache key. We can't determine which method + // will be used because doing so requires calling getPostBody() which is expensive and may + // throw AuthFailureError. + // TODO(#190): Remove support for non-GET methods. + int method = getMethod(); + if (method == Method.GET || method == Method.DEPRECATED_GET_OR_POST) { + return url; + } + return Integer.toString(method) + '-' + url; + } + + /** + * Annotates this request with an entry retrieved for it from cache. Used for cache coherency + * support. + * + * @return This Request object to allow for chaining. + */ + public Request setCacheEntry(Cache.Entry entry) { + mCacheEntry = entry; + return this; + } + + /** Returns the annotated cache entry, or null if there isn't one. */ + @Nullable + public Cache.Entry getCacheEntry() { + return mCacheEntry; + } + + /** + * Mark this request as canceled. + * + *

No callback will be delivered as long as either: + * + *

    + *
  • This method is called on the same thread as the {@link ResponseDelivery} is running on. + * By default, this is the main thread. + *
  • The request subclass being used overrides cancel() and ensures that it does not invoke + * the listener in {@link #deliverResponse} after cancel() has been called in a + * thread-safe manner. + *
+ * + *

There are no guarantees if both of these conditions aren't met. + */ + @CallSuper + public void cancel() { + synchronized (mLock) { + mCanceled = true; + mErrorListener = null; + } + } + + /** Returns true if this request has been canceled. */ + public boolean isCanceled() { + synchronized (mLock) { + return mCanceled; + } + } + + /** + * Returns a list of extra HTTP headers to go along with this request. Can throw {@link + * AuthFailureError} as authentication may be required to provide these values. + * + * @throws AuthFailureError In the event of auth failure + */ + public Map getHeaders() throws AuthFailureError { + return Collections.emptyMap(); + } + + /** + * Returns a Map of POST parameters to be used for this request, or null if a simple GET should + * be used. Can throw {@link AuthFailureError} as authentication may be required to provide + * these values. + * + *

Note that only one of getPostParams() and getPostBody() can return a non-null value. + * + * @throws AuthFailureError In the event of auth failure + * @deprecated Use {@link #getParams()} instead. + */ + @Deprecated + @Nullable + protected Map getPostParams() throws AuthFailureError { + return getParams(); + } + + /** + * Returns which encoding should be used when converting POST parameters returned by {@link + * #getPostParams()} into a raw POST body. + * + *

This controls both encodings: + * + *

    + *
  1. The string encoding used when converting parameter names and values into bytes prior to + * URL encoding them. + *
  2. The string encoding used when converting the URL encoded parameters into a raw byte + * array. + *
+ * + * @deprecated Use {@link #getParamsEncoding()} instead. + */ + @Deprecated + protected String getPostParamsEncoding() { + return getParamsEncoding(); + } + + /** @deprecated Use {@link #getBodyContentType()} instead. */ + @Deprecated + public String getPostBodyContentType() { + return getBodyContentType(); + } + + /** + * Returns the raw POST body to be sent. + * + * @throws AuthFailureError In the event of auth failure + * @deprecated Use {@link #getBody()} instead. + */ + @Deprecated + public byte[] getPostBody() throws AuthFailureError { + // Note: For compatibility with legacy clients of volley, this implementation must remain + // here instead of simply calling the getBody() function because this function must + // call getPostParams() and getPostParamsEncoding() since legacy clients would have + // overridden these two member functions for POST requests. + Map postParams = getPostParams(); + if (postParams != null && postParams.size() > 0) { + return encodeParameters(postParams, getPostParamsEncoding()); + } + return null; + } + + /** + * Returns a Map of parameters to be used for a POST or PUT request. Can throw {@link + * AuthFailureError} as authentication may be required to provide these values. + * + *

Note that you can directly override {@link #getBody()} for custom data. + * + * @throws AuthFailureError in the event of auth failure + */ + @Nullable + protected Map getParams() throws AuthFailureError { + return null; + } + + /** + * Returns which encoding should be used when converting POST or PUT parameters returned by + * {@link #getParams()} into a raw POST or PUT body. + * + *

This controls both encodings: + * + *

    + *
  1. The string encoding used when converting parameter names and values into bytes prior to + * URL encoding them. + *
  2. The string encoding used when converting the URL encoded parameters into a raw byte + * array. + *
+ */ + protected String getParamsEncoding() { + return DEFAULT_PARAMS_ENCODING; + } + + /** Returns the content type of the POST or PUT body. */ + public String getBodyContentType() { + return "application/x-www-form-urlencoded; charset=" + getParamsEncoding(); + } + + /** + * Returns the raw POST or PUT body to be sent. + * + *

By default, the body consists of the request parameters in + * application/x-www-form-urlencoded format. When overriding this method, consider overriding + * {@link #getBodyContentType()} as well to match the new body format. + * + * @throws AuthFailureError in the event of auth failure + */ + public byte[] getBody() throws AuthFailureError { + Map params = getParams(); + if (params != null && params.size() > 0) { + return encodeParameters(params, getParamsEncoding()); + } + return null; + } + + /** Converts params into an application/x-www-form-urlencoded encoded string. */ + private byte[] encodeParameters(Map params, String paramsEncoding) { + StringBuilder encodedParams = new StringBuilder(); + try { + for (Map.Entry entry : params.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + throw new IllegalArgumentException( + String.format( + "Request#getParams() or Request#getPostParams() returned a map " + + "containing a null key or value: (%s, %s). All keys " + + "and values must be non-null.", + entry.getKey(), entry.getValue())); + } + encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding)); + encodedParams.append('='); + encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding)); + encodedParams.append('&'); + } + return encodedParams.toString().getBytes(paramsEncoding); + } catch (UnsupportedEncodingException uee) { + throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee); + } + } + + /** + * Set whether or not responses to this request should be cached. + * + * @return This Request object to allow for chaining. + */ + public final Request setShouldCache(boolean shouldCache) { + mShouldCache = shouldCache; + return this; + } + + /** Returns true if responses to this request should be cached. */ + public final boolean shouldCache() { + return mShouldCache; + } + + /** + * Sets whether or not the request should be retried in the event of an HTTP 5xx (server) error. + * + * @return This Request object to allow for chaining. + */ + public final Request setShouldRetryServerErrors(boolean shouldRetryServerErrors) { + mShouldRetryServerErrors = shouldRetryServerErrors; + return this; + } + + /** + * Returns true if this request should be retried in the event of an HTTP 5xx (server) error. + */ + public final boolean shouldRetryServerErrors() { + return mShouldRetryServerErrors; + } + + /** + * Sets whether or not the request should be retried in the event that no connection could be + * established. + * + * @return This Request object to allow for chaining. + */ + public final Request setShouldRetryConnectionErrors(boolean shouldRetryConnectionErrors) { + mShouldRetryConnectionErrors = shouldRetryConnectionErrors; + return this; + } + + /** + * Returns true if this request should be retried in the event that no connection could be + * established. + */ + public final boolean shouldRetryConnectionErrors() { + return mShouldRetryConnectionErrors; + } + + /** + * Priority values. Requests will be processed from higher priorities to lower priorities, in + * FIFO order. + */ + public enum Priority { + LOW, + NORMAL, + HIGH, + IMMEDIATE + } + + /** Returns the {@link Priority} of this request; {@link Priority#NORMAL} by default. */ + public Priority getPriority() { + return Priority.NORMAL; + } + + /** + * Returns the socket timeout in milliseconds per retry attempt. (This value can be changed per + * retry attempt if a backoff is specified via backoffTimeout()). If there are no retry attempts + * remaining, this will cause delivery of a {@link TimeoutError} error. + */ + public final int getTimeoutMs() { + return getRetryPolicy().getCurrentTimeout(); + } + + /** Returns the retry policy that should be used for this request. */ + public RetryPolicy getRetryPolicy() { + return mRetryPolicy; + } + + /** + * Mark this request as having a response delivered on it. This can be used later in the + * request's lifetime for suppressing identical responses. + */ + public void markDelivered() { + synchronized (mLock) { + mResponseDelivered = true; + } + } + + /** Returns true if this request has had a response delivered for it. */ + public boolean hasHadResponseDelivered() { + synchronized (mLock) { + return mResponseDelivered; + } + } + + /** + * Subclasses must implement this to parse the raw network response and return an appropriate + * response type. This method will be called from a worker thread. The response will not be + * delivered if you return null. + * + * @param response Response from the network + * @return The parsed response, or null in the case of an error + */ + protected abstract Response parseNetworkResponse(NetworkResponse response); + + /** + * Subclasses can override this method to parse 'networkError' and return a more specific error. + * + *

The default implementation just returns the passed 'networkError'. + * + * @param volleyError the error retrieved from the network + * @return an NetworkError augmented with additional information + */ + protected VolleyError parseNetworkError(VolleyError volleyError) { + return volleyError; + } + + /** + * Subclasses must implement this to perform delivery of the parsed response to their listeners. + * The given response is guaranteed to be non-null; responses that fail to parse are not + * delivered. + * + * @param response The parsed response returned by {@link + * #parseNetworkResponse(NetworkResponse)} + */ + protected abstract void deliverResponse(T response); + + /** + * Delivers error message to the ErrorListener that the Request was initialized with. + * + * @param error Error details + */ + public void deliverError(VolleyError error) { + Response.ErrorListener listener; + synchronized (mLock) { + listener = mErrorListener; + } + if (listener != null) { + listener.onErrorResponse(error); + } + } + + /** + * {@link NetworkRequestCompleteListener} that will receive callbacks when the request returns + * from the network. + */ + /* package */ void setNetworkRequestCompleteListener( + NetworkRequestCompleteListener requestCompleteListener) { + synchronized (mLock) { + mRequestCompleteListener = requestCompleteListener; + } + } + + /** + * Notify NetworkRequestCompleteListener that a valid response has been received which can be + * used for other, waiting requests. + * + * @param response received from the network + */ + /* package */ void notifyListenerResponseReceived(Response response) { + NetworkRequestCompleteListener listener; + synchronized (mLock) { + listener = mRequestCompleteListener; + } + if (listener != null) { + listener.onResponseReceived(this, response); + } + } + + /** + * Notify NetworkRequestCompleteListener that the network request did not result in a response + * which can be used for other, waiting requests. + */ + /* package */ void notifyListenerResponseNotUsable() { + NetworkRequestCompleteListener listener; + synchronized (mLock) { + listener = mRequestCompleteListener; + } + if (listener != null) { + listener.onNoUsableResponseReceived(this); + } + } + + /** + * Our comparator sorts from high to low priority, and secondarily by sequence number to provide + * FIFO ordering. + */ + @Override + public int compareTo(Request other) { + Priority left = this.getPriority(); + Priority right = other.getPriority(); + + // High-priority requests are "lesser" so they are sorted to the front. + // Equal priorities are sorted by sequence number to provide FIFO ordering. + return left == right ? this.mSequence - other.mSequence : right.ordinal() - left.ordinal(); + } + + @Override + public String toString() { + String trafficStatsTag = "0x" + Integer.toHexString(getTrafficStatsTag()); + return (isCanceled() ? "[X] " : "[ ] ") + + getUrl() + + " " + + trafficStatsTag + + " " + + getPriority() + + " " + + mSequence; + } +} diff --git a/core/src/main/java/com/android/volley/RequestQueue.java b/core/src/main/java/com/android/volley/RequestQueue.java new file mode 100644 index 0000000..6db0b1c --- /dev/null +++ b/core/src/main/java/com/android/volley/RequestQueue.java @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.IntDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A request dispatch queue with a thread pool of dispatchers. + * + *

Calling {@link #add(Request)} will enqueue the given Request for dispatch, resolving from + * either cache or network on a worker thread, and then delivering a parsed response on the main + * thread. + */ +public class RequestQueue { + + /** Callback interface for completed requests. */ + // TODO: This should not be a generic class, because the request type can't be determined at + // compile time, so all calls to onRequestFinished are unsafe. However, changing this would be + // an API-breaking change. See also: https://github.com/google/volley/pull/109 + @Deprecated // Use RequestEventListener instead. + public interface RequestFinishedListener { + /** Called when a request has finished processing. */ + void onRequestFinished(Request request); + } + + /** Request event types the listeners {@link RequestEventListener} will be notified about. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + RequestEvent.REQUEST_QUEUED, + RequestEvent.REQUEST_CACHE_LOOKUP_STARTED, + RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED, + RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED, + RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED, + RequestEvent.REQUEST_FINISHED + }) + public @interface RequestEvent { + /** The request was added to the queue. */ + public static final int REQUEST_QUEUED = 0; + /** Cache lookup started for the request. */ + public static final int REQUEST_CACHE_LOOKUP_STARTED = 1; + /** + * Cache lookup finished for the request and cached response is delivered or request is + * queued for network dispatching. + */ + public static final int REQUEST_CACHE_LOOKUP_FINISHED = 2; + /** Network dispatch started for the request. */ + public static final int REQUEST_NETWORK_DISPATCH_STARTED = 3; + /** The network dispatch finished for the request and response (if any) is delivered. */ + public static final int REQUEST_NETWORK_DISPATCH_FINISHED = 4; + /** + * All the work associated with the request is finished and request is removed from all the + * queues. + */ + public static final int REQUEST_FINISHED = 5; + } + + /** Callback interface for request life cycle events. */ + public interface RequestEventListener { + /** + * Called on every request lifecycle event. Can be called from different threads. The call + * is blocking request processing, so any processing should be kept at minimum or moved to + * another thread. + */ + void onRequestEvent(Request request, @RequestEvent int event); + } + + /** Used for generating monotonically-increasing sequence numbers for requests. */ + private final AtomicInteger mSequenceGenerator = new AtomicInteger(); + + /** + * The set of all requests currently being processed by this RequestQueue. A Request will be in + * this set if it is waiting in any queue or currently being processed by any dispatcher. + */ + private final Set> mCurrentRequests = new HashSet<>(); + + /** The cache triage queue. */ + private final PriorityBlockingQueue> mCacheQueue = new PriorityBlockingQueue<>(); + + /** The queue of requests that are actually going out to the network. */ + private final PriorityBlockingQueue> mNetworkQueue = new PriorityBlockingQueue<>(); + + /** Number of network request dispatcher threads to start. */ + private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4; + + /** Cache interface for retrieving and storing responses. */ + private final Cache mCache; + + /** Network interface for performing requests. */ + private final Network mNetwork; + + /** Response delivery mechanism. */ + private final ResponseDelivery mDelivery; + + /** The network dispatchers. */ + private final NetworkDispatcher[] mDispatchers; + + /** The cache dispatcher. */ + private CacheDispatcher mCacheDispatcher; + + private final List mFinishedListeners = new ArrayList<>(); + + /** Collection of listeners for request life cycle events. */ + private final List mEventListeners = new ArrayList<>(); + + /** + * Creates the worker pool. Processing will not begin until {@link #start()} is called. + * + * @param cache A Cache to use for persisting responses to disk + * @param network A Network interface for performing HTTP requests + * @param threadPoolSize Number of network dispatcher threads to create + * @param delivery A ResponseDelivery interface for posting responses and errors + */ + public RequestQueue( + Cache cache, Network network, int threadPoolSize, ResponseDelivery delivery) { + mCache = cache; + mNetwork = network; + mDispatchers = new NetworkDispatcher[threadPoolSize]; + mDelivery = delivery; + } + + /** + * Creates the worker pool. Processing will not begin until {@link #start()} is called. + * + * @param cache A Cache to use for persisting responses to disk + * @param network A Network interface for performing HTTP requests + * @param threadPoolSize Number of network dispatcher threads to create + */ + public RequestQueue(Cache cache, Network network, int threadPoolSize) { + this( + cache, + network, + threadPoolSize, + new ExecutorDelivery(new Handler(Looper.getMainLooper()))); + } + + /** + * Creates the worker pool. Processing will not begin until {@link #start()} is called. + * + * @param cache A Cache to use for persisting responses to disk + * @param network A Network interface for performing HTTP requests + */ + public RequestQueue(Cache cache, Network network) { + this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE); + } + + /** Starts the dispatchers in this queue. */ + public void start() { + stop(); // Make sure any currently running dispatchers are stopped. + // Create the cache dispatcher and start it. + mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery); + mCacheDispatcher.start(); + + // Create network dispatchers (and corresponding threads) up to the pool size. + for (int i = 0; i < mDispatchers.length; i++) { + NetworkDispatcher networkDispatcher = + new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery); + mDispatchers[i] = networkDispatcher; + networkDispatcher.start(); + } + } + + /** Stops the cache and network dispatchers. */ + public void stop() { + if (mCacheDispatcher != null) { + mCacheDispatcher.quit(); + } + for (final NetworkDispatcher mDispatcher : mDispatchers) { + if (mDispatcher != null) { + mDispatcher.quit(); + } + } + } + + /** Gets a sequence number. */ + public int getSequenceNumber() { + return mSequenceGenerator.incrementAndGet(); + } + + /** Gets the {@link Cache} instance being used. */ + public Cache getCache() { + return mCache; + } + + /** + * A simple predicate or filter interface for Requests, for use by {@link + * RequestQueue#cancelAll(RequestFilter)}. + */ + public interface RequestFilter { + boolean apply(Request request); + } + + /** + * Cancels all requests in this queue for which the given filter applies. + * + * @param filter The filtering function to use + */ + public void cancelAll(RequestFilter filter) { + synchronized (mCurrentRequests) { + for (Request request : mCurrentRequests) { + if (filter.apply(request)) { + request.cancel(); + } + } + } + } + + /** + * Cancels all requests in this queue with the given tag. Tag must be non-null and equality is + * by identity. + */ + public void cancelAll(final Object tag) { + if (tag == null) { + throw new IllegalArgumentException("Cannot cancelAll with a null tag"); + } + cancelAll( + new RequestFilter() { + @Override + public boolean apply(Request request) { + return request.getTag() == tag; + } + }); + } + + /** + * Adds a Request to the dispatch queue. + * + * @param request The request to service + * @return The passed-in request + */ + public Request add(Request request) { + // Tag the request as belonging to this queue and add it to the set of current requests. + request.setRequestQueue(this); + synchronized (mCurrentRequests) { + mCurrentRequests.add(request); + } + + // Process requests in the order they are added. + request.setSequence(getSequenceNumber()); + request.addMarker("add-to-queue"); + sendRequestEvent(request, RequestEvent.REQUEST_QUEUED); + + beginRequest(request); + return request; + } + + void beginRequest(Request request) { + // If the request is uncacheable, skip the cache queue and go straight to the network. + if (!request.shouldCache()) { + sendRequestOverNetwork(request); + } else { + mCacheQueue.add(request); + } + } + + /** + * Called from {@link Request#finish(String)}, indicating that processing of the given request + * has finished. + */ + @SuppressWarnings("unchecked") // see above note on RequestFinishedListener + void finish(Request request) { + // Remove from the set of requests currently being processed. + synchronized (mCurrentRequests) { + mCurrentRequests.remove(request); + } + synchronized (mFinishedListeners) { + for (RequestFinishedListener listener : mFinishedListeners) { + listener.onRequestFinished(request); + } + } + sendRequestEvent(request, RequestEvent.REQUEST_FINISHED); + } + + /** Sends a request life cycle event to the listeners. */ + void sendRequestEvent(Request request, @RequestEvent int event) { + synchronized (mEventListeners) { + for (RequestEventListener listener : mEventListeners) { + listener.onRequestEvent(request, event); + } + } + } + + /** Add a listener for request life cycle events. */ + public void addRequestEventListener(RequestEventListener listener) { + synchronized (mEventListeners) { + mEventListeners.add(listener); + } + } + + /** Remove a listener for request life cycle events. */ + public void removeRequestEventListener(RequestEventListener listener) { + synchronized (mEventListeners) { + mEventListeners.remove(listener); + } + } + + @Deprecated // Use RequestEventListener instead. + public void addRequestFinishedListener(RequestFinishedListener listener) { + synchronized (mFinishedListeners) { + mFinishedListeners.add(listener); + } + } + + /** Remove a RequestFinishedListener. Has no effect if listener was not previously added. */ + @Deprecated // Use RequestEventListener instead. + public void removeRequestFinishedListener(RequestFinishedListener listener) { + synchronized (mFinishedListeners) { + mFinishedListeners.remove(listener); + } + } + + public ResponseDelivery getResponseDelivery() { + return mDelivery; + } + + void sendRequestOverNetwork(Request request) { + mNetworkQueue.add(request); + } +} diff --git a/core/src/main/java/com/android/volley/RequestTask.java b/core/src/main/java/com/android/volley/RequestTask.java new file mode 100644 index 0000000..b429f79 --- /dev/null +++ b/core/src/main/java/com/android/volley/RequestTask.java @@ -0,0 +1,20 @@ +package com.android.volley; + +/** + * Abstract runnable that's a task to be completed by the RequestQueue. + * + *

WARNING: This API is experimental and subject to breaking changes. Please see + * https://github.com/google/volley/wiki/Asynchronous-Volley for more details. + */ +public abstract class RequestTask implements Runnable { + final Request mRequest; + + public RequestTask(Request request) { + mRequest = request; + } + + @SuppressWarnings("unchecked") + public int compareTo(RequestTask other) { + return mRequest.compareTo((Request) other.mRequest); + } +} diff --git a/core/src/main/java/com/android/volley/Response.java b/core/src/main/java/com/android/volley/Response.java new file mode 100644 index 0000000..622bdc4 --- /dev/null +++ b/core/src/main/java/com/android/volley/Response.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import androidx.annotation.Nullable; + +/** + * Encapsulates a parsed response for delivery. + * + * @param Parsed type of this response + */ +public class Response { + + /** Callback interface for delivering parsed responses. */ + public interface Listener { + /** Called when a response is received. */ + void onResponse(T response); + } + + /** Callback interface for delivering error responses. */ + public interface ErrorListener { + /** + * Callback method that an error has been occurred with the provided error code and optional + * user-readable message. + */ + void onErrorResponse(VolleyError error); + } + + /** Returns a successful response containing the parsed result. */ + public static Response success(@Nullable T result, @Nullable Cache.Entry cacheEntry) { + return new Response<>(result, cacheEntry); + } + + /** + * Returns a failed response containing the given error code and an optional localized message + * displayed to the user. + */ + public static Response error(VolleyError error) { + return new Response<>(error); + } + + /** Parsed response, can be null; always null in the case of error. */ + @Nullable public final T result; + + /** Cache metadata for this response; null if not cached or in the case of error. */ + @Nullable public final Cache.Entry cacheEntry; + + /** Detailed error information if errorCode != OK. */ + @Nullable public final VolleyError error; + + /** True if this response was a soft-expired one and a second one MAY be coming. */ + public boolean intermediate = false; + + /** Returns whether this response is considered successful. */ + public boolean isSuccess() { + return error == null; + } + + private Response(@Nullable T result, @Nullable Cache.Entry cacheEntry) { + this.result = result; + this.cacheEntry = cacheEntry; + this.error = null; + } + + private Response(VolleyError error) { + this.result = null; + this.cacheEntry = null; + this.error = error; + } +} diff --git a/core/src/main/java/com/android/volley/ResponseDelivery.java b/core/src/main/java/com/android/volley/ResponseDelivery.java new file mode 100644 index 0000000..10aa137 --- /dev/null +++ b/core/src/main/java/com/android/volley/ResponseDelivery.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +public interface ResponseDelivery { + /** Parses a response from the network or cache and delivers it. */ + void postResponse(Request request, Response response); + + /** + * Parses a response from the network or cache and delivers it. The provided Runnable will be + * executed after delivery. + */ + void postResponse(Request request, Response response, Runnable runnable); + + /** Posts an error for the given request. */ + void postError(Request request, VolleyError error); +} diff --git a/core/src/main/java/com/android/volley/RetryPolicy.java b/core/src/main/java/com/android/volley/RetryPolicy.java new file mode 100644 index 0000000..3ef26de --- /dev/null +++ b/core/src/main/java/com/android/volley/RetryPolicy.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * Retry policy for a request. + * + *

A retry policy can control two parameters: + * + *

    + *
  • The number of tries. This can be a simple counter or more complex logic based on the type + * of error passed to {@link #retry(VolleyError)}, although {@link #getCurrentRetryCount()} + * should always return the current retry count for logging purposes. + *
  • The request timeout for each try, via {@link #getCurrentTimeout()}. In the common case that + * a request times out before the response has been received from the server, retrying again + * with a longer timeout can increase the likelihood of success (at the expense of causing the + * user to wait longer, especially if the request still fails). + *
+ * + *

Note that currently, retries triggered by a retry policy are attempted immediately in sequence + * with no delay between them (although the time between tries may increase if the requests are + * timing out and {@link #getCurrentTimeout()} is returning increasing values). + * + *

By default, Volley uses {@link DefaultRetryPolicy}. + */ +public interface RetryPolicy { + + /** Returns the current timeout (used for logging). */ + int getCurrentTimeout(); + + /** Returns the current retry count (used for logging). */ + int getCurrentRetryCount(); + + /** + * Prepares for the next retry by applying a backoff to the timeout. + * + * @param error The error code of the last attempt. + * @throws VolleyError In the event that the retry could not be performed (for example if we ran + * out of attempts), the passed in error is thrown. + */ + void retry(VolleyError error) throws VolleyError; +} diff --git a/core/src/main/java/com/android/volley/ServerError.java b/core/src/main/java/com/android/volley/ServerError.java new file mode 100644 index 0000000..84b2eb4 --- /dev/null +++ b/core/src/main/java/com/android/volley/ServerError.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** Indicates that the server responded with an error response. */ +@SuppressWarnings("serial") +public class ServerError extends VolleyError { + public ServerError(NetworkResponse networkResponse) { + super(networkResponse); + } + + public ServerError() { + super(); + } +} diff --git a/core/src/main/java/com/android/volley/TimeoutError.java b/core/src/main/java/com/android/volley/TimeoutError.java new file mode 100644 index 0000000..227ae08 --- /dev/null +++ b/core/src/main/java/com/android/volley/TimeoutError.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** Indicates that the connection or the socket timed out. */ +@SuppressWarnings("serial") +public class TimeoutError extends VolleyError {} diff --git a/core/src/main/java/com/android/volley/VolleyError.java b/core/src/main/java/com/android/volley/VolleyError.java new file mode 100644 index 0000000..45086da --- /dev/null +++ b/core/src/main/java/com/android/volley/VolleyError.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** Exception style class encapsulating Volley errors */ +@SuppressWarnings("serial") +public class VolleyError extends Exception { + public final NetworkResponse networkResponse; + private long networkTimeMs; + + public VolleyError() { + networkResponse = null; + } + + public VolleyError(NetworkResponse response) { + networkResponse = response; + } + + public VolleyError(String exceptionMessage) { + super(exceptionMessage); + networkResponse = null; + } + + public VolleyError(String exceptionMessage, Throwable reason) { + super(exceptionMessage, reason); + networkResponse = null; + } + + public VolleyError(Throwable cause) { + super(cause); + networkResponse = null; + } + + /* package */ void setNetworkTimeMs(long networkTimeMs) { + this.networkTimeMs = networkTimeMs; + } + + public long getNetworkTimeMs() { + return networkTimeMs; + } +} diff --git a/core/src/main/java/com/android/volley/VolleyLog.java b/core/src/main/java/com/android/volley/VolleyLog.java new file mode 100644 index 0000000..8477668 --- /dev/null +++ b/core/src/main/java/com/android/volley/VolleyLog.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.os.SystemClock; +import android.util.Log; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Logging helper class. + * + *

to see Volley logs call:
+ * {@code /platform-tools/adb shell setprop log.tag.Volley VERBOSE} + */ +public class VolleyLog { + public static String TAG = "Volley"; + + public static boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE); + + /** + * {@link Class#getName()} uses reflection and calling it on a potentially hot code path may + * have some cost. To minimize this cost we fetch class name once here and use it later. + */ + private static final String CLASS_NAME = VolleyLog.class.getName(); + + /** + * Customize the log tag for your application, so that other apps using Volley don't mix their + * logs with yours.
+ * Enable the log property for your tag before starting your app:
+ * {@code adb shell setprop log.tag.<tag>} + */ + public static void setTag(String tag) { + d("Changing log tag to %s", tag); + TAG = tag; + + // Reinitialize the DEBUG "constant" + DEBUG = Log.isLoggable(TAG, Log.VERBOSE); + } + + public static void v(String format, Object... args) { + if (DEBUG) { + Log.v(TAG, buildMessage(format, args)); + } + } + + public static void d(String format, Object... args) { + Log.d(TAG, buildMessage(format, args)); + } + + public static void e(String format, Object... args) { + Log.e(TAG, buildMessage(format, args)); + } + + public static void e(Throwable tr, String format, Object... args) { + Log.e(TAG, buildMessage(format, args), tr); + } + + public static void wtf(String format, Object... args) { + Log.wtf(TAG, buildMessage(format, args)); + } + + public static void wtf(Throwable tr, String format, Object... args) { + Log.wtf(TAG, buildMessage(format, args), tr); + } + + /** + * Formats the caller's provided message and prepends useful info like calling thread ID and + * method name. + */ + private static String buildMessage(String format, Object... args) { + String msg = (args == null) ? format : String.format(Locale.US, format, args); + StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace(); + + String caller = ""; + // Walk up the stack looking for the first caller outside of VolleyLog. + // It will be at least two frames up, so start there. + for (int i = 2; i < trace.length; i++) { + String clazz = trace[i].getClassName(); + if (!clazz.equals(VolleyLog.CLASS_NAME)) { + String callingClass = trace[i].getClassName(); + callingClass = callingClass.substring(callingClass.lastIndexOf('.') + 1); + callingClass = callingClass.substring(callingClass.lastIndexOf('$') + 1); + + caller = callingClass + "." + trace[i].getMethodName(); + break; + } + } + return String.format(Locale.US, "[%d] %s: %s", Thread.currentThread().getId(), caller, msg); + } + + /** A simple event log with records containing a name, thread ID, and timestamp. */ + static class MarkerLog { + public static final boolean ENABLED = VolleyLog.DEBUG; + + /** Minimum duration from first marker to last in an marker log to warrant logging. */ + private static final long MIN_DURATION_FOR_LOGGING_MS = 0; + + private static class Marker { + public final String name; + public final long thread; + public final long time; + + public Marker(String name, long thread, long time) { + this.name = name; + this.thread = thread; + this.time = time; + } + } + + private final List mMarkers = new ArrayList<>(); + private boolean mFinished = false; + + /** Adds a marker to this log with the specified name. */ + public synchronized void add(String name, long threadId) { + if (mFinished) { + throw new IllegalStateException("Marker added to finished log"); + } + + mMarkers.add(new Marker(name, threadId, SystemClock.elapsedRealtime())); + } + + /** + * Closes the log, dumping it to logcat if the time difference between the first and last + * markers is greater than {@link #MIN_DURATION_FOR_LOGGING_MS}. + * + * @param header Header string to print above the marker log. + */ + public synchronized void finish(String header) { + mFinished = true; + + long duration = getTotalDuration(); + if (duration <= MIN_DURATION_FOR_LOGGING_MS) { + return; + } + + long prevTime = mMarkers.get(0).time; + d("(%-4d ms) %s", duration, header); + for (Marker marker : mMarkers) { + long thisTime = marker.time; + d("(+%-4d) [%2d] %s", (thisTime - prevTime), marker.thread, marker.name); + prevTime = thisTime; + } + } + + @Override + protected void finalize() throws Throwable { + // Catch requests that have been collected (and hence end-of-lifed) + // but had no debugging output printed for them. + if (!mFinished) { + finish("Request on the loose"); + e("Marker log finalized without finish() - uncaught exit point for request"); + } + } + + /** Returns the time difference between the first and last events in this log. */ + private long getTotalDuration() { + if (mMarkers.size() == 0) { + return 0; + } + + long first = mMarkers.get(0).time; + long last = mMarkers.get(mMarkers.size() - 1).time; + return last - first; + } + } +} diff --git a/core/src/main/java/com/android/volley/WaitingRequestManager.java b/core/src/main/java/com/android/volley/WaitingRequestManager.java new file mode 100644 index 0000000..682e339 --- /dev/null +++ b/core/src/main/java/com/android/volley/WaitingRequestManager.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; + +/** + * Callback to notify the caller when the network request returns. Valid responses can be used by + * all duplicate requests. + */ +class WaitingRequestManager implements Request.NetworkRequestCompleteListener { + + /** + * Staging area for requests that already have a duplicate request in flight. + * + *

    + *
  • containsKey(cacheKey) indicates that there is a request in flight for the given cache + * key. + *
  • get(cacheKey) returns waiting requests for the given cache key. The in flight request + * is not contained in that list. Is null if no requests are staged. + *
+ */ + private final Map>> 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> mNetworkQueue; + + WaitingRequestManager(@NonNull RequestQueue requestQueue) { + mRequestQueue = requestQueue; + mResponseDelivery = mRequestQueue.getResponseDelivery(); + mCacheDispatcher = null; + mNetworkQueue = null; + } + + WaitingRequestManager( + @NonNull CacheDispatcher cacheDispatcher, + @NonNull BlockingQueue> 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> 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> 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> stagedRequests = mWaitingRequests.get(cacheKey); + if (stagedRequests == null) { + stagedRequests = new ArrayList<>(); + } + request.addMarker("waiting-for-response"); + stagedRequests.add(request); + mWaitingRequests.put(cacheKey, stagedRequests); + if (VolleyLog.DEBUG) { + VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey); + } + return true; + } else { + // Insert 'null' queue for this cacheKey, indicating there is now a request in + // flight. + mWaitingRequests.put(cacheKey, null); + request.setNetworkRequestCompleteListener(this); + if (VolleyLog.DEBUG) { + VolleyLog.d("new request, sending to network %s", cacheKey); + } + return false; + } + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java b/core/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java new file mode 100644 index 0000000..c75c25f --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.volley.toolbox; + +import com.android.volley.AuthFailureError; +import com.android.volley.Header; +import com.android.volley.Request; +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.http.conn.ConnectTimeoutException; + +/** + * {@link BaseHttpStack} implementation wrapping a {@link HttpStack}. + * + *

{@link BasicNetwork} uses this if it is provided a {@link HttpStack} at construction time, + * allowing it to have one implementation based atop {@link BaseHttpStack}. + */ +@SuppressWarnings("deprecation") +class AdaptedHttpStack extends BaseHttpStack { + + private final HttpStack mHttpStack; + + AdaptedHttpStack(HttpStack httpStack) { + mHttpStack = httpStack; + } + + @Override + public HttpResponse executeRequest(Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + org.apache.http.HttpResponse apacheResp; + try { + apacheResp = mHttpStack.performRequest(request, additionalHeaders); + } catch (ConnectTimeoutException e) { + // BasicNetwork won't know that this exception should be retried like a timeout, since + // it's an Apache-specific error, so wrap it in a standard timeout exception. + throw new SocketTimeoutException(e.getMessage()); + } + + int statusCode = apacheResp.getStatusLine().getStatusCode(); + + org.apache.http.Header[] headers = apacheResp.getAllHeaders(); + List

headerList = new ArrayList<>(headers.length); + for (org.apache.http.Header header : headers) { + headerList.add(new Header(header.getName(), header.getValue())); + } + + if (apacheResp.getEntity() == null) { + return new HttpResponse(statusCode, headerList); + } + + long contentLength = apacheResp.getEntity().getContentLength(); + if ((int) contentLength != contentLength) { + throw new IOException("Response too large: " + contentLength); + } + + return new HttpResponse( + statusCode, + headerList, + (int) apacheResp.getEntity().getContentLength(), + apacheResp.getEntity().getContent()); + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java b/core/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java new file mode 100644 index 0000000..f3381ae --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import androidx.annotation.VisibleForTesting; +import com.android.volley.AuthFailureError; + +/** + * An Authenticator that uses {@link AccountManager} to get auth tokens of a specified type for a + * specified account. + */ +// TODO: Update this to account for runtime permissions +@SuppressLint("MissingPermission") +public class AndroidAuthenticator implements Authenticator { + private final AccountManager mAccountManager; + private final Account mAccount; + private final String mAuthTokenType; + private final boolean mNotifyAuthFailure; + + /** + * Creates a new authenticator. + * + * @param context Context for accessing AccountManager + * @param account Account to authenticate as + * @param authTokenType Auth token type passed to AccountManager + */ + public AndroidAuthenticator(Context context, Account account, String authTokenType) { + this(context, account, authTokenType, /* notifyAuthFailure= */ false); + } + + /** + * Creates a new authenticator. + * + * @param context Context for accessing AccountManager + * @param account Account to authenticate as + * @param authTokenType Auth token type passed to AccountManager + * @param notifyAuthFailure Whether to raise a notification upon auth failure + */ + public AndroidAuthenticator( + Context context, Account account, String authTokenType, boolean notifyAuthFailure) { + this(AccountManager.get(context), account, authTokenType, notifyAuthFailure); + } + + @VisibleForTesting + AndroidAuthenticator( + AccountManager accountManager, + Account account, + String authTokenType, + boolean notifyAuthFailure) { + mAccountManager = accountManager; + mAccount = account; + mAuthTokenType = authTokenType; + mNotifyAuthFailure = notifyAuthFailure; + } + + /** Returns the Account being used by this authenticator. */ + public Account getAccount() { + return mAccount; + } + + /** Returns the Auth Token Type used by this authenticator. */ + public String getAuthTokenType() { + return mAuthTokenType; + } + + // TODO: Figure out what to do about notifyAuthFailure + @SuppressWarnings("deprecation") + @Override + public String getAuthToken() throws AuthFailureError { + AccountManagerFuture future = + mAccountManager.getAuthToken( + mAccount, + mAuthTokenType, + mNotifyAuthFailure, + /* callback= */ null, + /* handler= */ null); + Bundle result; + try { + result = future.getResult(); + } catch (Exception e) { + throw new AuthFailureError("Error while retrieving auth token", e); + } + String authToken = null; + if (future.isDone() && !future.isCancelled()) { + if (result.containsKey(AccountManager.KEY_INTENT)) { + Intent intent = result.getParcelable(AccountManager.KEY_INTENT); + throw new AuthFailureError(intent); + } + authToken = result.getString(AccountManager.KEY_AUTHTOKEN); + } + if (authToken == null) { + throw new AuthFailureError("Got null auth token for type: " + mAuthTokenType); + } + + return authToken; + } + + @Override + public void invalidateAuthToken(String authToken) { + mAccountManager.invalidateAuthToken(mAccount.type, authToken); + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java b/core/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java new file mode 100644 index 0000000..4165637 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.VolleyLog; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Asynchronous extension of the {@link BaseHttpStack} class. + * + *

WARNING: This API is experimental and subject to breaking changes. Please see + * https://github.com/google/volley/wiki/Asynchronous-Volley for more details. + */ +public abstract class AsyncHttpStack extends BaseHttpStack { + private ExecutorService mBlockingExecutor; + private ExecutorService mNonBlockingExecutor; + + public interface OnRequestComplete { + /** Invoked when the stack successfully completes a request. */ + void onSuccess(HttpResponse httpResponse); + + /** Invoked when the stack throws an {@link AuthFailureError} during a request. */ + void onAuthError(AuthFailureError authFailureError); + + /** Invoked when the stack throws an {@link IOException} during a request. */ + void onError(IOException ioException); + } + + /** + * Makes an HTTP request with the given parameters, and calls the {@link OnRequestComplete} + * callback, with either the {@link HttpResponse} or error that was thrown. + * + * @param request to perform + * @param additionalHeaders to be sent together with {@link Request#getHeaders()} + * @param callback to be called after retrieving the {@link HttpResponse} or throwing an error. + */ + public abstract void executeRequest( + Request request, Map 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 additionalHeaders) + throws IOException, AuthFailureError { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference entry = new AtomicReference<>(); + executeRequest( + request, + additionalHeaders, + new OnRequestComplete() { + @Override + public void onSuccess(HttpResponse httpResponse) { + Response response = + new Response( + httpResponse, + /* ioException= */ null, + /* authFailureError= */ null); + entry.set(response); + latch.countDown(); + } + + @Override + public void onAuthError(AuthFailureError authFailureError) { + Response response = + new Response( + /* httpResponse= */ null, + /* ioException= */ null, + authFailureError); + entry.set(response); + latch.countDown(); + } + + @Override + public void onError(IOException ioException) { + Response response = + new Response( + /* httpResponse= */ null, + ioException, + /* authFailureError= */ null); + entry.set(response); + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException e) { + VolleyLog.e(e, "while waiting for CountDownLatch"); + Thread.currentThread().interrupt(); + throw new InterruptedIOException(e.toString()); + } + Response response = entry.get(); + if (response.httpResponse != null) { + return response.httpResponse; + } else if (response.ioException != null) { + throw response.ioException; + } else { + throw response.authFailureError; + } + } + + private static class Response { + HttpResponse httpResponse; + IOException ioException; + AuthFailureError authFailureError; + + private Response( + @Nullable HttpResponse httpResponse, + @Nullable IOException ioException, + @Nullable AuthFailureError authFailureError) { + this.httpResponse = httpResponse; + this.ioException = ioException; + this.authFailureError = authFailureError; + } + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/Authenticator.java b/core/src/main/java/com/android/volley/toolbox/Authenticator.java new file mode 100644 index 0000000..2ba43db --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/Authenticator.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.AuthFailureError; + +/** An interface for interacting with auth tokens. */ +public interface Authenticator { + /** + * Synchronously retrieves an auth token. + * + * @throws AuthFailureError If authentication did not succeed + */ + String getAuthToken() throws AuthFailureError; + + /** Invalidates the provided auth token. */ + void invalidateAuthToken(String authToken); +} diff --git a/core/src/main/java/com/android/volley/toolbox/BaseHttpStack.java b/core/src/main/java/com/android/volley/toolbox/BaseHttpStack.java new file mode 100644 index 0000000..99a9899 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/BaseHttpStack.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.volley.toolbox; + +import com.android.volley.AuthFailureError; +import com.android.volley.Header; +import com.android.volley.Request; +import java.io.IOException; +import java.io.InputStream; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicHttpResponse; +import org.apache.http.message.BasicStatusLine; + +/** An HTTP stack abstraction. */ +@SuppressWarnings("deprecation") // for HttpStack +public abstract class BaseHttpStack implements HttpStack { + + /** + * Performs an HTTP request with the given parameters. + * + *

A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise, + * and the Content-Type header is set to request.getPostBodyContentType(). + * + * @param request the request to perform + * @param additionalHeaders additional headers to be sent together with {@link + * Request#getHeaders()} + * @return the {@link HttpResponse} + * @throws SocketTimeoutException if the request times out + * @throws IOException if another I/O error occurs during the request + * @throws AuthFailureError if an authentication failure occurs during the request + */ + public abstract HttpResponse executeRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError; + + /** + * @deprecated use {@link #executeRequest} instead to avoid a dependency on the deprecated + * Apache HTTP library. Nothing in Volley's own source calls this method. However, since + * {@link BasicNetwork#mHttpStack} is exposed to subclasses, we provide this implementation + * in case legacy client apps are dependent on that field. This method may be removed in a + * future release of Volley. + */ + @Deprecated + @Override + public final org.apache.http.HttpResponse performRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + HttpResponse response = executeRequest(request, additionalHeaders); + + ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1); + StatusLine statusLine = + new BasicStatusLine( + protocolVersion, response.getStatusCode(), /* reasonPhrase= */ ""); + BasicHttpResponse apacheResponse = new BasicHttpResponse(statusLine); + + List headers = new ArrayList<>(); + for (Header header : response.getHeaders()) { + headers.add(new BasicHeader(header.getName(), header.getValue())); + } + apacheResponse.setHeaders(headers.toArray(new org.apache.http.Header[0])); + + InputStream responseStream = response.getContent(); + if (responseStream != null) { + BasicHttpEntity entity = new BasicHttpEntity(); + entity.setContent(responseStream); + entity.setContentLength(response.getContentLength()); + apacheResponse.setEntity(entity); + } + + return apacheResponse; + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java b/core/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java new file mode 100644 index 0000000..cdedaff --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static com.android.volley.toolbox.NetworkUtility.logSlowRequests; + +import android.os.SystemClock; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import com.android.volley.AsyncNetwork; +import com.android.volley.AuthFailureError; +import com.android.volley.Header; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.RequestTask; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.NetworkUtility.RetryInfo; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; + +/** + * A network performing Volley requests over an {@link HttpStack}. + * + *

WARNING: This API is experimental and subject to breaking changes. Please see + * https://github.com/google/volley/wiki/Asynchronous-Volley for more details. + */ +public class BasicAsyncNetwork extends AsyncNetwork { + + private final AsyncHttpStack mAsyncStack; + private final ByteArrayPool mPool; + + /** + * @param httpStack HTTP stack to be used + * @param pool a buffer pool that improves GC performance in copy operations + */ + private BasicAsyncNetwork(AsyncHttpStack httpStack, ByteArrayPool pool) { + mAsyncStack = httpStack; + mPool = pool; + } + + @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP}) + @Override + public void setBlockingExecutor(ExecutorService executor) { + super.setBlockingExecutor(executor); + mAsyncStack.setBlockingExecutor(executor); + } + + @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP}) + @Override + public void setNonBlockingExecutor(ExecutorService executor) { + super.setNonBlockingExecutor(executor); + mAsyncStack.setNonBlockingExecutor(executor); + } + + /* Method to be called after a successful network request */ + private void onRequestSucceeded( + final Request request, + final long requestStartMs, + final HttpResponse httpResponse, + final OnRequestComplete callback) { + final int statusCode = httpResponse.getStatusCode(); + final List

responseHeaders = httpResponse.getHeaders(); + // Handle cache validation. + if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + long requestDuration = SystemClock.elapsedRealtime() - requestStartMs; + callback.onSuccess( + NetworkUtility.getNotModifiedNetworkResponse( + request, requestDuration, responseHeaders)); + return; + } + + byte[] responseContents = httpResponse.getContentBytes(); + if (responseContents == null && httpResponse.getContent() == null) { + // Add 0 byte response as a way of honestly representing a + // no-content request. + responseContents = new byte[0]; + } + + if (responseContents != null) { + onResponseRead( + requestStartMs, + statusCode, + httpResponse, + request, + callback, + responseHeaders, + responseContents); + return; + } + + // The underlying AsyncHttpStack does not support asynchronous reading of the response into + // a byte array, so we need to submit a blocking task to copy the response from the + // InputStream instead. + final InputStream inputStream = httpResponse.getContent(); + getBlockingExecutor() + .execute( + new ResponseParsingTask<>( + inputStream, + httpResponse, + request, + callback, + requestStartMs, + responseHeaders, + statusCode)); + } + + /* Method to be called after a failed network request */ + private void onRequestFailed( + Request request, + OnRequestComplete callback, + IOException exception, + long requestStartMs, + @Nullable HttpResponse httpResponse, + @Nullable byte[] responseContents) { + try { + RetryInfo retryInfo = + NetworkUtility.shouldRetryException( + request, exception, requestStartMs, httpResponse, responseContents); + // RetryPolicy#retry may need a background thread, so invoke in the blocking executor. + getBlockingExecutor() + .execute(new InvokeRetryPolicyTask<>(request, retryInfo, callback)); + } catch (VolleyError volleyError) { + callback.onError(volleyError); + } + } + + private class InvokeRetryPolicyTask extends RequestTask { + final Request request; + final RetryInfo retryInfo; + final OnRequestComplete callback; + + InvokeRetryPolicyTask(Request request, RetryInfo retryInfo, OnRequestComplete callback) { + super(request); + this.request = request; + this.retryInfo = retryInfo; + this.callback = callback; + } + + @Override + public void run() { + try { + NetworkUtility.attemptRetryOnException(request, retryInfo); + // attemptRetryOnException didn't throw, so proceed with the next attempt. + performRequest(request, callback); + } catch (VolleyError e) { + callback.onError(e); + } + } + } + + @Override + public void performRequest(final Request request, final OnRequestComplete callback) { + if (getBlockingExecutor() == null) { + throw new IllegalStateException( + "mBlockingExecuter must be set before making a request"); + } + final long requestStartMs = SystemClock.elapsedRealtime(); + // Gather headers. + final Map 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
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 extends RequestTask { + InputStream inputStream; + HttpResponse httpResponse; + Request request; + OnRequestComplete callback; + long requestStartMs; + List
responseHeaders; + int statusCode; + + ResponseParsingTask( + InputStream inputStream, + HttpResponse httpResponse, + Request request, + OnRequestComplete callback, + long requestStartMs, + List
responseHeaders, + int statusCode) { + super(request); + this.inputStream = inputStream; + this.httpResponse = httpResponse; + this.request = request; + this.callback = callback; + this.requestStartMs = requestStartMs; + this.responseHeaders = responseHeaders; + this.statusCode = statusCode; + } + + @Override + public void run() { + byte[] finalResponseContents; + try { + finalResponseContents = + NetworkUtility.inputStreamToBytes( + inputStream, httpResponse.getContentLength(), mPool); + } catch (IOException e) { + onRequestFailed(request, callback, e, requestStartMs, httpResponse, null); + return; + } + onResponseRead( + requestStartMs, + statusCode, + httpResponse, + request, + callback, + responseHeaders, + finalResponseContents); + } + } + + /** + * Builder is used to build an instance of {@link BasicAsyncNetwork} from values configured by + * the setters. + */ + public static class Builder { + private static final int DEFAULT_POOL_SIZE = 4096; + @NonNull private AsyncHttpStack mAsyncStack; + private ByteArrayPool mPool; + + public Builder(@NonNull AsyncHttpStack httpStack) { + mAsyncStack = httpStack; + mPool = null; + } + + /** + * Sets the ByteArrayPool to be used. If not set, it will default to a pool with the default + * pool size. + */ + public Builder setPool(ByteArrayPool pool) { + mPool = pool; + return this; + } + + /** Builds the {@link com.android.volley.toolbox.BasicAsyncNetwork} */ + public BasicAsyncNetwork build() { + if (mPool == null) { + mPool = new ByteArrayPool(DEFAULT_POOL_SIZE); + } + return new BasicAsyncNetwork(mAsyncStack, mPool); + } + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/core/src/main/java/com/android/volley/toolbox/BasicNetwork.java new file mode 100644 index 0000000..552e628 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/BasicNetwork.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.os.SystemClock; +import com.android.volley.Header; +import com.android.volley.Network; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.NetworkUtility.RetryInfo; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** A network performing Volley requests over an {@link HttpStack}. */ +public class BasicNetwork implements Network { + private static final int DEFAULT_POOL_SIZE = 4096; + + /** + * @deprecated Should never have been exposed in the API. This field may be removed in a future + * release of Volley. + */ + @Deprecated protected final HttpStack mHttpStack; + + private final BaseHttpStack mBaseHttpStack; + + protected final ByteArrayPool mPool; + + /** + * @param httpStack HTTP stack to be used + * @deprecated use {@link #BasicNetwork(BaseHttpStack)} instead to avoid depending on Apache + * HTTP. This method may be removed in a future release of Volley. + */ + @Deprecated + public BasicNetwork(HttpStack httpStack) { + // If a pool isn't passed in, then build a small default pool that will give us a lot of + // benefit and not use too much memory. + this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE)); + } + + /** + * @param httpStack HTTP stack to be used + * @param pool a buffer pool that improves GC performance in copy operations + * @deprecated use {@link #BasicNetwork(BaseHttpStack, ByteArrayPool)} instead to avoid + * depending on Apache HTTP. This method may be removed in a future release of Volley. + */ + @Deprecated + public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) { + mHttpStack = httpStack; + mBaseHttpStack = new AdaptedHttpStack(httpStack); + mPool = pool; + } + + /** @param httpStack HTTP stack to be used */ + public BasicNetwork(BaseHttpStack httpStack) { + // If a pool isn't passed in, then build a small default pool that will give us a lot of + // benefit and not use too much memory. + this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE)); + } + + /** + * @param httpStack HTTP stack to be used + * @param pool a buffer pool that improves GC performance in copy operations + */ + public BasicNetwork(BaseHttpStack httpStack, ByteArrayPool pool) { + mBaseHttpStack = httpStack; + // Populate mHttpStack for backwards compatibility, since it is a protected field. However, + // we won't use it directly here, so clients which don't access it directly won't need to + // depend on Apache HTTP. + mHttpStack = httpStack; + mPool = pool; + } + + @Override + public NetworkResponse performRequest(Request request) throws VolleyError { + long requestStart = SystemClock.elapsedRealtime(); + while (true) { + HttpResponse httpResponse = null; + byte[] responseContents = null; + List
responseHeaders = Collections.emptyList(); + try { + // Gather headers. + Map additionalRequestHeaders = + HttpHeaderParser.getCacheHeaders(request.getCacheEntry()); + httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders); + int statusCode = httpResponse.getStatusCode(); + + responseHeaders = httpResponse.getHeaders(); + // Handle cache validation. + if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + long requestDuration = SystemClock.elapsedRealtime() - requestStart; + return NetworkUtility.getNotModifiedNetworkResponse( + request, requestDuration, responseHeaders); + } + + // Some responses such as 204s do not have content. We must check. + InputStream inputStream = httpResponse.getContent(); + if (inputStream != null) { + responseContents = + NetworkUtility.inputStreamToBytes( + inputStream, httpResponse.getContentLength(), mPool); + } else { + // Add 0 byte response as a way of honestly representing a + // no-content request. + responseContents = new byte[0]; + } + + // if the request is slow, log it. + long requestLifetime = SystemClock.elapsedRealtime() - requestStart; + NetworkUtility.logSlowRequests( + requestLifetime, request, responseContents, statusCode); + + if (statusCode < 200 || statusCode > 299) { + throw new IOException(); + } + return new NetworkResponse( + statusCode, + responseContents, + /* notModified= */ false, + SystemClock.elapsedRealtime() - requestStart, + responseHeaders); + } catch (IOException e) { + // This will either throw an exception, breaking us from the loop, or will loop + // again and retry the request. + RetryInfo retryInfo = + NetworkUtility.shouldRetryException( + request, e, requestStart, httpResponse, responseContents); + // We should already be on a background thread, so we can invoke the retry inline. + NetworkUtility.attemptRetryOnException(request, retryInfo); + } + } + } + + /** + * Converts Headers[] to Map<String, String>. + * + * @deprecated Should never have been exposed in the API. This method may be removed in a future + * release of Volley. + */ + @Deprecated + protected static Map convertHeaders(Header[] headers) { + Map result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (int i = 0; i < headers.length; i++) { + result.put(headers[i].getName(), headers[i].getValue()); + } + return result; + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/ByteArrayPool.java b/core/src/main/java/com/android/volley/toolbox/ByteArrayPool.java new file mode 100644 index 0000000..0134fa2 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/ByteArrayPool.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * ByteArrayPool is a source and repository of byte[] objects. Its purpose is to supply + * those buffers to consumers who need to use them for a short period of time and then dispose of + * them. Simply creating and disposing such buffers in the conventional manner can considerable heap + * churn and garbage collection delays on Android, which lacks good management of short-lived heap + * objects. It may be advantageous to trade off some memory in the form of a permanently allocated + * pool of buffers in order to gain heap performance improvements; that is what this class does. + * + *

A good candidate user for this class is something like an I/O system that uses large temporary + * byte[] buffers to copy data around. In these use cases, often the consumer wants the + * buffer to be a certain minimum size to ensure good performance (e.g. when copying data chunks off + * of a stream), but doesn't mind if the buffer is larger than the minimum. Taking this into account + * and also to maximize the odds of being able to reuse a recycled buffer, this class is free to + * return buffers larger than the requested size. The caller needs to be able to gracefully deal + * with getting buffers any size over the minimum. + * + *

If there is not a suitably-sized buffer in its recycling pool when a buffer is requested, this + * class will allocate a new buffer and return it. + * + *

This class has no special ownership of buffers it creates; the caller is free to take a buffer + * it receives from this pool, use it permanently, and never return it to the pool; additionally, it + * is not harmful to return to this pool a buffer that was allocated elsewhere, provided there are + * no other lingering references to it. + * + *

This class ensures that the total size of the buffers in its recycling pool never exceeds a + * certain byte limit. When a buffer is returned that would cause the pool to exceed the limit, + * least-recently-used buffers are disposed. + */ +public class ByteArrayPool { + /** The buffer pool, arranged both by last use and by buffer size */ + private final List mBuffersByLastUse = new ArrayList<>(); + + private final List mBuffersBySize = new ArrayList<>(64); + + /** The total size of the buffers in the pool */ + private int mCurrentSize = 0; + + /** + * The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay + * under this limit. + */ + private final int mSizeLimit; + + /** Compares buffers by size */ + protected static final Comparator BUF_COMPARATOR = + new Comparator() { + @Override + public int compare(byte[] lhs, byte[] rhs) { + return lhs.length - rhs.length; + } + }; + + /** @param sizeLimit the maximum size of the pool, in bytes */ + public ByteArrayPool(int sizeLimit) { + mSizeLimit = sizeLimit; + } + + /** + * Returns a buffer from the pool if one is available in the requested size, or allocates a new + * one if a pooled one is not available. + * + * @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be + * larger. + * @return a byte[] buffer is always returned. + */ + public synchronized byte[] getBuf(int len) { + for (int i = 0; i < mBuffersBySize.size(); i++) { + byte[] buf = mBuffersBySize.get(i); + if (buf.length >= len) { + mCurrentSize -= buf.length; + mBuffersBySize.remove(i); + mBuffersByLastUse.remove(buf); + return buf; + } + } + return new byte[len]; + } + + /** + * Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted + * size. + * + * @param buf the buffer to return to the pool. + */ + public synchronized void returnBuf(byte[] buf) { + if (buf == null || buf.length > mSizeLimit) { + return; + } + mBuffersByLastUse.add(buf); + int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR); + if (pos < 0) { + pos = -pos - 1; + } + mBuffersBySize.add(pos, buf); + mCurrentSize += buf.length; + trim(); + } + + /** Removes buffers from the pool until it is under its size limit. */ + private synchronized void trim() { + while (mCurrentSize > mSizeLimit) { + byte[] buf = mBuffersByLastUse.remove(0); + mBuffersBySize.remove(buf); + mCurrentSize -= buf.length; + } + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java b/core/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java new file mode 100644 index 0000000..856ef80 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.os.Handler; +import android.os.Looper; +import com.android.volley.Cache; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; + +/** A synthetic request used for clearing the cache. */ +public class ClearCacheRequest extends Request { + private final Cache mCache; + private final Runnable mCallback; + + /** + * Creates a synthetic request for clearing the cache. + * + * @param cache Cache to clear + * @param callback Callback to make on the main thread once the cache is clear, or null for none + */ + public ClearCacheRequest(Cache cache, Runnable callback) { + super(Method.GET, null, null); + mCache = cache; + mCallback = callback; + } + + @Override + public boolean isCanceled() { + // This is a little bit of a hack, but hey, why not. + mCache.clear(); + if (mCallback != null) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.postAtFrontOfQueue(mCallback); + } + return true; + } + + @Override + public Priority getPriority() { + return Priority.IMMEDIATE; + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected void deliverResponse(Object response) {} +} diff --git a/core/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/core/src/main/java/com/android/volley/toolbox/DiskBasedCache.java new file mode 100644 index 0000000..d4310e0 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/DiskBasedCache.java @@ -0,0 +1,677 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.os.SystemClock; +import android.text.TextUtils; +import androidx.annotation.VisibleForTesting; +import com.android.volley.Cache; +import com.android.volley.Header; +import com.android.volley.VolleyLog; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Cache implementation that caches files directly onto the hard disk in the specified directory. + * The default disk usage size is 5MB, but is configurable. + * + *

This cache supports the {@link Entry#allResponseHeaders} headers field. + */ +public class DiskBasedCache implements Cache { + + /** Map of the Key, CacheHeader pairs */ + private final Map mEntries = new LinkedHashMap<>(16, .75f, true); + + /** Total amount of space currently used by the cache in bytes. */ + private long mTotalSize = 0; + + /** The supplier for the root directory to use for the cache. */ + private final FileSupplier mRootDirectorySupplier; + + /** The maximum size of the cache in bytes. */ + private final int mMaxCacheSizeInBytes; + + /** Default maximum disk usage in bytes. */ + private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024; + + /** High water mark percentage for the cache */ + @VisibleForTesting static final float HYSTERESIS_FACTOR = 0.9f; + + /** Magic number for current version of cache file format. */ + private static final int CACHE_MAGIC = 0x20150306; + + /** + * Constructs an instance of the DiskBasedCache at the specified directory. + * + * @param rootDirectory The root directory of the cache. + * @param maxCacheSizeInBytes The maximum size of the cache in bytes. Note that the cache may + * briefly exceed this size on disk when writing a new entry that pushes it over the limit + * until the ensuing pruning completes. + */ + public DiskBasedCache(final File rootDirectory, int maxCacheSizeInBytes) { + mRootDirectorySupplier = + new FileSupplier() { + @Override + public File get() { + return rootDirectory; + } + }; + mMaxCacheSizeInBytes = maxCacheSizeInBytes; + } + + /** + * Constructs an instance of the DiskBasedCache at the specified directory. + * + * @param rootDirectorySupplier The supplier for the root directory of the cache. + * @param maxCacheSizeInBytes The maximum size of the cache in bytes. Note that the cache may + * briefly exceed this size on disk when writing a new entry that pushes it over the limit + * until the ensuing pruning completes. + */ + public DiskBasedCache(FileSupplier rootDirectorySupplier, int maxCacheSizeInBytes) { + mRootDirectorySupplier = rootDirectorySupplier; + mMaxCacheSizeInBytes = maxCacheSizeInBytes; + } + + /** + * Constructs an instance of the DiskBasedCache at the specified directory using the default + * maximum cache size of 5MB. + * + * @param rootDirectory The root directory of the cache. + */ + public DiskBasedCache(File rootDirectory) { + this(rootDirectory, DEFAULT_DISK_USAGE_BYTES); + } + + /** + * Constructs an instance of the DiskBasedCache at the specified directory using the default + * maximum cache size of 5MB. + * + * @param rootDirectorySupplier The supplier for the root directory of the cache. + */ + public DiskBasedCache(FileSupplier rootDirectorySupplier) { + this(rootDirectorySupplier, DEFAULT_DISK_USAGE_BYTES); + } + + /** Clears the cache. Deletes all cached files from disk. */ + @Override + public synchronized void clear() { + File[] files = mRootDirectorySupplier.get().listFiles(); + if (files != null) { + for (File file : files) { + file.delete(); + } + } + mEntries.clear(); + mTotalSize = 0; + VolleyLog.d("Cache cleared."); + } + + /** Returns the cache entry with the specified key if it exists, null otherwise. */ + @Override + public synchronized Entry get(String key) { + CacheHeader entry = mEntries.get(key); + // if the entry does not exist, return. + if (entry == null) { + return null; + } + File file = getFileForKey(key); + try { + CountingInputStream cis = + new CountingInputStream( + new BufferedInputStream(createInputStream(file)), file.length()); + try { + CacheHeader entryOnDisk = CacheHeader.readHeader(cis); + if (!TextUtils.equals(key, entryOnDisk.key)) { + // File was shared by two keys and now holds data for a different entry! + VolleyLog.d( + "%s: key=%s, found=%s", file.getAbsolutePath(), key, entryOnDisk.key); + // Remove key whose contents on disk have been replaced. + removeEntry(key); + return null; + } + byte[] data = streamToBytes(cis, cis.bytesRemaining()); + return entry.toCacheEntry(data); + } finally { + // Any IOException thrown here is handled by the below catch block by design. + //noinspection ThrowFromFinallyBlock + cis.close(); + } + } catch (IOException e) { + VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString()); + remove(key); + return null; + } + } + + /** + * Initializes the DiskBasedCache by scanning for all files currently in the specified root + * directory. Creates the root directory if necessary. + */ + @Override + public synchronized void initialize() { + File rootDirectory = mRootDirectorySupplier.get(); + if (!rootDirectory.exists()) { + if (!rootDirectory.mkdirs()) { + VolleyLog.e("Unable to create cache dir %s", rootDirectory.getAbsolutePath()); + } + return; + } + File[] files = rootDirectory.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + try { + long entrySize = file.length(); + CountingInputStream cis = + new CountingInputStream( + new BufferedInputStream(createInputStream(file)), entrySize); + try { + CacheHeader entry = CacheHeader.readHeader(cis); + entry.size = entrySize; + putEntry(entry.key, entry); + } finally { + // Any IOException thrown here is handled by the below catch block by design. + //noinspection ThrowFromFinallyBlock + cis.close(); + } + } catch (IOException e) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + } + + /** + * Invalidates an entry in the cache. + * + * @param key Cache key + * @param fullExpire True to fully expire the entry, false to soft expire + */ + @Override + public synchronized void invalidate(String key, boolean fullExpire) { + Entry entry = get(key); + if (entry != null) { + entry.softTtl = 0; + if (fullExpire) { + entry.ttl = 0; + } + put(key, entry); + } + } + + /** Puts the entry with the specified key into the cache. */ + @Override + public synchronized void put(String key, Entry entry) { + // If adding this entry would trigger a prune, but pruning would cause the new entry to be + // deleted, then skip writing the entry in the first place, as this is just churn. + // Note that we don't include the cache header overhead in this calculation for simplicity, + // so putting entries which are just below the threshold may still cause this churn. + if (mTotalSize + entry.data.length > mMaxCacheSizeInBytes + && entry.data.length > mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) { + return; + } + File file = getFileForKey(key); + try { + BufferedOutputStream fos = new BufferedOutputStream(createOutputStream(file)); + CacheHeader e = new CacheHeader(key, entry); + boolean success = e.writeHeader(fos); + if (!success) { + fos.close(); + VolleyLog.d("Failed to write header for %s", file.getAbsolutePath()); + throw new IOException(); + } + fos.write(entry.data); + fos.close(); + e.size = file.length(); + putEntry(key, e); + pruneIfNeeded(); + } catch (IOException e) { + boolean deleted = file.delete(); + if (!deleted) { + VolleyLog.d("Could not clean up file %s", file.getAbsolutePath()); + } + initializeIfRootDirectoryDeleted(); + } + } + + /** Removes the specified key from the cache if it exists. */ + @Override + public synchronized void remove(String key) { + boolean deleted = getFileForKey(key).delete(); + removeEntry(key); + if (!deleted) { + VolleyLog.d( + "Could not delete cache entry for key=%s, filename=%s", + key, getFilenameForKey(key)); + } + } + + /** + * Creates a pseudo-unique filename for the specified cache key. + * + * @param key The key to generate a file name for. + * @return A pseudo-unique filename. + */ + private String getFilenameForKey(String key) { + int firstHalfLength = key.length() / 2; + String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode()); + localFilename += String.valueOf(key.substring(firstHalfLength).hashCode()); + return localFilename; + } + + /** Returns a file object for the given cache key. */ + public File getFileForKey(String key) { + return new File(mRootDirectorySupplier.get(), getFilenameForKey(key)); + } + + /** Re-initialize the cache if the directory was deleted. */ + private void initializeIfRootDirectoryDeleted() { + if (!mRootDirectorySupplier.get().exists()) { + VolleyLog.d("Re-initializing cache after external clearing."); + mEntries.clear(); + mTotalSize = 0; + initialize(); + } + } + + /** Represents a supplier for {@link File}s. */ + public interface FileSupplier { + File get(); + } + + /** Prunes the cache to fit the maximum size. */ + private void pruneIfNeeded() { + if (mTotalSize < mMaxCacheSizeInBytes) { + return; + } + if (VolleyLog.DEBUG) { + VolleyLog.v("Pruning old cache entries."); + } + + long before = mTotalSize; + int prunedFiles = 0; + long startTime = SystemClock.elapsedRealtime(); + + Iterator> iterator = mEntries.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + CacheHeader e = entry.getValue(); + boolean deleted = getFileForKey(e.key).delete(); + if (deleted) { + mTotalSize -= e.size; + } else { + VolleyLog.d( + "Could not delete cache entry for key=%s, filename=%s", + e.key, getFilenameForKey(e.key)); + } + iterator.remove(); + prunedFiles++; + + if (mTotalSize < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) { + break; + } + } + + if (VolleyLog.DEBUG) { + VolleyLog.v( + "pruned %d files, %d bytes, %d ms", + prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime); + } + } + + /** + * Puts the entry with the specified key into the cache. + * + * @param key The key to identify the entry by. + * @param entry The entry to cache. + */ + private void putEntry(String key, CacheHeader entry) { + if (!mEntries.containsKey(key)) { + mTotalSize += entry.size; + } else { + CacheHeader oldEntry = mEntries.get(key); + mTotalSize += (entry.size - oldEntry.size); + } + mEntries.put(key, entry); + } + + /** Removes the entry identified by 'key' from the cache. */ + private void removeEntry(String key) { + CacheHeader removed = mEntries.remove(key); + if (removed != null) { + mTotalSize -= removed.size; + } + } + + /** + * Reads length bytes from CountingInputStream into byte array. + * + * @param cis input stream + * @param length number of bytes to read + * @throws IOException if fails to read all bytes + */ + @VisibleForTesting + static byte[] streamToBytes(CountingInputStream cis, long length) throws IOException { + long maxLength = cis.bytesRemaining(); + // Length cannot be negative or greater than bytes remaining, and must not overflow int. + if (length < 0 || length > maxLength || (int) length != length) { + throw new IOException("streamToBytes length=" + length + ", maxLength=" + maxLength); + } + byte[] bytes = new byte[(int) length]; + new DataInputStream(cis).readFully(bytes); + return bytes; + } + + @VisibleForTesting + InputStream createInputStream(File file) throws FileNotFoundException { + return new FileInputStream(file); + } + + @VisibleForTesting + OutputStream createOutputStream(File file) throws FileNotFoundException { + return new FileOutputStream(file); + } + + /** Handles holding onto the cache headers for an entry. */ + @VisibleForTesting + static class CacheHeader { + /** + * The size of the data identified by this CacheHeader on disk (both header and data). + * + *

Must be set by the caller after it has been calculated. + * + *

This is not serialized to disk. + */ + long size; + + /** The key that identifies the cache entry. */ + final String key; + + /** ETag for cache coherence. */ + final String etag; + + /** Date of this response as reported by the server. */ + final long serverDate; + + /** The last modified date for the requested object. */ + final long lastModified; + + /** TTL for this record. */ + final long ttl; + + /** Soft TTL for this record. */ + final long softTtl; + + /** Headers from the response resulting in this cache entry. */ + final List

allResponseHeaders; + + private CacheHeader( + String key, + String etag, + long serverDate, + long lastModified, + long ttl, + long softTtl, + List
allResponseHeaders) { + this.key = key; + this.etag = "".equals(etag) ? null : etag; + this.serverDate = serverDate; + this.lastModified = lastModified; + this.ttl = ttl; + this.softTtl = softTtl; + this.allResponseHeaders = allResponseHeaders; + } + + /** + * Instantiates a new CacheHeader object. + * + * @param key The key that identifies the cache entry + * @param entry The cache entry. + */ + CacheHeader(String key, Entry entry) { + this( + key, + entry.etag, + entry.serverDate, + entry.lastModified, + entry.ttl, + entry.softTtl, + getAllResponseHeaders(entry)); + } + + private static List
getAllResponseHeaders(Entry entry) { + // If the entry contains all the response headers, use that field directly. + if (entry.allResponseHeaders != null) { + return entry.allResponseHeaders; + } + + // Legacy fallback - copy headers from the map. + return HttpHeaderParser.toAllHeaderList(entry.responseHeaders); + } + + /** + * Reads the header from a CountingInputStream and returns a CacheHeader object. + * + * @param is The InputStream to read from. + * @throws IOException if fails to read header + */ + static CacheHeader readHeader(CountingInputStream is) throws IOException { + int magic = readInt(is); + if (magic != CACHE_MAGIC) { + // don't bother deleting, it'll get pruned eventually + throw new IOException(); + } + String key = readString(is); + String etag = readString(is); + long serverDate = readLong(is); + long lastModified = readLong(is); + long ttl = readLong(is); + long softTtl = readLong(is); + List
allResponseHeaders = readHeaderList(is); + return new CacheHeader( + key, etag, serverDate, lastModified, ttl, softTtl, allResponseHeaders); + } + + /** Creates a cache entry for the specified data. */ + Entry toCacheEntry(byte[] data) { + Entry e = new Entry(); + e.data = data; + e.etag = etag; + e.serverDate = serverDate; + e.lastModified = lastModified; + e.ttl = ttl; + e.softTtl = softTtl; + e.responseHeaders = HttpHeaderParser.toHeaderMap(allResponseHeaders); + e.allResponseHeaders = Collections.unmodifiableList(allResponseHeaders); + return e; + } + + /** Writes the contents of this CacheHeader to the specified OutputStream. */ + boolean writeHeader(OutputStream os) { + try { + writeInt(os, CACHE_MAGIC); + writeString(os, key); + writeString(os, etag == null ? "" : etag); + writeLong(os, serverDate); + writeLong(os, lastModified); + writeLong(os, ttl); + writeLong(os, softTtl); + writeHeaderList(allResponseHeaders, os); + os.flush(); + return true; + } catch (IOException e) { + VolleyLog.d("%s", e.toString()); + return false; + } + } + } + + @VisibleForTesting + static class CountingInputStream extends FilterInputStream { + private final long length; + private long bytesRead; + + CountingInputStream(InputStream in, long length) { + super(in); + this.length = length; + } + + @Override + public int read() throws IOException { + int result = super.read(); + if (result != -1) { + bytesRead++; + } + return result; + } + + @Override + public int read(byte[] buffer, int offset, int count) throws IOException { + int result = super.read(buffer, offset, count); + if (result != -1) { + bytesRead += result; + } + return result; + } + + @VisibleForTesting + long bytesRead() { + return bytesRead; + } + + long bytesRemaining() { + return length - bytesRead; + } + } + + /* + * Homebrewed simple serialization system used for reading and writing cache + * headers on disk. Once upon a time, this used the standard Java + * Object{Input,Output}Stream, but the default implementation relies heavily + * on reflection (even for standard types) and generates a ton of garbage. + * + * TODO: Replace by standard DataInput and DataOutput in next cache version. + */ + + /** + * Simple wrapper around {@link InputStream#read()} that throws EOFException instead of + * returning -1. + */ + private static int read(InputStream is) throws IOException { + int b = is.read(); + if (b == -1) { + throw new EOFException(); + } + return b; + } + + static void writeInt(OutputStream os, int n) throws IOException { + os.write((n >> 0) & 0xff); + os.write((n >> 8) & 0xff); + os.write((n >> 16) & 0xff); + os.write((n >> 24) & 0xff); + } + + static int readInt(InputStream is) throws IOException { + int n = 0; + n |= (read(is) << 0); + n |= (read(is) << 8); + n |= (read(is) << 16); + n |= (read(is) << 24); + return n; + } + + static void writeLong(OutputStream os, long n) throws IOException { + os.write((byte) (n >>> 0)); + os.write((byte) (n >>> 8)); + os.write((byte) (n >>> 16)); + os.write((byte) (n >>> 24)); + os.write((byte) (n >>> 32)); + os.write((byte) (n >>> 40)); + os.write((byte) (n >>> 48)); + os.write((byte) (n >>> 56)); + } + + static long readLong(InputStream is) throws IOException { + long n = 0; + n |= ((read(is) & 0xFFL) << 0); + n |= ((read(is) & 0xFFL) << 8); + n |= ((read(is) & 0xFFL) << 16); + n |= ((read(is) & 0xFFL) << 24); + n |= ((read(is) & 0xFFL) << 32); + n |= ((read(is) & 0xFFL) << 40); + n |= ((read(is) & 0xFFL) << 48); + n |= ((read(is) & 0xFFL) << 56); + return n; + } + + static void writeString(OutputStream os, String s) throws IOException { + byte[] b = s.getBytes("UTF-8"); + writeLong(os, b.length); + os.write(b, 0, b.length); + } + + static String readString(CountingInputStream cis) throws IOException { + long n = readLong(cis); + byte[] b = streamToBytes(cis, n); + return new String(b, "UTF-8"); + } + + static void writeHeaderList(List
headers, OutputStream os) throws IOException { + if (headers != null) { + writeInt(os, headers.size()); + for (Header header : headers) { + writeString(os, header.getName()); + writeString(os, header.getValue()); + } + } else { + writeInt(os, 0); + } + } + + static List
readHeaderList(CountingInputStream cis) throws IOException { + int size = readInt(cis); + if (size < 0) { + throw new IOException("readHeaderList size=" + size); + } + List
result = + (size == 0) ? Collections.
emptyList() : new ArrayList
(); + for (int i = 0; i < size; i++) { + String name = readString(cis).intern(); + String value = readString(cis).intern(); + result.add(new Header(name, value)); + } + return result; + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/FileSupplier.java b/core/src/main/java/com/android/volley/toolbox/FileSupplier.java new file mode 100644 index 0000000..70898a6 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/FileSupplier.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import java.io.File; + +/** Represents a supplier for {@link File}s. */ +public interface FileSupplier { + File get(); +} diff --git a/core/src/main/java/com/android/volley/toolbox/HttpClientStack.java b/core/src/main/java/com/android/volley/toolbox/HttpClientStack.java new file mode 100644 index 0000000..1e9e4b0 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/HttpClientStack.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.Request.Method; +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpTrace; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; + +/** + * An HttpStack that performs request over an {@link HttpClient}. + * + * @deprecated The Apache HTTP library on Android is deprecated. Use {@link HurlStack} or another + * {@link BaseHttpStack} implementation. + */ +@Deprecated +public class HttpClientStack implements HttpStack { + protected final HttpClient mClient; + + private static final String HEADER_CONTENT_TYPE = "Content-Type"; + + public HttpClientStack(HttpClient client) { + mClient = client; + } + + private static void setHeaders(HttpUriRequest httpRequest, Map headers) { + for (String key : headers.keySet()) { + httpRequest.setHeader(key, headers.get(key)); + } + } + + @SuppressWarnings("unused") + private static List getPostParameterPairs(Map postParams) { + List result = new ArrayList<>(postParams.size()); + for (String key : postParams.keySet()) { + result.add(new BasicNameValuePair(key, postParams.get(key))); + } + return result; + } + + @Override + public HttpResponse performRequest(Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders); + setHeaders(httpRequest, additionalHeaders); + // Request.getHeaders() takes precedence over the given additional (cache) headers) and any + // headers set by createHttpRequest (like the Content-Type header). + setHeaders(httpRequest, request.getHeaders()); + onPrepareRequest(httpRequest); + HttpParams httpParams = httpRequest.getParams(); + int timeoutMs = request.getTimeoutMs(); + // TODO: Reevaluate this connection timeout based on more wide-scale + // data collection and possibly different for wifi vs. 3G. + HttpConnectionParams.setConnectionTimeout(httpParams, 5000); + HttpConnectionParams.setSoTimeout(httpParams, timeoutMs); + return mClient.execute(httpRequest); + } + + /** Creates the appropriate subclass of HttpUriRequest for passed in request. */ + @SuppressWarnings("deprecation") + /* protected */ static HttpUriRequest createHttpRequest( + Request request, Map additionalHeaders) throws AuthFailureError { + switch (request.getMethod()) { + case Method.DEPRECATED_GET_OR_POST: + { + // This is the deprecated way that needs to be handled for backwards + // compatibility. + // If the request's post body is null, then the assumption is that the request + // is + // GET. Otherwise, it is assumed that the request is a POST. + byte[] postBody = request.getPostBody(); + if (postBody != null) { + HttpPost postRequest = new HttpPost(request.getUrl()); + postRequest.addHeader( + HEADER_CONTENT_TYPE, request.getPostBodyContentType()); + HttpEntity entity; + entity = new ByteArrayEntity(postBody); + postRequest.setEntity(entity); + return postRequest; + } else { + return new HttpGet(request.getUrl()); + } + } + case Method.GET: + return new HttpGet(request.getUrl()); + case Method.DELETE: + return new HttpDelete(request.getUrl()); + case Method.POST: + { + HttpPost postRequest = new HttpPost(request.getUrl()); + postRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); + setEntityIfNonEmptyBody(postRequest, request); + return postRequest; + } + case Method.PUT: + { + HttpPut putRequest = new HttpPut(request.getUrl()); + putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); + setEntityIfNonEmptyBody(putRequest, request); + return putRequest; + } + case Method.HEAD: + return new HttpHead(request.getUrl()); + case Method.OPTIONS: + return new HttpOptions(request.getUrl()); + case Method.TRACE: + return new HttpTrace(request.getUrl()); + case Method.PATCH: + { + HttpPatch patchRequest = new HttpPatch(request.getUrl()); + patchRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); + setEntityIfNonEmptyBody(patchRequest, request); + return patchRequest; + } + default: + throw new IllegalStateException("Unknown request method."); + } + } + + private static void setEntityIfNonEmptyBody( + HttpEntityEnclosingRequestBase httpRequest, Request request) + throws AuthFailureError { + byte[] body = request.getBody(); + if (body != null) { + HttpEntity entity = new ByteArrayEntity(body); + httpRequest.setEntity(entity); + } + } + + /** + * Called before the request is executed using the underlying HttpClient. + * + *

Overwrite in subclasses to augment the request. + */ + protected void onPrepareRequest(HttpUriRequest request) throws IOException { + // Nothing. + } + + /** + * The HttpPatch class does not exist in the Android framework, so this has been defined here. + */ + public static final class HttpPatch extends HttpEntityEnclosingRequestBase { + + public static final String METHOD_NAME = "PATCH"; + + public HttpPatch() { + super(); + } + + public HttpPatch(final URI uri) { + super(); + setURI(uri); + } + + /** @throws IllegalArgumentException if the uri is invalid. */ + public HttpPatch(final String uri) { + super(); + setURI(URI.create(uri)); + } + + @Override + public String getMethod() { + return METHOD_NAME; + } + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java new file mode 100644 index 0000000..0b29e80 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.android.volley.Cache; +import com.android.volley.Header; +import com.android.volley.NetworkResponse; +import com.android.volley.VolleyLog; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeMap; +import java.util.TreeSet; + +/** Utility methods for parsing HTTP headers. */ +public class HttpHeaderParser { + + @RestrictTo({Scope.LIBRARY_GROUP}) + public static final String HEADER_CONTENT_TYPE = "Content-Type"; + + private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1"; + + private static final String RFC1123_PARSE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; + + // Hardcode 'GMT' rather than using 'zzz' since some platforms append an extraneous +00:00. + // See #287. + private static final String RFC1123_OUTPUT_FORMAT = "EEE, dd MMM yyyy HH:mm:ss 'GMT'"; + + /** + * Extracts a {@link com.android.volley.Cache.Entry} from a {@link NetworkResponse}. + * + * @param response The network response to parse headers from + * @return a cache entry for the given response, or null if the response is not cacheable. + */ + @Nullable + public static Cache.Entry parseCacheHeaders(NetworkResponse response) { + long now = System.currentTimeMillis(); + + Map headers = response.headers; + if (headers == null) { + return null; + } + + long serverDate = 0; + long lastModified = 0; + long serverExpires = 0; + long softExpire = 0; + long finalExpire = 0; + long maxAge = 0; + long staleWhileRevalidate = 0; + boolean hasCacheControl = false; + boolean mustRevalidate = false; + + String serverEtag = null; + String headerValue; + + headerValue = headers.get("Date"); + if (headerValue != null) { + serverDate = parseDateAsEpoch(headerValue); + } + + headerValue = headers.get("Cache-Control"); + if (headerValue != null) { + hasCacheControl = true; + String[] tokens = headerValue.split(",", 0); + for (int i = 0; i < tokens.length; i++) { + String token = tokens[i].trim(); + if (token.equals("no-cache") || token.equals("no-store")) { + return null; + } else if (token.startsWith("max-age=")) { + try { + maxAge = Long.parseLong(token.substring(8)); + } catch (Exception e) { + } + } else if (token.startsWith("stale-while-revalidate=")) { + try { + staleWhileRevalidate = Long.parseLong(token.substring(23)); + } catch (Exception e) { + } + } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) { + mustRevalidate = true; + } + } + } + + headerValue = headers.get("Expires"); + if (headerValue != null) { + serverExpires = parseDateAsEpoch(headerValue); + } + + headerValue = headers.get("Last-Modified"); + if (headerValue != null) { + lastModified = parseDateAsEpoch(headerValue); + } + + serverEtag = headers.get("ETag"); + + // Cache-Control takes precedence over an Expires header, even if both exist and Expires + // is more restrictive. + if (hasCacheControl) { + softExpire = now + maxAge * 1000; + finalExpire = mustRevalidate ? softExpire : softExpire + staleWhileRevalidate * 1000; + } else if (serverDate > 0 && serverExpires >= serverDate) { + // Default semantic for Expire header in HTTP specification is softExpire. + softExpire = now + (serverExpires - serverDate); + finalExpire = softExpire; + } + + Cache.Entry entry = new Cache.Entry(); + entry.data = response.data; + entry.etag = serverEtag; + entry.softTtl = softExpire; + entry.ttl = finalExpire; + entry.serverDate = serverDate; + entry.lastModified = lastModified; + entry.responseHeaders = headers; + entry.allResponseHeaders = response.allHeaders; + + return entry; + } + + /** Parse date in RFC1123 format, and return its value as epoch */ + public static long parseDateAsEpoch(String dateStr) { + try { + // Parse date in RFC1123 format if this header contains one + return newUsGmtFormatter(RFC1123_PARSE_FORMAT).parse(dateStr).getTime(); + } catch (ParseException e) { + // Date in invalid format, fallback to 0 + // If the value is either "0" or "-1" we only log to verbose, + // these values are pretty common and cause log spam. + String message = "Unable to parse dateStr: %s, falling back to 0"; + if ("0".equals(dateStr) || "-1".equals(dateStr)) { + VolleyLog.v(message, dateStr); + } else { + VolleyLog.e(e, message, dateStr); + } + + return 0; + } + } + + /** Format an epoch date in RFC1123 format. */ + static String formatEpochAsRfc1123(long epoch) { + return newUsGmtFormatter(RFC1123_OUTPUT_FORMAT).format(new Date(epoch)); + } + + private static SimpleDateFormat newUsGmtFormatter(String format) { + SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.US); + formatter.setTimeZone(TimeZone.getTimeZone("GMT")); + return formatter; + } + + /** + * Retrieve a charset from headers + * + * @param headers An {@link java.util.Map} of headers + * @param defaultCharset Charset to return if none can be found + * @return Returns the charset specified in the Content-Type of this header, or the + * defaultCharset if none can be found. + */ + public static String parseCharset( + @Nullable Map headers, String defaultCharset) { + if (headers == null) { + return defaultCharset; + } + String contentType = headers.get(HEADER_CONTENT_TYPE); + if (contentType != null) { + String[] params = contentType.split(";", 0); + for (int i = 1; i < params.length; i++) { + String[] pair = params[i].trim().split("=", 0); + if (pair.length == 2) { + if (pair[0].equals("charset")) { + return pair[1]; + } + } + } + } + + return defaultCharset; + } + + /** + * Returns the charset specified in the Content-Type of this header, or the HTTP default + * (ISO-8859-1) if none can be found. + */ + public static String parseCharset(@Nullable Map headers) { + return parseCharset(headers, DEFAULT_CONTENT_CHARSET); + } + + // Note - these are copied from NetworkResponse to avoid making them public (as needed to access + // them from the .toolbox package), which would mean they'd become part of the Volley API. + // TODO: Consider obfuscating official releases so we can share utility methods between Volley + // and Toolbox without making them public APIs. + + static Map toHeaderMap(List

allHeaders) { + Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + // Later elements in the list take precedence. + for (Header header : allHeaders) { + headers.put(header.getName(), header.getValue()); + } + return headers; + } + + static List
toAllHeaderList(Map headers) { + List
allHeaders = new ArrayList<>(headers.size()); + for (Map.Entry header : headers.entrySet()) { + allHeaders.add(new Header(header.getKey(), header.getValue())); + } + return allHeaders; + } + + /** + * Combine cache headers with network response headers for an HTTP 304 response. + * + *

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

combineHeaders(List
responseHeaders, Cache.Entry entry) { + // First, create a case-insensitive set of header names from the network + // response. + Set 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
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 header : entry.responseHeaders.entrySet()) { + if (!headerNamesFromNetworkResponse.contains(header.getKey())) { + combinedHeaders.add(new Header(header.getKey(), header.getValue())); + } + } + } + } + return combinedHeaders; + } + + static Map getCacheHeaders(Cache.Entry entry) { + // If there's no cache entry, we're done. + if (entry == null) { + return Collections.emptyMap(); + } + + Map headers = new HashMap<>(); + + if (entry.etag != null) { + headers.put("If-None-Match", entry.etag); + } + + if (entry.lastModified > 0) { + headers.put( + "If-Modified-Since", HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified)); + } + + return headers; + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/HttpResponse.java b/core/src/main/java/com/android/volley/toolbox/HttpResponse.java new file mode 100644 index 0000000..595f926 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/HttpResponse.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.volley.toolbox; + +import androidx.annotation.Nullable; +import com.android.volley.Header; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; + +/** A response from an HTTP server. */ +public final class HttpResponse { + + private final int mStatusCode; + private final List
mHeaders; + private final int mContentLength; + @Nullable private final InputStream mContent; + @Nullable private final byte[] mContentBytes; + + /** + * Construct a new HttpResponse for an empty response body. + * + * @param statusCode the HTTP status code of the response + * @param headers the response headers + */ + public HttpResponse(int statusCode, List
headers) { + this(statusCode, headers, /* contentLength= */ -1, /* content= */ null); + } + + /** + * Construct a new HttpResponse. + * + * @param statusCode the HTTP status code of the response + * @param headers the response headers + * @param contentLength the length of the response content. Ignored if there is no content. + * @param content an {@link InputStream} of the response content. May be null to indicate that + * the response has no content. + */ + public HttpResponse( + int statusCode, List
headers, int contentLength, InputStream content) { + mStatusCode = statusCode; + mHeaders = headers; + mContentLength = contentLength; + mContent = content; + mContentBytes = null; + } + + /** + * Construct a new HttpResponse. + * + * @param statusCode the HTTP status code of the response + * @param headers the response headers + * @param contentBytes a byte[] of the response content. This is an optimization for HTTP stacks + * that natively support returning a byte[]. + */ + public HttpResponse(int statusCode, List
headers, byte[] contentBytes) { + mStatusCode = statusCode; + mHeaders = headers; + mContentLength = contentBytes.length; + mContentBytes = contentBytes; + mContent = null; + } + + /** Returns the HTTP status code of the response. */ + public final int getStatusCode() { + return mStatusCode; + } + + /** Returns the response headers. Must not be mutated directly. */ + public final List
getHeaders() { + return Collections.unmodifiableList(mHeaders); + } + + /** Returns the length of the content. Only valid if {@link #getContent} is non-null. */ + public final int getContentLength() { + return mContentLength; + } + + /** + * If a byte[] was already provided by an HTTP stack that natively supports returning one, this + * method will return that byte[] as an optimization over copying the bytes from an input + * stream. It may return null, even if the response has content, as long as mContent is + * provided. + */ + @Nullable + public final byte[] getContentBytes() { + return mContentBytes; + } + + /** + * Returns an {@link InputStream} of the response content. May be null to indicate that the + * response has no content. + */ + @Nullable + public final InputStream getContent() { + if (mContent != null) { + return mContent; + } else if (mContentBytes != null) { + return new ByteArrayInputStream(mContentBytes); + } else { + return null; + } + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/HttpStack.java b/core/src/main/java/com/android/volley/toolbox/HttpStack.java new file mode 100644 index 0000000..85179a7 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/HttpStack.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import java.io.IOException; +import java.util.Map; +import org.apache.http.HttpResponse; + +/** + * An HTTP stack abstraction. + * + * @deprecated This interface should be avoided as it depends on the deprecated Apache HTTP library. + * Use {@link BaseHttpStack} to avoid this dependency. This class may be removed in a future + * release of Volley. + */ +@Deprecated +public interface HttpStack { + /** + * Performs an HTTP request with the given parameters. + * + *

A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise, + * and the Content-Type header is set to request.getPostBodyContentType(). + * + * @param request the request to perform + * @param additionalHeaders additional headers to be sent together with {@link + * Request#getHeaders()} + * @return the HTTP response + */ + HttpResponse performRequest(Request request, Map additionalHeaders) + throws IOException, AuthFailureError; +} diff --git a/core/src/main/java/com/android/volley/toolbox/HurlStack.java b/core/src/main/java/com/android/volley/toolbox/HurlStack.java new file mode 100644 index 0000000..35c6a72 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/HurlStack.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import androidx.annotation.VisibleForTesting; +import com.android.volley.AuthFailureError; +import com.android.volley.Header; +import com.android.volley.Request; +import com.android.volley.Request.Method; +import java.io.DataOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +/** A {@link BaseHttpStack} based on {@link HttpURLConnection}. */ +public class HurlStack extends BaseHttpStack { + + private static final int HTTP_CONTINUE = 100; + + /** An interface for transforming URLs before use. */ + public interface UrlRewriter extends com.android.volley.toolbox.UrlRewriter {} + + private final UrlRewriter mUrlRewriter; + private final SSLSocketFactory mSslSocketFactory; + + public HurlStack() { + this(/* urlRewriter = */ null); + } + + /** @param urlRewriter Rewriter to use for request URLs */ + public HurlStack(UrlRewriter urlRewriter) { + this(urlRewriter, /* sslSocketFactory = */ null); + } + + /** + * @param urlRewriter Rewriter to use for request URLs + * @param sslSocketFactory SSL factory to use for HTTPS connections + */ + public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) { + mUrlRewriter = urlRewriter; + mSslSocketFactory = sslSocketFactory; + } + + @Override + public HttpResponse executeRequest(Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + String url = request.getUrl(); + HashMap map = new HashMap<>(); + map.putAll(additionalHeaders); + // Request.getHeaders() takes precedence over the given additional (cache) headers). + map.putAll(request.getHeaders()); + if (mUrlRewriter != null) { + String rewritten = mUrlRewriter.rewriteUrl(url); + if (rewritten == null) { + throw new IOException("URL blocked by rewriter: " + url); + } + url = rewritten; + } + URL parsedUrl = new URL(url); + HttpURLConnection connection = openConnection(parsedUrl, request); + boolean keepConnectionOpen = false; + try { + for (String headerName : map.keySet()) { + connection.setRequestProperty(headerName, map.get(headerName)); + } + setConnectionParametersForRequest(connection, request); + // Initialize HttpResponse with data from the HttpURLConnection. + int responseCode = connection.getResponseCode(); + if (responseCode == -1) { + // -1 is returned by getResponseCode() if the response code could not be retrieved. + // Signal to the caller that something was wrong with the connection. + throw new IOException("Could not retrieve response code from HttpUrlConnection."); + } + + if (!hasResponseBody(request.getMethod(), responseCode)) { + return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields())); + } + + // Need to keep the connection open until the stream is consumed by the caller. Wrap the + // stream such that close() will disconnect the connection. + keepConnectionOpen = true; + return new HttpResponse( + responseCode, + convertHeaders(connection.getHeaderFields()), + connection.getContentLength(), + createInputStream(request, connection)); + } finally { + if (!keepConnectionOpen) { + connection.disconnect(); + } + } + } + + @VisibleForTesting + static List

convertHeaders(Map> responseHeaders) { + List
headerList = new ArrayList<>(responseHeaders.size()); + for (Map.Entry> entry : responseHeaders.entrySet()) { + // HttpUrlConnection includes the status line as a header with a null key; omit it here + // since it's not really a header and the rest of Volley assumes non-null keys. + if (entry.getKey() != null) { + for (String value : entry.getValue()) { + headerList.add(new Header(entry.getKey(), value)); + } + } + } + return headerList; + } + + /** + * Checks if a response message contains a body. + * + * @see RFC 7230 section 3.3 + * @param requestMethod request method + * @param responseCode response status code + * @return whether the response has a body + */ + private static boolean hasResponseBody(int requestMethod, int responseCode) { + return requestMethod != Request.Method.HEAD + && !(HTTP_CONTINUE <= responseCode && responseCode < HttpURLConnection.HTTP_OK) + && responseCode != HttpURLConnection.HTTP_NO_CONTENT + && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED; + } + + /** + * Wrapper for a {@link HttpURLConnection}'s InputStream which disconnects the connection on + * stream close. + */ + static class UrlConnectionInputStream extends FilterInputStream { + private final HttpURLConnection mConnection; + + UrlConnectionInputStream(HttpURLConnection connection) { + super(inputStreamFromConnection(connection)); + mConnection = connection; + } + + @Override + public void close() throws IOException { + super.close(); + mConnection.disconnect(); + } + } + + /** + * Create and return an InputStream from which the response will be read. + * + *

May be overridden by subclasses to manipulate or monitor this input stream. + * + * @param request current request. + * @param connection current connection of request. + * @return an InputStream from which the response will be read. + */ + protected InputStream createInputStream(Request request, HttpURLConnection connection) { + return new UrlConnectionInputStream(connection); + } + + /** + * Initializes an {@link InputStream} from the given {@link HttpURLConnection}. + * + * @param connection + * @return an HttpEntity populated with data from connection. + */ + private static InputStream inputStreamFromConnection(HttpURLConnection connection) { + InputStream inputStream; + try { + inputStream = connection.getInputStream(); + } catch (IOException ioe) { + inputStream = connection.getErrorStream(); + } + return inputStream; + } + + /** Create an {@link HttpURLConnection} for the specified {@code url}. */ + protected HttpURLConnection createConnection(URL url) throws IOException { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + // Workaround for the M release HttpURLConnection not observing the + // HttpURLConnection.setFollowRedirects() property. + // https://code.google.com/p/android/issues/detail?id=194495 + connection.setInstanceFollowRedirects(HttpURLConnection.getFollowRedirects()); + + return connection; + } + + /** + * Opens an {@link HttpURLConnection} with parameters. + * + * @param url + * @return an open connection + * @throws IOException + */ + private HttpURLConnection openConnection(URL url, Request request) throws IOException { + HttpURLConnection connection = createConnection(url); + + int timeoutMs = request.getTimeoutMs(); + connection.setConnectTimeout(timeoutMs); + connection.setReadTimeout(timeoutMs); + connection.setUseCaches(false); + connection.setDoInput(true); + + // use caller-provided custom SslSocketFactory, if any, for HTTPS + if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) { + ((HttpsURLConnection) connection).setSSLSocketFactory(mSslSocketFactory); + } + + return connection; + } + + // NOTE: Any request headers added here (via setRequestProperty or addRequestProperty) should be + // checked against the existing properties in the connection and not overridden if already set. + @SuppressWarnings("deprecation") + /* package */ void setConnectionParametersForRequest( + HttpURLConnection connection, Request request) throws IOException, AuthFailureError { + switch (request.getMethod()) { + case Method.DEPRECATED_GET_OR_POST: + // This is the deprecated way that needs to be handled for backwards compatibility. + // If the request's post body is null, then the assumption is that the request is + // GET. Otherwise, it is assumed that the request is a POST. + byte[] postBody = request.getPostBody(); + if (postBody != null) { + connection.setRequestMethod("POST"); + addBody(connection, request, postBody); + } + break; + case Method.GET: + // Not necessary to set the request method because connection defaults to GET but + // being explicit here. + connection.setRequestMethod("GET"); + break; + case Method.DELETE: + connection.setRequestMethod("DELETE"); + break; + case Method.POST: + connection.setRequestMethod("POST"); + addBodyIfExists(connection, request); + break; + case Method.PUT: + connection.setRequestMethod("PUT"); + addBodyIfExists(connection, request); + break; + case Method.HEAD: + connection.setRequestMethod("HEAD"); + break; + case Method.OPTIONS: + connection.setRequestMethod("OPTIONS"); + break; + case Method.TRACE: + connection.setRequestMethod("TRACE"); + break; + case Method.PATCH: + connection.setRequestMethod("PATCH"); + addBodyIfExists(connection, request); + break; + default: + throw new IllegalStateException("Unknown method type."); + } + } + + private void addBodyIfExists(HttpURLConnection connection, Request request) + throws IOException, AuthFailureError { + byte[] body = request.getBody(); + if (body != null) { + addBody(connection, request, body); + } + } + + private void addBody(HttpURLConnection connection, Request request, byte[] body) + throws IOException { + // Prepare output. There is no need to set Content-Length explicitly, + // since this is handled by HttpURLConnection using the size of the prepared + // output stream. + connection.setDoOutput(true); + // Set the content-type unless it was already set (by Request#getHeaders). + if (!connection.getRequestProperties().containsKey(HttpHeaderParser.HEADER_CONTENT_TYPE)) { + connection.setRequestProperty( + HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType()); + } + DataOutputStream out = + new DataOutputStream(createOutputStream(request, connection, body.length)); + out.write(body); + out.close(); + } + + /** + * Create and return an OutputStream to which the request body will be written. + * + *

May be overridden by subclasses to manipulate or monitor this output stream. + * + * @param request current request. + * @param connection current connection of request. + * @param length size of stream to write. + * @return an OutputStream to which the request body will be written. + * @throws IOException if an I/O error occurs while creating the stream. + */ + protected OutputStream createOutputStream( + Request request, HttpURLConnection connection, int length) throws IOException { + return connection.getOutputStream(); + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/ImageLoader.java b/core/src/main/java/com/android/volley/toolbox/ImageLoader.java new file mode 100644 index 0000000..eece2cf --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/ImageLoader.java @@ -0,0 +1,541 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.volley.toolbox; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.os.Handler; +import android.os.Looper; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response.ErrorListener; +import com.android.volley.Response.Listener; +import com.android.volley.ResponseDelivery; +import com.android.volley.VolleyError; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Helper that handles loading and caching images from remote URLs. + * + *

The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)} and + * to pass in the default image listener provided by {@link ImageLoader#getImageListener(ImageView, + * int, int)}. Note that all function calls to this class must be made from the main thread, and all + * responses will be delivered to the main thread as well. Custom {@link ResponseDelivery}s which + * don't use the main thread are not supported. + */ +public class ImageLoader { + /** RequestQueue for dispatching ImageRequests onto. */ + private final RequestQueue mRequestQueue; + + /** Amount of time to wait after first response arrives before delivering all responses. */ + private int mBatchResponseDelayMs = 100; + + /** The cache implementation to be used as an L1 cache before calling into volley. */ + private final ImageCache mCache; + + /** + * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so that we can + * coalesce multiple requests to the same URL into a single network request. + */ + private final HashMap mInFlightRequests = new HashMap<>(); + + /** HashMap of the currently pending responses (waiting to be delivered). */ + private final HashMap mBatchedResponses = new HashMap<>(); + + /** Handler to the main thread. */ + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + /** Runnable for in-flight response delivery. */ + private Runnable mRunnable; + + /** + * Simple cache adapter interface. If provided to the ImageLoader, it will be used as an L1 + * cache before dispatch to Volley. Implementations must not block. Implementation with an + * LruCache is recommended. + */ + public interface ImageCache { + @Nullable + Bitmap getBitmap(String url); + + void putBitmap(String url, Bitmap bitmap); + } + + /** + * Constructs a new ImageLoader. + * + * @param queue The RequestQueue to use for making image requests. + * @param imageCache The cache to use as an L1 cache. + */ + public ImageLoader(RequestQueue queue, ImageCache imageCache) { + mRequestQueue = queue; + mCache = imageCache; + } + + /** + * The default implementation of ImageListener which handles basic functionality of showing a + * default image until the network response is received, at which point it will switch to either + * the actual image or the error image. + * + * @param view The imageView that the listener is associated with. + * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist. + * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist. + */ + public static ImageListener getImageListener( + final ImageView view, final int defaultImageResId, final int errorImageResId) { + return new ImageListener() { + @Override + public void onErrorResponse(VolleyError error) { + if (errorImageResId != 0) { + view.setImageResource(errorImageResId); + } + } + + @Override + public void onResponse(ImageContainer response, boolean isImmediate) { + if (response.getBitmap() != null) { + view.setImageBitmap(response.getBitmap()); + } else if (defaultImageResId != 0) { + view.setImageResource(defaultImageResId); + } + } + }; + } + + /** + * Interface for the response handlers on image requests. + * + *

The call flow is this: 1. Upon being attached to a request, onResponse(response, true) + * will be invoked to reflect any cached data that was already available. If the data was + * available, response.getBitmap() will be non-null. + * + *

2. After a network response returns, only one of the following cases will happen: - + * onResponse(response, false) will be called if the image was loaded. or - onErrorResponse will + * be called if there was an error loading the image. + */ + public interface ImageListener extends ErrorListener { + /** + * Listens for non-error changes to the loading of the image request. + * + * @param response Holds all information pertaining to the request, as well as the bitmap + * (if it is loaded). + * @param isImmediate True if this was called during ImageLoader.get() variants. This can be + * used to differentiate between a cached image loading and a network image loading in + * order to, for example, run an animation to fade in network loaded images. + */ + void onResponse(ImageContainer response, boolean isImmediate); + } + + /** + * Checks if the item is available in the cache. + * + * @param requestUrl The url of the remote image + * @param maxWidth The maximum width of the returned image. + * @param maxHeight The maximum height of the returned image. + * @return True if the item exists in cache, false otherwise. + */ + public boolean isCached(String requestUrl, int maxWidth, int maxHeight) { + return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); + } + + /** + * Checks if the item is available in the cache. + * + *

Must be called from the main thread. + * + * @param requestUrl The url of the remote image + * @param maxWidth The maximum width of the returned image. + * @param maxHeight The maximum height of the returned image. + * @param scaleType The scaleType of the imageView. + * @return True if the item exists in cache, false otherwise. + */ + @MainThread + public boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) { + Threads.throwIfNotOnMainThread(); + + String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); + return mCache.getBitmap(cacheKey) != null; + } + + /** + * Returns an ImageContainer for the requested URL. + * + *

The ImageContainer will contain either the specified default bitmap or the loaded bitmap. + * If the default was returned, the {@link ImageLoader} will be invoked when the request is + * fulfilled. + * + * @param requestUrl The URL of the image to be loaded. + */ + public ImageContainer get(String requestUrl, final ImageListener listener) { + return get(requestUrl, listener, /* maxWidth= */ 0, /* maxHeight= */ 0); + } + + /** + * Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with {@code + * Scaletype == ScaleType.CENTER_INSIDE}. + */ + public ImageContainer get( + String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight) { + return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); + } + + /** + * Issues a bitmap request with the given URL if that image is not available in the cache, and + * returns a bitmap container that contains all of the data relating to the request (as well as + * the default image if the requested image is not available). + * + *

Must be called from the main thread. + * + * @param requestUrl The url of the remote image + * @param imageListener The listener to call when the remote image is loaded + * @param maxWidth The maximum width of the returned image. + * @param maxHeight The maximum height of the returned image. + * @param scaleType The ImageViews ScaleType used to calculate the needed image size. + * @return A container object that contains all of the properties of the request, as well as the + * currently available image (default if remote is not loaded). + */ + @MainThread + public ImageContainer get( + String requestUrl, + ImageListener imageListener, + int maxWidth, + int maxHeight, + ScaleType scaleType) { + + // only fulfill requests that were initiated from the main thread. + Threads.throwIfNotOnMainThread(); + + final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); + + // Try to look up the request in the cache of remote images. + Bitmap cachedBitmap = mCache.getBitmap(cacheKey); + if (cachedBitmap != null) { + // Return the cached bitmap. + ImageContainer container = + new ImageContainer( + cachedBitmap, requestUrl, /* cacheKey= */ null, /* listener= */ null); + imageListener.onResponse(container, true); + return container; + } + + // The bitmap did not exist in the cache, fetch it! + ImageContainer imageContainer = + new ImageContainer(null, requestUrl, cacheKey, imageListener); + + // Update the caller to let them know that they should use the default bitmap. + imageListener.onResponse(imageContainer, true); + + // Check to see if a request is already in-flight or completed but pending batch delivery. + BatchedImageRequest request = mInFlightRequests.get(cacheKey); + if (request == null) { + request = mBatchedResponses.get(cacheKey); + } + if (request != null) { + // If it is, add this request to the list of listeners. + request.addContainer(imageContainer); + return imageContainer; + } + + // The request is not already in flight. Send the new request to the network and + // track it. + Request newRequest = + makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey); + + mRequestQueue.add(newRequest); + mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); + return imageContainer; + } + + protected Request makeImageRequest( + String requestUrl, + int maxWidth, + int maxHeight, + ScaleType scaleType, + final String cacheKey) { + return new ImageRequest( + requestUrl, + new Listener() { + @Override + public void onResponse(Bitmap response) { + onGetImageSuccess(cacheKey, response); + } + }, + maxWidth, + maxHeight, + scaleType, + Config.RGB_565, + new ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + onGetImageError(cacheKey, error); + } + }); + } + + /** + * Sets the amount of time to wait after the first response arrives before delivering all + * responses. Batching can be disabled entirely by passing in 0. + * + * @param newBatchedResponseDelayMs The time in milliseconds to wait. + */ + public void setBatchedResponseDelay(int newBatchedResponseDelayMs) { + mBatchResponseDelayMs = newBatchedResponseDelayMs; + } + + /** + * Handler for when an image was successfully loaded. + * + * @param cacheKey The cache key that is associated with the image request. + * @param response The bitmap that was returned from the network. + */ + protected void onGetImageSuccess(String cacheKey, Bitmap response) { + // cache the image that was fetched. + mCache.putBitmap(cacheKey, response); + + // remove the request from the list of in-flight requests. + BatchedImageRequest request = mInFlightRequests.remove(cacheKey); + + if (request != null) { + // Update the response bitmap. + request.mResponseBitmap = response; + + // Send the batched response + batchResponse(cacheKey, request); + } + } + + /** + * Handler for when an image failed to load. + * + * @param cacheKey The cache key that is associated with the image request. + */ + protected void onGetImageError(String cacheKey, VolleyError error) { + // Notify the requesters that something failed via a null result. + // Remove this request from the list of in-flight requests. + BatchedImageRequest request = mInFlightRequests.remove(cacheKey); + + if (request != null) { + // Set the error for this request + request.setError(error); + + // Send the batched response + batchResponse(cacheKey, request); + } + } + + /** Container object for all of the data surrounding an image request. */ + public class ImageContainer { + /** + * The most relevant bitmap for the container. If the image was in cache, the Holder to use + * for the final bitmap (the one that pairs to the requested URL). + */ + private Bitmap mBitmap; + + private final ImageListener mListener; + + /** The cache key that was associated with the request */ + private final String mCacheKey; + + /** The request URL that was specified */ + private final String mRequestUrl; + + /** + * Constructs a BitmapContainer object. + * + * @param bitmap The final bitmap (if it exists). + * @param requestUrl The requested URL for this container. + * @param cacheKey The cache key that identifies the requested URL for this container. + */ + public ImageContainer( + Bitmap bitmap, String requestUrl, String cacheKey, ImageListener listener) { + mBitmap = bitmap; + mRequestUrl = requestUrl; + mCacheKey = cacheKey; + mListener = listener; + } + + /** + * Releases interest in the in-flight request (and cancels it if no one else is listening). + * + *

Must be called from the main thread. + */ + @MainThread + public void cancelRequest() { + Threads.throwIfNotOnMainThread(); + + if (mListener == null) { + return; + } + + BatchedImageRequest request = mInFlightRequests.get(mCacheKey); + if (request != null) { + boolean canceled = request.removeContainerAndCancelIfNecessary(this); + if (canceled) { + mInFlightRequests.remove(mCacheKey); + } + } else { + // check to see if it is already batched for delivery. + request = mBatchedResponses.get(mCacheKey); + if (request != null) { + request.removeContainerAndCancelIfNecessary(this); + if (request.mContainers.size() == 0) { + mBatchedResponses.remove(mCacheKey); + } + } + } + } + + /** + * Returns the bitmap associated with the request URL if it has been loaded, null otherwise. + */ + public Bitmap getBitmap() { + return mBitmap; + } + + /** Returns the requested URL for this container. */ + public String getRequestUrl() { + return mRequestUrl; + } + } + + /** + * Wrapper class used to map a Request to the set of active ImageContainer objects that are + * interested in its results. + */ + private static class BatchedImageRequest { + /** The request being tracked */ + private final Request mRequest; + + /** The result of the request being tracked by this item */ + private Bitmap mResponseBitmap; + + /** Error if one occurred for this response */ + private VolleyError mError; + + /** List of all of the active ImageContainers that are interested in the request */ + private final List mContainers = new ArrayList<>(); + + /** + * Constructs a new BatchedImageRequest object + * + * @param request The request being tracked + * @param container The ImageContainer of the person who initiated the request. + */ + public BatchedImageRequest(Request request, ImageContainer container) { + mRequest = request; + mContainers.add(container); + } + + /** Set the error for this response */ + public void setError(VolleyError error) { + mError = error; + } + + /** Get the error for this response */ + public VolleyError getError() { + return mError; + } + + /** + * Adds another ImageContainer to the list of those interested in the results of the + * request. + */ + public void addContainer(ImageContainer container) { + mContainers.add(container); + } + + /** + * Detaches the bitmap container from the request and cancels the request if no one is left + * listening. + * + * @param container The container to remove from the list + * @return True if the request was canceled, false otherwise. + */ + public boolean removeContainerAndCancelIfNecessary(ImageContainer container) { + mContainers.remove(container); + if (mContainers.size() == 0) { + mRequest.cancel(); + return true; + } + return false; + } + } + + /** + * Starts the runnable for batched delivery of responses if it is not already started. + * + * @param cacheKey The cacheKey of the response being delivered. + * @param request The BatchedImageRequest to be delivered. + */ + private void batchResponse(String cacheKey, BatchedImageRequest request) { + mBatchedResponses.put(cacheKey, request); + // If we don't already have a batch delivery runnable in flight, make a new one. + // Note that this will be used to deliver responses to all callers in mBatchedResponses. + if (mRunnable == null) { + mRunnable = + new Runnable() { + @Override + public void run() { + for (BatchedImageRequest bir : mBatchedResponses.values()) { + for (ImageContainer container : bir.mContainers) { + // If one of the callers in the batched request canceled the + // request + // after the response was received but before it was delivered, + // skip them. + if (container.mListener == null) { + continue; + } + if (bir.getError() == null) { + container.mBitmap = bir.mResponseBitmap; + container.mListener.onResponse(container, false); + } else { + container.mListener.onErrorResponse(bir.getError()); + } + } + } + mBatchedResponses.clear(); + mRunnable = null; + } + }; + // Post the runnable. + mHandler.postDelayed(mRunnable, mBatchResponseDelayMs); + } + } + + /** + * Creates a cache key for use with the L1 cache. + * + * @param url The URL of the request. + * @param maxWidth The max-width of the output. + * @param maxHeight The max-height of the output. + * @param scaleType The scaleType of the imageView. + */ + private static String getCacheKey( + String url, int maxWidth, int maxHeight, ScaleType scaleType) { + return new StringBuilder(url.length() + 12) + .append("#W") + .append(maxWidth) + .append("#H") + .append(maxHeight) + .append("#S") + .append(scaleType.ordinal()) + .append(url) + .toString(); + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/ImageRequest.java b/core/src/main/java/com/android/volley/toolbox/ImageRequest.java new file mode 100644 index 0000000..32b5aa3 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/ImageRequest.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.widget.ImageView.ScaleType; +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.android.volley.DefaultRetryPolicy; +import com.android.volley.NetworkResponse; +import com.android.volley.ParseError; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.VolleyLog; + +/** A canned request for getting an image at a given URL and calling back with a decoded Bitmap. */ +public class ImageRequest extends Request { + /** Socket timeout in milliseconds for image requests */ + public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000; + + /** Default number of retries for image requests */ + public static final int DEFAULT_IMAGE_MAX_RETRIES = 2; + + /** Default backoff multiplier for image requests */ + public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f; + + /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */ + private final Object mLock = new Object(); + + @GuardedBy("mLock") + @Nullable + private Response.Listener mListener; + + private final Config mDecodeConfig; + private final int mMaxWidth; + private final int mMaxHeight; + private final ScaleType mScaleType; + + /** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */ + private static final Object sDecodeLock = new Object(); + + /** + * Creates a new image request, decoding to a maximum specified width and height. If both width + * and height are zero, the image will be decoded to its natural size. If one of the two is + * nonzero, that dimension will be clamped and the other one will be set to preserve the image's + * aspect ratio. If both width and height are nonzero, the image will be decoded to be fit in + * the rectangle of dimensions width x height while keeping its aspect ratio. + * + * @param url URL of the image + * @param listener Listener to receive the decoded bitmap + * @param maxWidth Maximum width to decode this bitmap to, or zero for none + * @param maxHeight Maximum height to decode this bitmap to, or zero for none + * @param scaleType The ImageViews ScaleType used to calculate the needed image size. + * @param decodeConfig Format to decode the bitmap to + * @param errorListener Error listener, or null to ignore errors + */ + public ImageRequest( + String url, + Response.Listener listener, + int maxWidth, + int maxHeight, + ScaleType scaleType, + Config decodeConfig, + @Nullable Response.ErrorListener errorListener) { + super(Method.GET, url, errorListener); + setRetryPolicy( + new DefaultRetryPolicy( + DEFAULT_IMAGE_TIMEOUT_MS, + DEFAULT_IMAGE_MAX_RETRIES, + DEFAULT_IMAGE_BACKOFF_MULT)); + mListener = listener; + mDecodeConfig = decodeConfig; + mMaxWidth = maxWidth; + mMaxHeight = maxHeight; + mScaleType = scaleType; + } + + /** + * For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to the + * normal constructor with {@code ScaleType.CENTER_INSIDE}. + */ + @Deprecated + public ImageRequest( + String url, + Response.Listener listener, + int maxWidth, + int maxHeight, + Config decodeConfig, + Response.ErrorListener errorListener) { + this( + url, + listener, + maxWidth, + maxHeight, + ScaleType.CENTER_INSIDE, + decodeConfig, + errorListener); + } + + @Override + public Priority getPriority() { + return Priority.LOW; + } + + /** + * Scales one side of a rectangle to fit aspect ratio. + * + * @param maxPrimary Maximum size of the primary dimension (i.e. width for max width), or zero + * to maintain aspect ratio with secondary dimension + * @param maxSecondary Maximum size of the secondary dimension, or zero to maintain aspect ratio + * with primary dimension + * @param actualPrimary Actual size of the primary dimension + * @param actualSecondary Actual size of the secondary dimension + * @param scaleType The ScaleType used to calculate the needed image size. + */ + private static int getResizedDimension( + int maxPrimary, + int maxSecondary, + int actualPrimary, + int actualSecondary, + ScaleType scaleType) { + + // If no dominant value at all, just return the actual. + if ((maxPrimary == 0) && (maxSecondary == 0)) { + return actualPrimary; + } + + // If ScaleType.FIT_XY fill the whole rectangle, ignore ratio. + if (scaleType == ScaleType.FIT_XY) { + if (maxPrimary == 0) { + return actualPrimary; + } + return maxPrimary; + } + + // If primary is unspecified, scale primary to match secondary's scaling ratio. + if (maxPrimary == 0) { + double ratio = (double) maxSecondary / (double) actualSecondary; + return (int) (actualPrimary * ratio); + } + + if (maxSecondary == 0) { + return maxPrimary; + } + + double ratio = (double) actualSecondary / (double) actualPrimary; + int resized = maxPrimary; + + // If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio. + if (scaleType == ScaleType.CENTER_CROP) { + if ((resized * ratio) < maxSecondary) { + resized = (int) (maxSecondary / ratio); + } + return resized; + } + + if ((resized * ratio) > maxSecondary) { + resized = (int) (maxSecondary / ratio); + } + return resized; + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + // Serialize all decode on a global lock to reduce concurrent heap usage. + synchronized (sDecodeLock) { + try { + return doParse(response); + } catch (OutOfMemoryError e) { + VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl()); + return Response.error(new ParseError(e)); + } + } + } + + /** The real guts of parseNetworkResponse. Broken out for readability. */ + private Response doParse(NetworkResponse response) { + byte[] data = response.data; + BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); + Bitmap bitmap = null; + if (mMaxWidth == 0 && mMaxHeight == 0) { + decodeOptions.inPreferredConfig = mDecodeConfig; + bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); + } else { + // If we have to resize this image, first get the natural bounds. + decodeOptions.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); + int actualWidth = decodeOptions.outWidth; + int actualHeight = decodeOptions.outHeight; + + // Then compute the dimensions we would ideally like to decode to. + int desiredWidth = + getResizedDimension( + mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType); + int desiredHeight = + getResizedDimension( + mMaxHeight, mMaxWidth, actualHeight, actualWidth, mScaleType); + + // Decode to the nearest power of two scaling factor. + decodeOptions.inJustDecodeBounds = false; + // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it? + // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED; + decodeOptions.inSampleSize = + findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); + Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); + + // If necessary, scale down to the maximal acceptable size. + if (tempBitmap != null + && (tempBitmap.getWidth() > desiredWidth + || tempBitmap.getHeight() > desiredHeight)) { + bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true); + tempBitmap.recycle(); + } else { + bitmap = tempBitmap; + } + } + + if (bitmap == null) { + return Response.error(new ParseError(response)); + } else { + return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); + } + } + + @Override + public void cancel() { + super.cancel(); + synchronized (mLock) { + mListener = null; + } + } + + @Override + protected void deliverResponse(Bitmap response) { + Response.Listener listener; + synchronized (mLock) { + listener = mListener; + } + if (listener != null) { + listener.onResponse(response); + } + } + + /** + * Returns the largest power-of-two divisor for use in downscaling a bitmap that will not result + * in the scaling past the desired dimensions. + * + * @param actualWidth Actual width of the bitmap + * @param actualHeight Actual height of the bitmap + * @param desiredWidth Desired width of the bitmap + * @param desiredHeight Desired height of the bitmap + */ + @VisibleForTesting + static int findBestSampleSize( + int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { + double wr = (double) actualWidth / desiredWidth; + double hr = (double) actualHeight / desiredHeight; + double ratio = Math.min(wr, hr); + float n = 1.0f; + while ((n * 2) <= ratio) { + n *= 2; + } + + return (int) n; + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java b/core/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java new file mode 100644 index 0000000..9f56746 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import androidx.annotation.Nullable; +import com.android.volley.NetworkResponse; +import com.android.volley.ParseError; +import com.android.volley.Response; +import com.android.volley.Response.ErrorListener; +import com.android.volley.Response.Listener; +import java.io.UnsupportedEncodingException; +import org.json.JSONArray; +import org.json.JSONException; + +/** + * A request for retrieving a {@link JSONArray} response body at a given URL, allowing for an + * optional {@link JSONArray} to be passed in as part of the request body. + */ +public class JsonArrayRequest extends JsonRequest { + + /** + * Creates a new request. + * + * @param url URL to fetch the JSON from + * @param listener Listener to receive the JSON response + * @param errorListener Error listener, or null to ignore errors. + */ + public JsonArrayRequest( + String url, Listener listener, @Nullable ErrorListener errorListener) { + super(Method.GET, url, null, listener, errorListener); + } + + /** + * Creates a new request. + * + * @param method the HTTP method to use + * @param url URL to fetch the JSON from + * @param jsonRequest A {@link JSONArray} to post with the request. Null indicates no parameters + * will be posted along with request. + * @param listener Listener to receive the JSON response + * @param errorListener Error listener, or null to ignore errors. + */ + public JsonArrayRequest( + int method, + String url, + @Nullable JSONArray jsonRequest, + Listener listener, + @Nullable ErrorListener errorListener) { + super( + method, + url, + jsonRequest != null ? jsonRequest.toString() : null, + listener, + errorListener); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + try { + String jsonString = + new String( + response.data, + HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET)); + return Response.success( + new JSONArray(jsonString), HttpHeaderParser.parseCacheHeaders(response)); + } catch (UnsupportedEncodingException e) { + return Response.error(new ParseError(e)); + } catch (JSONException je) { + return Response.error(new ParseError(je)); + } + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java b/core/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java new file mode 100644 index 0000000..eccb54b --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import androidx.annotation.Nullable; +import com.android.volley.NetworkResponse; +import com.android.volley.ParseError; +import com.android.volley.Response; +import com.android.volley.Response.ErrorListener; +import com.android.volley.Response.Listener; +import java.io.UnsupportedEncodingException; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * A request for retrieving a {@link JSONObject} response body at a given URL, allowing for an + * optional {@link JSONObject} to be passed in as part of the request body. + */ +public class JsonObjectRequest extends JsonRequest { + + /** + * Creates a new request. + * + * @param url URL to fetch the JSON from + * @param listener Listener to receive the JSON response + * @param errorListener Error listener, or null to ignore errors. + */ + public JsonObjectRequest( + String url, Listener listener, @Nullable ErrorListener errorListener) { + super(Method.GET, url, null, listener, errorListener); + } + + /** + * Constructor which defaults to GET if jsonRequest is null + * , POST otherwise. + * + * @deprecated Use {@link #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener)}. + */ + @Deprecated + public JsonObjectRequest( + String url, + @Nullable JSONObject jsonRequest, + Listener listener, + @Nullable ErrorListener errorListener) { + super( + jsonRequest == null ? Method.GET : Method.POST, + url, + jsonRequest != null ? jsonRequest.toString() : null, + listener, + errorListener); + } + + /** + * Creates a new request. + * + * @param method the HTTP method to use + * @param url URL to fetch the JSON from + * @param jsonRequest A {@link JSONObject} to post with the request. Null indicates no + * parameters will be posted along with request. + * @param listener Listener to receive the JSON response + * @param errorListener Error listener, or null to ignore errors. + */ + public JsonObjectRequest( + int method, + String url, + @Nullable JSONObject jsonRequest, + Listener listener, + @Nullable ErrorListener errorListener) { + super( + method, + url, + jsonRequest != null ? jsonRequest.toString() : null, + listener, + errorListener); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + try { + String jsonString = + new String( + response.data, + HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET)); + return Response.success( + new JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response)); + } catch (UnsupportedEncodingException e) { + return Response.error(new ParseError(e)); + } catch (JSONException je) { + return Response.error(new ParseError(je)); + } + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/JsonRequest.java b/core/src/main/java/com/android/volley/toolbox/JsonRequest.java new file mode 100644 index 0000000..c2d1fad --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/JsonRequest.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.Response.ErrorListener; +import com.android.volley.Response.Listener; +import com.android.volley.VolleyLog; +import java.io.UnsupportedEncodingException; + +/** + * A request for retrieving a T type response body at a given URL that also optionally sends along a + * JSON body in the request specified. + * + * @param JSON type of response expected + */ +public abstract class JsonRequest extends Request { + /** Default charset for JSON request. */ + protected static final String PROTOCOL_CHARSET = "utf-8"; + + /** Content type for request. */ + private static final String PROTOCOL_CONTENT_TYPE = + String.format("application/json; charset=%s", PROTOCOL_CHARSET); + + /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */ + private final Object mLock = new Object(); + + @Nullable + @GuardedBy("mLock") + private Listener mListener; + + @Nullable private final String mRequestBody; + + /** + * Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()} + * or {@link #getPostParams()} is overridden (which defaults to POST). + * + * @deprecated Use {@link #JsonRequest(int, String, String, Listener, ErrorListener)}. + */ + @Deprecated + public JsonRequest( + String url, String requestBody, Listener listener, ErrorListener errorListener) { + this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener); + } + + /** + * Creates a new request. + * + * @param method the HTTP method to use + * @param url URL to fetch the JSON from + * @param requestBody The content to post as the body of the request. Null indicates no + * parameters will be posted along with request. + * @param listener Listener to receive the JSON response + * @param errorListener Error listener, or null to ignore errors. + */ + public JsonRequest( + int method, + String url, + @Nullable String requestBody, + Listener listener, + @Nullable ErrorListener errorListener) { + super(method, url, errorListener); + mListener = listener; + mRequestBody = requestBody; + } + + @Override + public void cancel() { + super.cancel(); + synchronized (mLock) { + mListener = null; + } + } + + @Override + protected void deliverResponse(T response) { + Response.Listener listener; + synchronized (mLock) { + listener = mListener; + } + if (listener != null) { + listener.onResponse(response); + } + } + + @Override + protected abstract Response parseNetworkResponse(NetworkResponse response); + + /** @deprecated Use {@link #getBodyContentType()}. */ + @Deprecated + @Override + public String getPostBodyContentType() { + return getBodyContentType(); + } + + /** @deprecated Use {@link #getBody()}. */ + @Deprecated + @Override + public byte[] getPostBody() { + return getBody(); + } + + @Override + public String getBodyContentType() { + return PROTOCOL_CONTENT_TYPE; + } + + @Override + public byte[] getBody() { + try { + return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET); + } catch (UnsupportedEncodingException uee) { + VolleyLog.wtf( + "Unsupported Encoding while trying to get the bytes of %s using %s", + mRequestBody, PROTOCOL_CHARSET); + return null; + } + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/NetworkImageView.java b/core/src/main/java/com/android/volley/toolbox/NetworkImageView.java new file mode 100644 index 0000000..a24b3e2 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/NetworkImageView.java @@ -0,0 +1,332 @@ +/** + * Copyright (C) 2013 The Android Open Source Project + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.volley.toolbox; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.ViewGroup.LayoutParams; +import android.widget.ImageView; +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.ImageLoader.ImageContainer; +import com.android.volley.toolbox.ImageLoader.ImageListener; + +/** Handles fetching an image from a URL as well as the life-cycle of the associated request. */ +public class NetworkImageView extends ImageView { + /** The URL of the network image to load */ + private String mUrl; + + /** + * Resource ID of the image to be used as a placeholder until the network image is loaded. Won't + * be set at the same time as mDefaultImageDrawable or mDefaultImageBitmap. + */ + private int mDefaultImageId; + + /** + * Drawable of the image to be used as a placeholder until the network image is loaded. Won't be + * set at the same time as mDefaultImageId or mDefaultImageBitmap. + */ + @Nullable private Drawable mDefaultImageDrawable; + + /** + * Bitmap of the image to be used as a placeholder until the network image is loaded. Won't be + * set at the same time as mDefaultImageId or mDefaultImageDrawable. + */ + @Nullable private Bitmap mDefaultImageBitmap; + + /** + * Resource ID of the image to be used if the network response fails. Won't be set at the same + * time as mErrorImageDrawable or mErrorImageBitmap. + */ + private int mErrorImageId; + + /** + * Bitmap of the image to be used if the network response fails. Won't be set at the same time + * as mErrorImageId or mErrorImageBitmap. + */ + @Nullable private Drawable mErrorImageDrawable; + + /** + * Bitmap of the image to be used if the network response fails. Won't be set at the same time + * as mErrorImageId or mErrorImageDrawable. + */ + @Nullable private Bitmap mErrorImageBitmap; + + /** Local copy of the ImageLoader. */ + private ImageLoader mImageLoader; + + /** Current ImageContainer. (either in-flight or finished) */ + private ImageContainer mImageContainer; + + public NetworkImageView(Context context) { + this(context, null); + } + + public NetworkImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NetworkImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Sets URL of the image that should be loaded into this view. Note that calling this will + * immediately either set the cached image (if available) or the default image specified by + * {@link NetworkImageView#setDefaultImageResId(int)} on the view. + * + *

NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} or {@link + * NetworkImageView#setDefaultImageBitmap} and {@link NetworkImageView#setErrorImageResId(int)} + * or {@link NetworkImageView#setErrorImageBitmap(Bitmap)} should be called prior to calling + * this function. + * + *

Must be called from the main thread. + * + * @param url The URL that should be loaded into this ImageView. + * @param imageLoader ImageLoader that will be used to make the request. + */ + @MainThread + public void setImageUrl(String url, ImageLoader imageLoader) { + Threads.throwIfNotOnMainThread(); + mUrl = url; + mImageLoader = imageLoader; + // The URL has potentially changed. See if we need to load it. + loadImageIfNecessary(/* isInLayoutPass= */ false); + } + + /** + * Sets the default image resource ID to be used for this view until the attempt to load it + * completes. + * + *

This will clear anything set by {@link NetworkImageView#setDefaultImageBitmap} or {@link + * NetworkImageView#setDefaultImageDrawable}. + */ + public void setDefaultImageResId(int defaultImage) { + mDefaultImageBitmap = null; + mDefaultImageDrawable = null; + mDefaultImageId = defaultImage; + } + + /** + * Sets the default image drawable to be used for this view until the attempt to load it + * completes. + * + *

This will clear anything set by {@link NetworkImageView#setDefaultImageResId} or {@link + * NetworkImageView#setDefaultImageBitmap}. + */ + public void setDefaultImageDrawable(@Nullable Drawable defaultImageDrawable) { + mDefaultImageId = 0; + mDefaultImageBitmap = null; + mDefaultImageDrawable = defaultImageDrawable; + } + + /** + * Sets the default image bitmap to be used for this view until the attempt to load it + * completes. + * + *

This will clear anything set by {@link NetworkImageView#setDefaultImageResId} or {@link + * NetworkImageView#setDefaultImageDrawable}. + */ + public void setDefaultImageBitmap(Bitmap defaultImage) { + mDefaultImageId = 0; + mDefaultImageDrawable = null; + mDefaultImageBitmap = defaultImage; + } + + /** + * Sets the error image resource ID to be used for this view in the event that the image + * requested fails to load. + * + *

This will clear anything set by {@link NetworkImageView#setErrorImageBitmap} or {@link + * NetworkImageView#setErrorImageDrawable}. + */ + public void setErrorImageResId(int errorImage) { + mErrorImageBitmap = null; + mErrorImageDrawable = null; + mErrorImageId = errorImage; + } + + /** + * Sets the error image drawable to be used for this view in the event that the image requested + * fails to load. + * + *

This will clear anything set by {@link NetworkImageView#setErrorImageResId} or {@link + * NetworkImageView#setDefaultImageBitmap}. + */ + public void setErrorImageDrawable(@Nullable Drawable errorImageDrawable) { + mErrorImageId = 0; + mErrorImageBitmap = null; + mErrorImageDrawable = errorImageDrawable; + } + + /** + * Sets the error image bitmap to be used for this view in the event that the image requested + * fails to load. + * + *

This will clear anything set by {@link NetworkImageView#setErrorImageResId} or {@link + * NetworkImageView#setDefaultImageDrawable}. + */ + public void setErrorImageBitmap(Bitmap errorImage) { + mErrorImageId = 0; + mErrorImageDrawable = null; + mErrorImageBitmap = errorImage; + } + + /** + * Loads the image for the view if it isn't already loaded. + * + * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise. + */ + void loadImageIfNecessary(final boolean isInLayoutPass) { + int width = getWidth(); + int height = getHeight(); + ScaleType scaleType = getScaleType(); + + boolean wrapWidth = false, wrapHeight = false; + if (getLayoutParams() != null) { + wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT; + wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT; + } + + // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content + // view, hold off on loading the image. + boolean isFullyWrapContent = wrapWidth && wrapHeight; + if (width == 0 && height == 0 && !isFullyWrapContent) { + return; + } + + // if the URL to be loaded in this view is empty, cancel any old requests and clear the + // currently loaded image. + if (TextUtils.isEmpty(mUrl)) { + if (mImageContainer != null) { + mImageContainer.cancelRequest(); + mImageContainer = null; + } + setDefaultImageOrNull(); + return; + } + + // if there was an old request in this view, check if it needs to be canceled. + if (mImageContainer != null && mImageContainer.getRequestUrl() != null) { + if (mImageContainer.getRequestUrl().equals(mUrl)) { + // if the request is from the same URL, return. + return; + } else { + // if there is a pre-existing request, cancel it if it's fetching a different URL. + mImageContainer.cancelRequest(); + setDefaultImageOrNull(); + } + } + + // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens. + int maxWidth = wrapWidth ? 0 : width; + int maxHeight = wrapHeight ? 0 : height; + + // The pre-existing content of this view didn't match the current URL. Load the new image + // from the network. + + // update the ImageContainer to be the new bitmap container. + mImageContainer = + mImageLoader.get( + mUrl, + new ImageListener() { + @Override + public void onErrorResponse(VolleyError error) { + if (mErrorImageId != 0) { + setImageResource(mErrorImageId); + } else if (mErrorImageDrawable != null) { + setImageDrawable(mErrorImageDrawable); + } else if (mErrorImageBitmap != null) { + setImageBitmap(mErrorImageBitmap); + } + } + + @Override + public void onResponse( + final ImageContainer response, boolean isImmediate) { + // If this was an immediate response that was delivered inside of a + // layout + // pass do not set the image immediately as it will trigger a + // requestLayout + // inside of a layout. Instead, defer setting the image by posting + // back to + // the main thread. + if (isImmediate && isInLayoutPass) { + post( + new Runnable() { + @Override + public void run() { + onResponse(response, /* isImmediate= */ false); + } + }); + return; + } + + if (response.getBitmap() != null) { + setImageBitmap(response.getBitmap()); + } else if (mDefaultImageId != 0) { + setImageResource(mDefaultImageId); + } else if (mDefaultImageDrawable != null) { + setImageDrawable(mDefaultImageDrawable); + } else if (mDefaultImageBitmap != null) { + setImageBitmap(mDefaultImageBitmap); + } + } + }, + maxWidth, + maxHeight, + scaleType); + } + + private void setDefaultImageOrNull() { + if (mDefaultImageId != 0) { + setImageResource(mDefaultImageId); + } else if (mDefaultImageDrawable != null) { + setImageDrawable(mDefaultImageDrawable); + } else if (mDefaultImageBitmap != null) { + setImageBitmap(mDefaultImageBitmap); + } else { + setImageBitmap(null); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + loadImageIfNecessary(/* isInLayoutPass= */ true); + } + + @Override + protected void onDetachedFromWindow() { + if (mImageContainer != null) { + // If the view was bound to an image request, cancel it and clear + // out the image from the view. + mImageContainer.cancelRequest(); + setImageBitmap(null); + // also clear out the container so we can reload the image if necessary. + mImageContainer = null; + } + super.onDetachedFromWindow(); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + invalidate(); + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/NetworkUtility.java b/core/src/main/java/com/android/volley/toolbox/NetworkUtility.java new file mode 100644 index 0000000..58a3bb3 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/NetworkUtility.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.os.SystemClock; +import androidx.annotation.Nullable; +import com.android.volley.AuthFailureError; +import com.android.volley.Cache; +import com.android.volley.ClientError; +import com.android.volley.Header; +import com.android.volley.NetworkError; +import com.android.volley.NetworkResponse; +import com.android.volley.NoConnectionError; +import com.android.volley.Request; +import com.android.volley.RetryPolicy; +import com.android.volley.ServerError; +import com.android.volley.TimeoutError; +import com.android.volley.VolleyError; +import com.android.volley.VolleyLog; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.util.List; + +/** + * Utility class for methods that are shared between {@link BasicNetwork} and {@link + * BasicAsyncNetwork} + */ +final class NetworkUtility { + private static final int SLOW_REQUEST_THRESHOLD_MS = 3000; + + private NetworkUtility() {} + + /** Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. */ + static void logSlowRequests( + long requestLifetime, Request request, byte[] responseContents, int statusCode) { + if (VolleyLog.DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) { + VolleyLog.d( + "HTTP response for request=<%s> [lifetime=%d], [size=%s], " + + "[rc=%d], [retryCount=%s]", + request, + requestLifetime, + responseContents != null ? responseContents.length : "null", + statusCode, + request.getRetryPolicy().getCurrentRetryCount()); + } + } + + static NetworkResponse getNotModifiedNetworkResponse( + Request request, long requestDuration, List

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
combinedHeaders = HttpHeaderParser.combineHeaders(responseHeaders, entry); + return new NetworkResponse( + HttpURLConnection.HTTP_NOT_MODIFIED, + entry.data, + /* notModified= */ true, + requestDuration, + combinedHeaders); + } + + /** Reads the contents of an InputStream into a byte[]. */ + static byte[] inputStreamToBytes(InputStream in, int contentLength, ByteArrayPool pool) + throws IOException { + PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(pool, contentLength); + byte[] buffer = null; + try { + buffer = pool.getBuf(1024); + int count; + while ((count = in.read(buffer)) != -1) { + bytes.write(buffer, 0, count); + } + return bytes.toByteArray(); + } finally { + try { + // Close the InputStream and release the resources by "consuming the content". + if (in != null) { + in.close(); + } + } catch (IOException e) { + // This can happen if there was an exception above that left the stream in + // an invalid state. + VolleyLog.v("Error occurred when closing InputStream"); + } + pool.returnBuf(buffer); + bytes.close(); + } + } + + /** + * Attempts to prepare the request for a retry. If there are no more attempts remaining in the + * request's retry policy, the provided exception is thrown. + * + *

Must be invoked from a background thread, as client implementations of RetryPolicy#retry + * may make blocking calls. + * + * @param request The request to use. + */ + static void attemptRetryOnException(final Request request, final RetryInfo retryInfo) + throws VolleyError { + final RetryPolicy retryPolicy = request.getRetryPolicy(); + final int oldTimeout = request.getTimeoutMs(); + try { + retryPolicy.retry(retryInfo.errorToRetry); + } catch (VolleyError e) { + request.addMarker( + String.format( + "%s-timeout-giveup [timeout=%s]", retryInfo.logPrefix, oldTimeout)); + throw e; + } + request.addMarker(String.format("%s-retry [timeout=%s]", retryInfo.logPrefix, oldTimeout)); + } + + static class RetryInfo { + private final String logPrefix; + private final VolleyError errorToRetry; + + private RetryInfo(String logPrefix, VolleyError errorToRetry) { + this.logPrefix = logPrefix; + this.errorToRetry = errorToRetry; + } + } + + /** + * Based on the exception thrown, decides whether to attempt to retry, or to throw the error. + * + *

If this method returns without throwing, {@link #attemptRetryOnException} should be called + * with the provided {@link RetryInfo} to consult the client's retry policy. + */ + static RetryInfo shouldRetryException( + Request request, + IOException exception, + long requestStartMs, + @Nullable HttpResponse httpResponse, + @Nullable byte[] responseContents) + throws VolleyError { + if (exception instanceof SocketTimeoutException) { + return new RetryInfo("socket", new TimeoutError()); + } else if (exception instanceof MalformedURLException) { + throw new RuntimeException("Bad URL " + request.getUrl(), exception); + } else { + int statusCode; + if (httpResponse != null) { + statusCode = httpResponse.getStatusCode(); + } else { + if (request.shouldRetryConnectionErrors()) { + return new RetryInfo("connection", new NoConnectionError()); + } + throw new NoConnectionError(exception); + } + VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); + NetworkResponse networkResponse; + if (responseContents != null) { + List

responseHeaders; + responseHeaders = httpResponse.getHeaders(); + networkResponse = + new NetworkResponse( + statusCode, + responseContents, + /* notModified= */ false, + SystemClock.elapsedRealtime() - requestStartMs, + responseHeaders); + if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED + || statusCode == HttpURLConnection.HTTP_FORBIDDEN) { + return new RetryInfo("auth", new AuthFailureError(networkResponse)); + } + if (statusCode >= 400 && statusCode <= 499) { + // Don't retry other client errors. + throw new ClientError(networkResponse); + } + if (statusCode >= 500 && statusCode <= 599) { + if (request.shouldRetryServerErrors()) { + return new RetryInfo("server", new ServerError(networkResponse)); + } + } + // Server error and client has opted out of retries, or 3xx. No reason to retry. + throw new ServerError(networkResponse); + } + return new RetryInfo("network", new NetworkError()); + } + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/NoAsyncCache.java b/core/src/main/java/com/android/volley/toolbox/NoAsyncCache.java new file mode 100644 index 0000000..1fda58f --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/NoAsyncCache.java @@ -0,0 +1,42 @@ +package com.android.volley.toolbox; + +import com.android.volley.AsyncCache; +import com.android.volley.Cache; + +/** + * An AsyncCache that doesn't cache anything. + * + *

WARNING: This API is experimental and subject to breaking changes. Please see + * https://github.com/google/volley/wiki/Asynchronous-Volley for more details. + */ +public class NoAsyncCache extends AsyncCache { + @Override + public void get(String key, OnGetCompleteCallback callback) { + callback.onGetComplete(null); + } + + @Override + public void put(String key, Cache.Entry entry, OnWriteCompleteCallback callback) { + callback.onWriteComplete(); + } + + @Override + public void clear(OnWriteCompleteCallback callback) { + callback.onWriteComplete(); + } + + @Override + public void initialize(OnWriteCompleteCallback callback) { + callback.onWriteComplete(); + } + + @Override + public void invalidate(String key, boolean fullExpire, OnWriteCompleteCallback callback) { + callback.onWriteComplete(); + } + + @Override + public void remove(String key, OnWriteCompleteCallback callback) { + callback.onWriteComplete(); + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/NoCache.java b/core/src/main/java/com/android/volley/toolbox/NoCache.java new file mode 100644 index 0000000..51f9945 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/NoCache.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Cache; + +/** A cache that doesn't. */ +public class NoCache implements Cache { + @Override + public void clear() {} + + @Override + public Entry get(String key) { + return null; + } + + @Override + public void put(String key, Entry entry) {} + + @Override + public void invalidate(String key, boolean fullExpire) {} + + @Override + public void remove(String key) {} + + @Override + public void initialize() {} +} diff --git a/core/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java b/core/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java new file mode 100644 index 0000000..bdcc45e --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * A variation of {@link java.io.ByteArrayOutputStream} that uses a pool of byte[] buffers instead + * of always allocating them fresh, saving on heap churn. + */ +public class PoolingByteArrayOutputStream extends ByteArrayOutputStream { + /** + * If the {@link #PoolingByteArrayOutputStream(ByteArrayPool)} constructor is called, this is + * the default size to which the underlying byte array is initialized. + */ + private static final int DEFAULT_SIZE = 256; + + private final ByteArrayPool mPool; + + /** + * Constructs a new PoolingByteArrayOutputStream with a default size. If more bytes are written + * to this instance, the underlying byte array will expand. + */ + public PoolingByteArrayOutputStream(ByteArrayPool pool) { + this(pool, DEFAULT_SIZE); + } + + /** + * Constructs a new {@code ByteArrayOutputStream} with a default size of {@code size} bytes. If + * more than {@code size} bytes are written to this instance, the underlying byte array will + * expand. + * + * @param size initial size for the underlying byte array. The value will be pinned to a default + * minimum size. + */ + public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) { + mPool = pool; + buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE)); + } + + @Override + public void close() throws IOException { + mPool.returnBuf(buf); + buf = null; + super.close(); + } + + @Override + public void finalize() { + mPool.returnBuf(buf); + } + + /** Ensures there is enough space in the buffer for the given number of additional bytes. */ + @SuppressWarnings("UnsafeFinalization") + private void expand(int i) { + /* Can the buffer handle @i more bytes, if not expand it */ + if (count + i <= buf.length) { + return; + } + byte[] newbuf = mPool.getBuf((count + i) * 2); + System.arraycopy(buf, 0, newbuf, 0, count); + mPool.returnBuf(buf); + buf = newbuf; + } + + @Override + public synchronized void write(byte[] buffer, int offset, int len) { + expand(len); + super.write(buffer, offset, len); + } + + @Override + public synchronized void write(int oneByte) { + expand(1); + super.write(oneByte); + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/RequestFuture.java b/core/src/main/java/com/android/volley/toolbox/RequestFuture.java new file mode 100644 index 0000000..f9cbce2 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/RequestFuture.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.os.SystemClock; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * A Future that represents a Volley request. + * + *

Used by providing as your response and error listeners. For example: + * + *

+ * RequestFuture<JSONObject> future = RequestFuture.newFuture();
+ * MyRequest request = new MyRequest(URL, future, future);
+ *
+ * // If you want to be able to cancel the request:
+ * future.setRequest(requestQueue.add(request));
+ *
+ * // Otherwise:
+ * requestQueue.add(request);
+ *
+ * try {
+ *   JSONObject response = future.get();
+ *   // do something with response
+ * } catch (InterruptedException e) {
+ *   // handle the error
+ * } catch (ExecutionException e) {
+ *   // handle the error
+ * }
+ * 
+ * + * @param The type of parsed response this future expects. + */ +public class RequestFuture implements Future, Response.Listener, Response.ErrorListener { + private Request mRequest; + private boolean mResultReceived = false; + private T mResult; + private VolleyError mException; + + public static RequestFuture newFuture() { + return new RequestFuture<>(); + } + + private RequestFuture() {} + + public void setRequest(Request request) { + mRequest = request; + } + + @Override + public synchronized boolean cancel(boolean mayInterruptIfRunning) { + if (mRequest == null) { + return false; + } + + if (!isDone()) { + mRequest.cancel(); + return true; + } else { + return false; + } + } + + @Override + public T get() throws InterruptedException, ExecutionException { + try { + return doGet(/* timeoutMs= */ null); + } catch (TimeoutException e) { + throw new AssertionError(e); + } + } + + @Override + public T get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return doGet(TimeUnit.MILLISECONDS.convert(timeout, unit)); + } + + private synchronized T doGet(Long timeoutMs) + throws InterruptedException, ExecutionException, TimeoutException { + if (mException != null) { + throw new ExecutionException(mException); + } + + if (mResultReceived) { + return mResult; + } + + if (timeoutMs == null) { + while (!isDone()) { + wait(0); + } + } else if (timeoutMs > 0) { + long nowMs = SystemClock.uptimeMillis(); + long deadlineMs = nowMs + timeoutMs; + while (!isDone() && nowMs < deadlineMs) { + wait(deadlineMs - nowMs); + nowMs = SystemClock.uptimeMillis(); + } + } + + if (mException != null) { + throw new ExecutionException(mException); + } + + if (!mResultReceived) { + throw new TimeoutException(); + } + + return mResult; + } + + @Override + public boolean isCancelled() { + if (mRequest == null) { + return false; + } + return mRequest.isCanceled(); + } + + @Override + public synchronized boolean isDone() { + return mResultReceived || mException != null || isCancelled(); + } + + @Override + public synchronized void onResponse(T response) { + mResultReceived = true; + mResult = response; + notifyAll(); + } + + @Override + public synchronized void onErrorResponse(VolleyError error) { + mException = error; + notifyAll(); + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/StringRequest.java b/core/src/main/java/com/android/volley/toolbox/StringRequest.java new file mode 100644 index 0000000..df7b386 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/StringRequest.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.Response.ErrorListener; +import com.android.volley.Response.Listener; +import java.io.UnsupportedEncodingException; + +/** A canned request for retrieving the response body at a given URL as a String. */ +public class StringRequest extends Request { + + /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */ + private final Object mLock = new Object(); + + @Nullable + @GuardedBy("mLock") + private Listener mListener; + + /** + * Creates a new request with the given method. + * + * @param method the request {@link Method} to use + * @param url URL to fetch the string at + * @param listener Listener to receive the String response + * @param errorListener Error listener, or null to ignore errors + */ + public StringRequest( + int method, + String url, + Listener listener, + @Nullable ErrorListener errorListener) { + super(method, url, errorListener); + mListener = listener; + } + + /** + * Creates a new GET request. + * + * @param url URL to fetch the string at + * @param listener Listener to receive the String response + * @param errorListener Error listener, or null to ignore errors + */ + public StringRequest( + String url, Listener listener, @Nullable ErrorListener errorListener) { + this(Method.GET, url, listener, errorListener); + } + + @Override + public void cancel() { + super.cancel(); + synchronized (mLock) { + mListener = null; + } + } + + @Override + protected void deliverResponse(String response) { + Response.Listener listener; + synchronized (mLock) { + listener = mListener; + } + if (listener != null) { + listener.onResponse(response); + } + } + + @Override + @SuppressWarnings("DefaultCharset") + protected Response parseNetworkResponse(NetworkResponse response) { + String parsed; + try { + parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); + } catch (UnsupportedEncodingException e) { + // Since minSdkVersion = 8, we can't call + // new String(response.data, Charset.defaultCharset()) + // So suppress the warning instead. + parsed = new String(response.data); + } + return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response)); + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/Threads.java b/core/src/main/java/com/android/volley/toolbox/Threads.java new file mode 100644 index 0000000..66c3e41 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/Threads.java @@ -0,0 +1,13 @@ +package com.android.volley.toolbox; + +import android.os.Looper; + +final class Threads { + private Threads() {} + + static void throwIfNotOnMainThread() { + if (Looper.myLooper() != Looper.getMainLooper()) { + throw new IllegalStateException("Must be invoked from the main thread."); + } + } +} diff --git a/core/src/main/java/com/android/volley/toolbox/UrlRewriter.java b/core/src/main/java/com/android/volley/toolbox/UrlRewriter.java new file mode 100644 index 0000000..8bbb770 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/UrlRewriter.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import androidx.annotation.Nullable; + +/** An interface for transforming URLs before use. */ +public interface UrlRewriter { + /** + * Returns a URL to use instead of the provided one, or null to indicate this URL should not be + * used at all. + */ + @Nullable + String rewriteUrl(String originalUrl); +} diff --git a/core/src/main/java/com/android/volley/toolbox/Volley.java b/core/src/main/java/com/android/volley/toolbox/Volley.java new file mode 100644 index 0000000..6ab34bb --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/Volley.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.http.AndroidHttpClient; +import android.os.Build; +import androidx.annotation.NonNull; +import com.android.volley.Network; +import com.android.volley.RequestQueue; +import java.io.File; + +public class Volley { + + /** Default on-disk cache directory. */ + private static final String DEFAULT_CACHE_DIR = "volley"; + + /** + * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it. + * + * @param context A {@link Context} to use for creating the cache dir. + * @param stack A {@link BaseHttpStack} to use for the network, or null for default. + * @return A started {@link RequestQueue} instance. + */ + @NonNull + public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) { + BasicNetwork network; + if (stack == null) { + if (Build.VERSION.SDK_INT >= 9) { + network = new BasicNetwork(new HurlStack()); + } else { + // Prior to Gingerbread, HttpUrlConnection was unreliable. + // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html + // At some point in the future we'll move our minSdkVersion past Froyo and can + // delete this fallback (along with all Apache HTTP code). + String userAgent = "volley/0"; + try { + String packageName = context.getPackageName(); + PackageInfo info = + context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0); + userAgent = packageName + "/" + info.versionCode; + } catch (NameNotFoundException e) { + } + + network = + new BasicNetwork( + new HttpClientStack(AndroidHttpClient.newInstance(userAgent))); + } + } else { + network = new BasicNetwork(stack); + } + + return newRequestQueue(context, network); + } + + /** + * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it. + * + * @param context A {@link Context} to use for creating the cache dir. + * @param stack An {@link HttpStack} to use for the network, or null for default. + * @return A started {@link RequestQueue} instance. + * @deprecated Use {@link #newRequestQueue(Context, BaseHttpStack)} instead to avoid depending + * on Apache HTTP. This method may be removed in a future release of Volley. + */ + @Deprecated + @SuppressWarnings("deprecation") + @NonNull + public static RequestQueue newRequestQueue(Context context, HttpStack stack) { + if (stack == null) { + return newRequestQueue(context, (BaseHttpStack) null); + } + return newRequestQueue(context, new BasicNetwork(stack)); + } + + @NonNull + private static RequestQueue newRequestQueue(Context context, Network network) { + final Context appContext = context.getApplicationContext(); + // Use a lazy supplier for the cache directory so that newRequestQueue() can be called on + // main thread without causing strict mode violation. + DiskBasedCache.FileSupplier cacheSupplier = + new DiskBasedCache.FileSupplier() { + private File cacheDir = null; + + @Override + public File get() { + if (cacheDir == null) { + cacheDir = new File(appContext.getCacheDir(), DEFAULT_CACHE_DIR); + } + return cacheDir; + } + }; + RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheSupplier), network); + queue.start(); + return queue; + } + + /** + * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it. + * + * @param context A {@link Context} to use for creating the cache dir. + * @return A started {@link RequestQueue} instance. + */ + @NonNull + public static RequestQueue newRequestQueue(Context context) { + return newRequestQueue(context, (BaseHttpStack) null); + } +} diff --git a/core/src/test/java/com/android/volley/AsyncRequestQueueTest.java b/core/src/test/java/com/android/volley/AsyncRequestQueueTest.java new file mode 100644 index 0000000..aef4f01 --- /dev/null +++ b/core/src/test/java/com/android/volley/AsyncRequestQueueTest.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.AsyncCache.OnGetCompleteCallback; +import com.android.volley.AsyncCache.OnWriteCompleteCallback; +import com.android.volley.mock.ShadowSystemClock; +import com.android.volley.toolbox.NoAsyncCache; +import com.android.volley.toolbox.StringRequest; +import com.android.volley.utils.ImmediateResponseDelivery; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Unit tests for AsyncRequestQueue, with all dependencies mocked out */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowSystemClock.class}) +public class AsyncRequestQueueTest { + + @Mock private AsyncNetwork mMockNetwork; + @Mock private ScheduledExecutorService mMockScheduledExecutor; + private final ResponseDelivery mDelivery = new ImmediateResponseDelivery(); + private AsyncRequestQueue queue; + + @Before + public void setUp() throws Exception { + initMocks(this); + queue = createRequestQueue(new NoAsyncCache()); + } + + @Test + public void cancelAll_onlyCorrectTag() throws Exception { + queue.start(); + Object tagA = new Object(); + Object tagB = new Object(); + StringRequest req1 = mock(StringRequest.class); + when(req1.getTag()).thenReturn(tagA); + StringRequest req2 = mock(StringRequest.class); + when(req2.getTag()).thenReturn(tagB); + StringRequest req3 = mock(StringRequest.class); + when(req3.getTag()).thenReturn(tagA); + StringRequest req4 = mock(StringRequest.class); + when(req4.getTag()).thenReturn(tagA); + + queue.add(req1); // A + queue.add(req2); // B + queue.add(req3); // A + queue.cancelAll(tagA); + queue.add(req4); // A + + verify(req1).cancel(); // A cancelled + verify(req3).cancel(); // A cancelled + verify(req2, never()).cancel(); // B not cancelled + verify(req4, never()).cancel(); // A added after cancel not cancelled + queue.stop(); + } + + @Test + public void add_notifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + queue.start(); + queue.addRequestEventListener(listener); + StringRequest req = mock(StringRequest.class); + + queue.add(req); + + verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_QUEUED); + verifyNoMoreInteractions(listener); + queue.stop(); + } + + @Test + public void finish_notifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + queue.start(); + queue.addRequestEventListener(listener); + StringRequest req = mock(StringRequest.class); + + queue.finish(req); + + verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_FINISHED); + verifyNoMoreInteractions(listener); + queue.stop(); + } + + @Test + public void sendRequestEvent_notifiesListener() throws Exception { + StringRequest req = mock(StringRequest.class); + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + queue.start(); + queue.addRequestEventListener(listener); + + queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + + verify(listener) + .onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + verifyNoMoreInteractions(listener); + queue.stop(); + } + + @Test + public void removeRequestEventListener_removesListener() throws Exception { + StringRequest req = mock(StringRequest.class); + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + queue.start(); + queue.addRequestEventListener(listener); + queue.removeRequestEventListener(listener); + + queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + + verifyNoMoreInteractions(listener); + queue.stop(); + } + + @Test + public void requestsQueuedBeforeCacheInitialization_asyncCache() { + // Create a new queue with a mock cache in order to verify the initialization. + AsyncCache mockAsyncCache = mock(AsyncCache.class); + AsyncRequestQueue queue = createRequestQueue(mockAsyncCache); + queue.start(); + + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(OnWriteCompleteCallback.class); + verify(mockAsyncCache).initialize(callbackCaptor.capture()); + + StringRequest req = mock(StringRequest.class); + req.setShouldCache(true); + when(req.getCacheKey()).thenReturn("cache-key"); + queue.add(req); + + // Cache should not be read before initialization completes. + verify(mockAsyncCache, never()).get(anyString(), any(OnGetCompleteCallback.class)); + + callbackCaptor.getValue().onWriteComplete(); + + // Once the write completes, the request should be kicked off (in the form of a cache + // lookup). + verify(mockAsyncCache).get(eq("cache-key"), any(OnGetCompleteCallback.class)); + + queue.stop(); + } + + private AsyncRequestQueue createRequestQueue(AsyncCache asyncCache) { + return new AsyncRequestQueue.Builder(mMockNetwork) + .setResponseDelivery(mDelivery) + .setAsyncCache(asyncCache) + .setExecutorFactory( + new AsyncRequestQueue.ExecutorFactory() { + @Override + public ExecutorService createNonBlockingExecutor( + BlockingQueue taskQueue) { + return MoreExecutors.newDirectExecutorService(); + } + + @Override + public ExecutorService createBlockingExecutor( + BlockingQueue taskQueue) { + return MoreExecutors.newDirectExecutorService(); + } + + @Override + public ScheduledExecutorService createNonBlockingScheduledExecutor() { + return mMockScheduledExecutor; + } + }) + .build(); + } +} diff --git a/core/src/test/java/com/android/volley/CacheDispatcherTest.java b/core/src/test/java/com/android/volley/CacheDispatcherTest.java new file mode 100644 index 0000000..aef6785 --- /dev/null +++ b/core/src/test/java/com/android/volley/CacheDispatcherTest.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.toolbox.StringRequest; +import com.android.volley.utils.CacheTestUtils; +import java.util.concurrent.BlockingQueue; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +@SuppressWarnings("rawtypes") +public class CacheDispatcherTest { + private CacheDispatcher mDispatcher; + private @Mock BlockingQueue> mCacheQueue; + private @Mock BlockingQueue> mNetworkQueue; + private @Mock Cache mCache; + private @Mock ResponseDelivery mDelivery; + private @Mock Network mNetwork; + private StringRequest mRequest; + + @Before + public void setUp() throws Exception { + initMocks(this); + + mRequest = new StringRequest(Request.Method.GET, "http://foo", null, null); + mDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery); + } + + private static class WaitForever implements Answer { + @Override + public Object answer(InvocationOnMock invocationOnMock) throws Throwable { + Thread.sleep(Long.MAX_VALUE); + return null; + } + } + + @Test + public void runStopsOnQuit() throws Exception { + when(mCacheQueue.take()).then(new WaitForever()); + mDispatcher.start(); + mDispatcher.quit(); + mDispatcher.join(1000); + } + + private static void verifyNoResponse(ResponseDelivery delivery) { + verify(delivery, never()).postResponse(any(Request.class), any(Response.class)); + verify(delivery, never()) + .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); + verify(delivery, never()).postError(any(Request.class), any(VolleyError.class)); + } + + // A cancelled request should not be processed at all. + @Test + public void cancelledRequest() throws Exception { + mRequest.cancel(); + mDispatcher.processRequest(mRequest); + verify(mCache, never()).get(anyString()); + verifyNoResponse(mDelivery); + } + + // A cache miss does not post a response and puts the request on the network queue. + @Test + public void cacheMiss() throws Exception { + mDispatcher.processRequest(mRequest); + verifyNoResponse(mDelivery); + verify(mNetworkQueue).put(mRequest); + assertNull(mRequest.getCacheEntry()); + } + + // A non-expired cache hit posts a response and does not queue to the network. + @Test + public void nonExpiredCacheHit() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); + when(mCache.get(anyString())).thenReturn(entry); + mDispatcher.processRequest(mRequest); + verify(mDelivery).postResponse(any(Request.class), any(Response.class)); + verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class)); + } + + // A soft-expired cache hit posts a response and queues to the network. + @Test + public void softExpiredCacheHit() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); + when(mCache.get(anyString())).thenReturn(entry); + mDispatcher.processRequest(mRequest); + + // Soft expiration needs to use the deferred Runnable variant of postResponse, + // so make sure it gets to run. + ArgumentCaptor runnable = ArgumentCaptor.forClass(Runnable.class); + verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture()); + runnable.getValue().run(); + // This way we can verify the behavior of the Runnable as well. + verify(mNetworkQueue).put(mRequest); + assertSame(entry, mRequest.getCacheEntry()); + + verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class)); + } + + // An expired cache hit does not post a response and queues to the network. + @Test + public void expiredCacheHit() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, true, true); + when(mCache.get(anyString())).thenReturn(entry); + mDispatcher.processRequest(mRequest); + verifyNoResponse(mDelivery); + verify(mNetworkQueue).put(mRequest); + assertSame(entry, mRequest.getCacheEntry()); + } + + // An fresh cache hit with parse error, does not post a response and queues to the network. + @Test + public void freshCacheHit_parseError() throws Exception { + Request request = mock(Request.class); + when(request.parseNetworkResponse(any(NetworkResponse.class))) + .thenReturn(Response.error(new ParseError())); + when(request.getCacheKey()).thenReturn("cache/key"); + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); + when(mCache.get(anyString())).thenReturn(entry); + + mDispatcher.processRequest(request); + + verifyNoResponse(mDelivery); + verify(mNetworkQueue).put(request); + assertNull(request.getCacheEntry()); + verify(mCache).invalidate("cache/key", true); + verify(request).addMarker("cache-parsing-failed"); + } + + @Test + public void duplicateCacheMiss() throws Exception { + StringRequest secondRequest = + new StringRequest(Request.Method.GET, "http://foo", null, null); + mRequest.setSequence(1); + secondRequest.setSequence(2); + mDispatcher.processRequest(mRequest); + mDispatcher.processRequest(secondRequest); + verify(mNetworkQueue).put(mRequest); + verifyNoResponse(mDelivery); + } + + @Test + public void tripleCacheMiss_networkErrorOnFirst() throws Exception { + StringRequest secondRequest = + new StringRequest(Request.Method.GET, "http://foo", null, null); + StringRequest thirdRequest = + new StringRequest(Request.Method.GET, "http://foo", null, null); + mRequest.setSequence(1); + secondRequest.setSequence(2); + thirdRequest.setSequence(3); + mDispatcher.processRequest(mRequest); + mDispatcher.processRequest(secondRequest); + mDispatcher.processRequest(thirdRequest); + + verify(mNetworkQueue).put(mRequest); + verifyNoResponse(mDelivery); + + ((Request) mRequest).notifyListenerResponseNotUsable(); + // Second request should now be in network queue. + verify(mNetworkQueue).put(secondRequest); + // Another unusable response, third request should now be added. + ((Request) secondRequest).notifyListenerResponseNotUsable(); + verify(mNetworkQueue).put(thirdRequest); + } + + @Test + public void duplicateSoftExpiredCacheHit_failedRequest() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); + when(mCache.get(anyString())).thenReturn(entry); + + StringRequest secondRequest = + new StringRequest(Request.Method.GET, "http://foo", null, null); + mRequest.setSequence(1); + secondRequest.setSequence(2); + + mDispatcher.processRequest(mRequest); + mDispatcher.processRequest(secondRequest); + + // Soft expiration needs to use the deferred Runnable variant of postResponse, + // so make sure it gets to run. + ArgumentCaptor runnable = ArgumentCaptor.forClass(Runnable.class); + verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture()); + runnable.getValue().run(); + // This way we can verify the behavior of the Runnable as well. + + verify(mNetworkQueue).put(mRequest); + verify(mDelivery) + .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); + + ((Request) mRequest).notifyListenerResponseNotUsable(); + // Second request should now be in network queue. + verify(mNetworkQueue).put(secondRequest); + } + + @Test + public void duplicateSoftExpiredCacheHit_successfulRequest() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); + when(mCache.get(anyString())).thenReturn(entry); + + StringRequest secondRequest = + new StringRequest(Request.Method.GET, "http://foo", null, null); + mRequest.setSequence(1); + secondRequest.setSequence(2); + + mDispatcher.processRequest(mRequest); + mDispatcher.processRequest(secondRequest); + + // Soft expiration needs to use the deferred Runnable variant of postResponse, + // so make sure it gets to run. + ArgumentCaptor runnable = ArgumentCaptor.forClass(Runnable.class); + verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture()); + runnable.getValue().run(); + // This way we can verify the behavior of the Runnable as well. + + verify(mNetworkQueue).put(mRequest); + verify(mDelivery) + .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); + + ((Request) mRequest).notifyListenerResponseReceived(Response.success(null, entry)); + // Second request should have delivered response. + verify(mNetworkQueue, never()).put(secondRequest); + verify(mDelivery) + .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); + } + + @Test + public void processRequestNotifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(mCache, mNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + mRequest.setRequestQueue(queue); + + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); + when(mCache.get(anyString())).thenReturn(entry); + mDispatcher.processRequest(mRequest); + + InOrder inOrder = inOrder(listener); + inOrder.verify(listener) + .onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED); + inOrder.verify(listener) + .onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED); + inOrder.verifyNoMoreInteractions(); + } +} diff --git a/core/src/test/java/com/android/volley/NetworkDispatcherTest.java b/core/src/test/java/com/android/volley/NetworkDispatcherTest.java new file mode 100644 index 0000000..74dfe8a --- /dev/null +++ b/core/src/test/java/com/android/volley/NetworkDispatcherTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.toolbox.NoCache; +import com.android.volley.toolbox.StringRequest; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.BlockingQueue; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class NetworkDispatcherTest { + private NetworkDispatcher mDispatcher; + private @Mock ResponseDelivery mDelivery; + private @Mock BlockingQueue> mNetworkQueue; + private @Mock Network mNetwork; + private @Mock Cache mCache; + private StringRequest mRequest; + + private static final byte[] CANNED_DATA = + "Ceci n'est pas une vraie reponse".getBytes(StandardCharsets.UTF_8); + + @Before + public void setUp() throws Exception { + initMocks(this); + mRequest = new StringRequest(Request.Method.GET, "http://foo", null, null); + mDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery); + } + + @Test + public void successPostsResponse() throws Exception { + when(mNetwork.performRequest(any(Request.class))) + .thenReturn(new NetworkResponse(CANNED_DATA)); + mDispatcher.processRequest(mRequest); + + ArgumentCaptor response = ArgumentCaptor.forClass(Response.class); + verify(mDelivery).postResponse(any(Request.class), response.capture()); + assertTrue(response.getValue().isSuccess()); + assertEquals(response.getValue().result, new String(CANNED_DATA, StandardCharsets.UTF_8)); + + verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class)); + } + + @Test + public void successNotifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + mRequest.setRequestQueue(queue); + + when(mNetwork.performRequest(any(Request.class))) + .thenReturn(new NetworkResponse(CANNED_DATA)); + + mDispatcher.processRequest(mRequest); + + InOrder inOrder = inOrder(listener); + inOrder.verify(listener) + .onRequestEvent( + mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + inOrder.verify(listener) + .onRequestEvent( + mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void exceptionPostsError() throws Exception { + when(mNetwork.performRequest(any(Request.class))).thenThrow(new ServerError()); + mDispatcher.processRequest(mRequest); + + verify(mDelivery).postError(any(Request.class), any(VolleyError.class)); + verify(mDelivery, never()).postResponse(any(Request.class), any(Response.class)); + } + + @Test + public void exceptionNotifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + mRequest.setRequestQueue(queue); + + when(mNetwork.performRequest(any(Request.class))).thenThrow(new ServerError()); + + mDispatcher.processRequest(mRequest); + + InOrder inOrder = inOrder(listener); + inOrder.verify(listener) + .onRequestEvent( + mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + inOrder.verify(listener) + .onRequestEvent( + mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void shouldCacheFalse() throws Exception { + mRequest.setShouldCache(false); + mDispatcher.processRequest(mRequest); + verify(mCache, never()).put(anyString(), any(Cache.Entry.class)); + } + + @Test + public void shouldCacheTrue() throws Exception { + when(mNetwork.performRequest(any(Request.class))) + .thenReturn(new NetworkResponse(CANNED_DATA)); + mRequest.setShouldCache(true); + mDispatcher.processRequest(mRequest); + ArgumentCaptor entry = ArgumentCaptor.forClass(Cache.Entry.class); + verify(mCache).put(eq(mRequest.getCacheKey()), entry.capture()); + assertTrue(Arrays.equals(entry.getValue().data, CANNED_DATA)); + } +} diff --git a/core/src/test/java/com/android/volley/NetworkResponseTest.java b/core/src/test/java/com/android/volley/NetworkResponseTest.java new file mode 100644 index 0000000..70210da --- /dev/null +++ b/core/src/test/java/com/android/volley/NetworkResponseTest.java @@ -0,0 +1,61 @@ +package com.android.volley; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class NetworkResponseTest { + + @SuppressWarnings("deprecation") + @Test + public void mapToList() { + Map headers = new HashMap<>(); + headers.put("key1", "value1"); + headers.put("key2", "value2"); + + NetworkResponse resp = new NetworkResponse(200, null, headers, false); + + List
expectedHeaders = new ArrayList<>(); + expectedHeaders.add(new Header("key1", "value1")); + expectedHeaders.add(new Header("key2", "value2")); + + assertThat(expectedHeaders, containsInAnyOrder(resp.allHeaders.toArray(new Header[0]))); + } + + @Test + public void listToMap() { + List
headers = new ArrayList<>(); + headers.add(new Header("key1", "value1")); + // Later values should be preferred. + headers.add(new Header("key2", "ignoredvalue")); + headers.add(new Header("key2", "value2")); + + NetworkResponse resp = new NetworkResponse(200, null, false, 0L, headers); + + Map expectedHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + expectedHeaders.put("key1", "value1"); + expectedHeaders.put("key2", "value2"); + + assertEquals(expectedHeaders, resp.headers); + } + + @SuppressWarnings("deprecation") + @Test + public void nullValuesDontCrash() { + new NetworkResponse(null); + new NetworkResponse(null, null); + new NetworkResponse(200, null, null, false); + new NetworkResponse(200, null, null, false, 0L); + new NetworkResponse(200, null, false, 0L, null); + } +} diff --git a/core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java b/core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java new file mode 100644 index 0000000..a2bfbc6 --- /dev/null +++ b/core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.Request.Priority; +import com.android.volley.RequestQueue.RequestFinishedListener; +import com.android.volley.mock.MockRequest; +import com.android.volley.mock.ShadowSystemClock; +import com.android.volley.toolbox.NoCache; +import com.android.volley.utils.ImmediateResponseDelivery; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Integration tests for {@link RequestQueue} that verify its behavior in conjunction with real + * dispatcher, queues and Requests. + * + *

The Network is mocked out. + */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowSystemClock.class}) +public class RequestQueueIntegrationTest { + + private ResponseDelivery mDelivery; + @Mock private Network mMockNetwork; + @Mock private RequestFinishedListener mMockListener; + @Mock private RequestFinishedListener mMockListener2; + + @Before + public void setUp() throws Exception { + mDelivery = new ImmediateResponseDelivery(); + initMocks(this); + } + + @Test + public void add_requestProcessedInCorrectOrder() throws Exception { + // Enqueue 2 requests with different cache keys, and different priorities. The second, + // higher priority request takes 20ms. + // Assert that the first request is only handled after the first one has been parsed and + // delivered. + MockRequest lowerPriorityReq = new MockRequest(); + MockRequest higherPriorityReq = new MockRequest(); + lowerPriorityReq.setCacheKey("1"); + higherPriorityReq.setCacheKey("2"); + lowerPriorityReq.setPriority(Priority.LOW); + higherPriorityReq.setPriority(Priority.HIGH); + + Answer delayAnswer = + new Answer() { + @Override + public NetworkResponse answer(InvocationOnMock invocationOnMock) + throws Throwable { + Thread.sleep(20); + return mock(NetworkResponse.class); + } + }; + // delay only for higher request + when(mMockNetwork.performRequest(higherPriorityReq)).thenAnswer(delayAnswer); + when(mMockNetwork.performRequest(lowerPriorityReq)).thenReturn(mock(NetworkResponse.class)); + + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); + queue.addRequestFinishedListener(mMockListener); + queue.add(lowerPriorityReq); + queue.add(higherPriorityReq); + queue.start(); + + InOrder inOrder = inOrder(mMockListener); + // verify higherPriorityReq goes through first + inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(higherPriorityReq); + // verify lowerPriorityReq goes last + inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(lowerPriorityReq); + + queue.stop(); + } + + /** Asserts that requests with same cache key are processed in order. */ + @Test + public void add_dedupeByCacheKey() throws Exception { + // Enqueue 2 requests with the same cache key. The first request takes 20ms. Assert that the + // second request is only handled after the first one has been parsed and delivered. + MockRequest req1 = new MockRequest(); + MockRequest req2 = new MockRequest(); + Answer delayAnswer = + new Answer() { + @Override + public NetworkResponse answer(InvocationOnMock invocationOnMock) + throws Throwable { + Thread.sleep(20); + return mock(NetworkResponse.class); + } + }; + // delay only for first + when(mMockNetwork.performRequest(req1)).thenAnswer(delayAnswer); + when(mMockNetwork.performRequest(req2)).thenReturn(mock(NetworkResponse.class)); + + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 3, mDelivery); + queue.addRequestFinishedListener(mMockListener); + queue.add(req1); + queue.add(req2); + queue.start(); + + InOrder inOrder = inOrder(mMockListener); + // verify req1 goes through first + inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(req1); + // verify req2 goes last + inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(req2); + + queue.stop(); + } + + /** Verify RequestFinishedListeners are informed when requests are canceled. */ + @Test + public void add_requestFinishedListenerCanceled() throws Exception { + MockRequest request = new MockRequest(); + Answer delayAnswer = + new Answer() { + @Override + public NetworkResponse answer(InvocationOnMock invocationOnMock) + throws Throwable { + Thread.sleep(200); + return mock(NetworkResponse.class); + } + }; + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); + + when(mMockNetwork.performRequest(request)).thenAnswer(delayAnswer); + + queue.addRequestFinishedListener(mMockListener); + queue.start(); + queue.add(request); + + request.cancel(); + verify(mMockListener, timeout(10000)).onRequestFinished(request); + queue.stop(); + } + + /** Verify RequestFinishedListeners are informed when requests are successfully delivered. */ + @Test + public void add_requestFinishedListenerSuccess() throws Exception { + MockRequest request = new MockRequest(); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); + + queue.addRequestFinishedListener(mMockListener); + queue.addRequestFinishedListener(mMockListener2); + queue.start(); + queue.add(request); + + verify(mMockListener, timeout(10000)).onRequestFinished(request); + verify(mMockListener2, timeout(10000)).onRequestFinished(request); + + queue.stop(); + } + + /** Verify RequestFinishedListeners are informed when request errors. */ + @Test + public void add_requestFinishedListenerError() throws Exception { + MockRequest request = new MockRequest(); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); + + when(mMockNetwork.performRequest(request)).thenThrow(new VolleyError()); + + queue.addRequestFinishedListener(mMockListener); + queue.start(); + queue.add(request); + + verify(mMockListener, timeout(10000)).onRequestFinished(request); + queue.stop(); + } +} diff --git a/core/src/test/java/com/android/volley/RequestQueueTest.java b/core/src/test/java/com/android/volley/RequestQueueTest.java new file mode 100644 index 0000000..ba9b0f8 --- /dev/null +++ b/core/src/test/java/com/android/volley/RequestQueueTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.mock.ShadowSystemClock; +import com.android.volley.toolbox.NoCache; +import com.android.volley.toolbox.StringRequest; +import com.android.volley.utils.ImmediateResponseDelivery; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Unit tests for RequestQueue, with all dependencies mocked out */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowSystemClock.class}) +public class RequestQueueTest { + + private ResponseDelivery mDelivery; + @Mock private Network mMockNetwork; + + @Before + public void setUp() throws Exception { + mDelivery = new ImmediateResponseDelivery(); + initMocks(this); + } + + @Test + public void cancelAll_onlyCorrectTag() throws Exception { + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); + Object tagA = new Object(); + Object tagB = new Object(); + StringRequest req1 = mock(StringRequest.class); + when(req1.getTag()).thenReturn(tagA); + StringRequest req2 = mock(StringRequest.class); + when(req2.getTag()).thenReturn(tagB); + StringRequest req3 = mock(StringRequest.class); + when(req3.getTag()).thenReturn(tagA); + StringRequest req4 = mock(StringRequest.class); + when(req4.getTag()).thenReturn(tagA); + + queue.add(req1); // A + queue.add(req2); // B + queue.add(req3); // A + queue.cancelAll(tagA); + queue.add(req4); // A + + verify(req1).cancel(); // A cancelled + verify(req3).cancel(); // A cancelled + verify(req2, never()).cancel(); // B not cancelled + verify(req4, never()).cancel(); // A added after cancel not cancelled + } + + @Test + public void add_notifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + StringRequest req = mock(StringRequest.class); + + queue.add(req); + + verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_QUEUED); + verifyNoMoreInteractions(listener); + } + + @Test + public void finish_notifiesListener() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + StringRequest req = mock(StringRequest.class); + + queue.finish(req); + + verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_FINISHED); + verifyNoMoreInteractions(listener); + } + + @Test + public void sendRequestEvent_notifiesListener() throws Exception { + StringRequest req = mock(StringRequest.class); + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + + queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + + verify(listener) + .onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + verifyNoMoreInteractions(listener); + } + + @Test + public void removeRequestEventListener_removesListener() throws Exception { + StringRequest req = mock(StringRequest.class); + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + queue.removeRequestEventListener(listener); + + queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + + verifyNoMoreInteractions(listener); + } +} diff --git a/core/src/test/java/com/android/volley/RequestTest.java b/core/src/test/java/com/android/volley/RequestTest.java new file mode 100644 index 0000000..cced39f --- /dev/null +++ b/core/src/test/java/com/android/volley/RequestTest.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.Request.Method; +import com.android.volley.Request.Priority; +import com.android.volley.toolbox.NoCache; +import java.util.Collections; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class RequestTest { + private @Mock ResponseDelivery mDelivery; + private @Mock Network mNetwork; + + @Before + public void setUp() throws Exception { + initMocks(this); + } + + @Test + public void compareTo() { + int sequence = 0; + TestRequest low = new TestRequest(Priority.LOW); + low.setSequence(sequence++); + TestRequest low2 = new TestRequest(Priority.LOW); + low2.setSequence(sequence++); + TestRequest high = new TestRequest(Priority.HIGH); + high.setSequence(sequence++); + TestRequest immediate = new TestRequest(Priority.IMMEDIATE); + immediate.setSequence(sequence++); + + // "Low" should sort higher because it's really processing order. + assertTrue(low.compareTo(high) > 0); + assertTrue(high.compareTo(low) < 0); + assertTrue(low.compareTo(low2) < 0); + assertTrue(low.compareTo(immediate) > 0); + assertTrue(immediate.compareTo(high) < 0); + } + + private static class TestRequest extends Request { + private Priority mPriority = Priority.NORMAL; + + public TestRequest(Priority priority) { + super(Request.Method.GET, "", null); + mPriority = priority; + } + + @Override + public Priority getPriority() { + return mPriority; + } + + @Override + protected void deliverResponse(Object response) {} + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + } + + @Test + public void urlParsing() { + UrlParseRequest nullUrl = new UrlParseRequest(null); + assertEquals(0, nullUrl.getTrafficStatsTag()); + UrlParseRequest emptyUrl = new UrlParseRequest(""); + assertEquals(0, emptyUrl.getTrafficStatsTag()); + UrlParseRequest noHost = new UrlParseRequest("http:///"); + assertEquals(0, noHost.getTrafficStatsTag()); + UrlParseRequest badProtocol = new UrlParseRequest("bad:http://foo"); + assertEquals(0, badProtocol.getTrafficStatsTag()); + UrlParseRequest goodProtocol = new UrlParseRequest("http://foo"); + assertFalse(0 == goodProtocol.getTrafficStatsTag()); + } + + @Test + public void getCacheKey() { + assertEquals( + "http://example.com", + new UrlParseRequest(Method.GET, "http://example.com").getCacheKey()); + assertEquals( + "http://example.com", + new UrlParseRequest(Method.DEPRECATED_GET_OR_POST, "http://example.com") + .getCacheKey()); + assertEquals( + "1-http://example.com", + new UrlParseRequest(Method.POST, "http://example.com").getCacheKey()); + assertEquals( + "2-http://example.com", + new UrlParseRequest(Method.PUT, "http://example.com").getCacheKey()); + } + + private static class UrlParseRequest extends Request { + UrlParseRequest(String url) { + this(Method.GET, url); + } + + UrlParseRequest(int method, String url) { + super(method, url, null); + } + + @Override + protected void deliverResponse(Object response) {} + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + } + + @Test + public void nullKeyInPostParams() throws Exception { + Request request = + new Request(Method.POST, "url", null) { + @Override + protected void deliverResponse(Object response) {} + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected Map getParams() { + return Collections.singletonMap(null, "value"); + } + + @Override + protected Map getPostParams() { + return Collections.singletonMap(null, "value"); + } + }; + try { + request.getBody(); + } catch (IllegalArgumentException e) { + // expected + } + try { + request.getPostBody(); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void nullValueInPostParams() throws Exception { + Request request = + new Request(Method.POST, "url", null) { + @Override + protected void deliverResponse(Object response) {} + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected Map getParams() { + return Collections.singletonMap("key", null); + } + + @Override + protected Map getPostParams() { + return Collections.singletonMap("key", null); + } + }; + try { + request.getBody(); + } catch (IllegalArgumentException e) { + // expected + } + try { + request.getPostBody(); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void sendEvent_notifiesListeners() throws Exception { + RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mNetwork, 0, mDelivery); + queue.addRequestEventListener(listener); + + Request request = + new Request(Method.POST, "url", null) { + @Override + protected void deliverResponse(Object response) {} + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + }; + request.setRequestQueue(queue); + + request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + + verify(listener) + .onRequestEvent( + request, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); + verifyNoMoreInteractions(listener); + } +} diff --git a/core/src/test/java/com/android/volley/ResponseDeliveryTest.java b/core/src/test/java/com/android/volley/ResponseDeliveryTest.java new file mode 100644 index 0000000..6e71c3b --- /dev/null +++ b/core/src/test/java/com/android/volley/ResponseDeliveryTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.android.volley.mock.MockRequest; +import com.android.volley.utils.CacheTestUtils; +import com.android.volley.utils.ImmediateResponseDelivery; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ResponseDeliveryTest { + + private ExecutorDelivery mDelivery; + private MockRequest mRequest; + private Response mSuccessResponse; + + @Before + public void setUp() throws Exception { + // Make the delivery just run its posted responses immediately. + mDelivery = new ImmediateResponseDelivery(); + mRequest = new MockRequest(); + mRequest.setSequence(1); + byte[] data = new byte[16]; + Cache.Entry cacheEntry = CacheTestUtils.makeRandomCacheEntry(data); + mSuccessResponse = Response.success(data, cacheEntry); + } + + @Test + public void postResponseCallsDeliverResponse() { + mDelivery.postResponse(mRequest, mSuccessResponse); + assertTrue(mRequest.deliverResponse_called); + assertFalse(mRequest.deliverError_called); + } + + @Test + public void postResponseSuppressesCanceled() { + mRequest.cancel(); + mDelivery.postResponse(mRequest, mSuccessResponse); + assertFalse(mRequest.deliverResponse_called); + assertFalse(mRequest.deliverError_called); + } + + @Test + public void postErrorCallsDeliverError() { + Response errorResponse = Response.error(new ServerError()); + + mDelivery.postResponse(mRequest, errorResponse); + assertTrue(mRequest.deliverError_called); + assertFalse(mRequest.deliverResponse_called); + } +} diff --git a/core/src/test/java/com/android/volley/mock/MockAsyncStack.java b/core/src/test/java/com/android/volley/mock/MockAsyncStack.java new file mode 100644 index 0000000..5ea8343 --- /dev/null +++ b/core/src/test/java/com/android/volley/mock/MockAsyncStack.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.toolbox.AsyncHttpStack; +import com.android.volley.toolbox.HttpResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class MockAsyncStack extends AsyncHttpStack { + + private HttpResponse mResponseToReturn; + + private IOException mExceptionToThrow; + + private String mLastUrl; + + private Map mLastHeaders; + + private byte[] mLastPostBody; + + public String getLastUrl() { + return mLastUrl; + } + + public Map getLastHeaders() { + return mLastHeaders; + } + + public byte[] getLastPostBody() { + return mLastPostBody; + } + + public void setResponseToReturn(HttpResponse response) { + mResponseToReturn = response; + } + + public void setExceptionToThrow(IOException exception) { + mExceptionToThrow = exception; + } + + @Override + public void executeRequest( + Request request, Map additionalHeaders, OnRequestComplete callback) { + if (mExceptionToThrow != null) { + callback.onError(mExceptionToThrow); + return; + } + mLastUrl = request.getUrl(); + mLastHeaders = new HashMap<>(); + try { + if (request.getHeaders() != null) { + mLastHeaders.putAll(request.getHeaders()); + } + } catch (AuthFailureError authFailureError) { + callback.onAuthError(authFailureError); + return; + } + if (additionalHeaders != null) { + mLastHeaders.putAll(additionalHeaders); + } + try { + mLastPostBody = request.getBody(); + } catch (AuthFailureError e) { + mLastPostBody = null; + } + callback.onSuccess(mResponseToReturn); + } +} diff --git a/core/src/test/java/com/android/volley/mock/MockHttpStack.java b/core/src/test/java/com/android/volley/mock/MockHttpStack.java new file mode 100644 index 0000000..b86e7a0 --- /dev/null +++ b/core/src/test/java/com/android/volley/mock/MockHttpStack.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.toolbox.BaseHttpStack; +import com.android.volley.toolbox.HttpResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class MockHttpStack extends BaseHttpStack { + + private HttpResponse mResponseToReturn; + + private IOException mExceptionToThrow; + + private String mLastUrl; + + private Map mLastHeaders; + + private byte[] mLastPostBody; + + public String getLastUrl() { + return mLastUrl; + } + + public Map getLastHeaders() { + return mLastHeaders; + } + + public byte[] getLastPostBody() { + return mLastPostBody; + } + + public void setResponseToReturn(HttpResponse response) { + mResponseToReturn = response; + } + + public void setExceptionToThrow(IOException exception) { + mExceptionToThrow = exception; + } + + @Override + public HttpResponse executeRequest(Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + if (mExceptionToThrow != null) { + throw mExceptionToThrow; + } + mLastUrl = request.getUrl(); + mLastHeaders = new HashMap<>(); + if (request.getHeaders() != null) { + mLastHeaders.putAll(request.getHeaders()); + } + if (additionalHeaders != null) { + mLastHeaders.putAll(additionalHeaders); + } + try { + mLastPostBody = request.getBody(); + } catch (AuthFailureError e) { + mLastPostBody = null; + } + return mResponseToReturn; + } +} diff --git a/core/src/test/java/com/android/volley/mock/MockRequest.java b/core/src/test/java/com/android/volley/mock/MockRequest.java new file mode 100644 index 0000000..6fc26b4 --- /dev/null +++ b/core/src/test/java/com/android/volley/mock/MockRequest.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.Response.ErrorListener; +import com.android.volley.VolleyError; +import com.android.volley.utils.CacheTestUtils; +import java.util.HashMap; +import java.util.Map; + +public class MockRequest extends Request { + public MockRequest() { + super(Request.Method.GET, "http://foo.com", null); + } + + public MockRequest(String url, ErrorListener listener) { + super(Request.Method.GET, url, listener); + } + + private Map mPostParams = new HashMap(); + + public void setPostParams(Map postParams) { + mPostParams = postParams; + } + + @Override + public Map getPostParams() { + return mPostParams; + } + + private String mCacheKey = super.getCacheKey(); + + public void setCacheKey(String cacheKey) { + mCacheKey = cacheKey; + } + + @Override + public String getCacheKey() { + return mCacheKey; + } + + public boolean deliverResponse_called = false; + public boolean parseResponse_called = false; + + @Override + protected void deliverResponse(byte[] response) { + deliverResponse_called = true; + } + + public boolean deliverError_called = false; + + @Override + public void deliverError(VolleyError error) { + super.deliverError(error); + deliverError_called = true; + } + + public boolean cancel_called = false; + + @Override + public void cancel() { + cancel_called = true; + super.cancel(); + } + + private Priority mPriority = super.getPriority(); + + public void setPriority(Priority priority) { + mPriority = priority; + } + + @Override + public Priority getPriority() { + return mPriority; + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + parseResponse_called = true; + return Response.success(response.data, CacheTestUtils.makeRandomCacheEntry(response.data)); + } +} diff --git a/core/src/test/java/com/android/volley/mock/ShadowSystemClock.java b/core/src/test/java/com/android/volley/mock/ShadowSystemClock.java new file mode 100644 index 0000000..6d75d4b --- /dev/null +++ b/core/src/test/java/com/android/volley/mock/ShadowSystemClock.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import android.os.SystemClock; +import org.robolectric.annotation.Implements; + +@Implements(value = SystemClock.class, callThroughByDefault = true) +public class ShadowSystemClock { + public static long elapsedRealtime() { + return 0; + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java b/core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java new file mode 100644 index 0000000..dbd6535 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java @@ -0,0 +1,128 @@ +package com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.when; + +import com.android.volley.Header; +import com.android.volley.Request; +import com.android.volley.mock.TestRequest; +import java.io.IOException; +import java.io.InputStream; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.message.BasicHeader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class AdaptedHttpStackTest { + private static final Request REQUEST = new TestRequest.Get(); + private static final Map ADDITIONAL_HEADERS = Collections.emptyMap(); + + @Mock private HttpStack mHttpStack; + @Mock private HttpResponse mHttpResponse; + @Mock private StatusLine mStatusLine; + @Mock private HttpEntity mHttpEntity; + @Mock private InputStream mContent; + + private AdaptedHttpStack mAdaptedHttpStack; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mAdaptedHttpStack = new AdaptedHttpStack(mHttpStack); + when(mHttpResponse.getStatusLine()).thenReturn(mStatusLine); + } + + @Test(expected = SocketTimeoutException.class) + public void requestTimeout() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)) + .thenThrow(new ConnectTimeoutException()); + + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + } + + @Test + public void emptyResponse() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); + when(mStatusLine.getStatusCode()).thenReturn(12345); + when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]); + + com.android.volley.toolbox.HttpResponse response = + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + + assertEquals(12345, response.getStatusCode()); + assertEquals(Collections.emptyList(), response.getHeaders()); + assertNull(response.getContent()); + } + + @Test + public void nonEmptyResponse() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); + when(mStatusLine.getStatusCode()).thenReturn(12345); + when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]); + when(mHttpResponse.getEntity()).thenReturn(mHttpEntity); + when(mHttpEntity.getContentLength()).thenReturn((long) Integer.MAX_VALUE); + when(mHttpEntity.getContent()).thenReturn(mContent); + + com.android.volley.toolbox.HttpResponse response = + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + + assertEquals(12345, response.getStatusCode()); + assertEquals(Collections.emptyList(), response.getHeaders()); + assertEquals(Integer.MAX_VALUE, response.getContentLength()); + assertSame(mContent, response.getContent()); + } + + @Test(expected = IOException.class) + public void responseTooBig() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); + when(mStatusLine.getStatusCode()).thenReturn(12345); + when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]); + when(mHttpResponse.getEntity()).thenReturn(mHttpEntity); + when(mHttpEntity.getContentLength()).thenReturn(Integer.MAX_VALUE + 1L); + when(mHttpEntity.getContent()).thenReturn(mContent); + + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + } + + @Test + public void responseWithHeaders() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); + when(mStatusLine.getStatusCode()).thenReturn(12345); + when(mHttpResponse.getAllHeaders()) + .thenReturn( + new org.apache.http.Header[] { + new BasicHeader("header1", "value1_B"), + new BasicHeader("header3", "value3"), + new BasicHeader("HEADER2", "value2"), + new BasicHeader("header1", "value1_A") + }); + + com.android.volley.toolbox.HttpResponse response = + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + + assertEquals(12345, response.getStatusCode()); + assertNull(response.getContent()); + + List
expectedHeaders = new ArrayList<>(); + expectedHeaders.add(new Header("header1", "value1_B")); + expectedHeaders.add(new Header("header3", "value3")); + expectedHeaders.add(new Header("HEADER2", "value2")); + expectedHeaders.add(new Header("header1", "value1_A")); + assertEquals(expectedHeaders, response.getHeaders()); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java b/core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java new file mode 100644 index 0000000..982eda2 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import com.android.volley.AuthFailureError; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class AndroidAuthenticatorTest { + @Mock private AccountManager mAccountManager; + @Mock private AccountManagerFuture mFuture; + private Account mAccount; + private AndroidAuthenticator mAuthenticator; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mAccount = new Account("coolperson", "cooltype"); + mAuthenticator = new AndroidAuthenticator(mAccountManager, mAccount, "cooltype", false); + } + + @Test(expected = AuthFailureError.class) + public void failedGetAuthToken() throws Exception { + when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)) + .thenReturn(mFuture); + when(mFuture.getResult()).thenThrow(new AuthenticatorException("sadness!")); + mAuthenticator.getAuthToken(); + } + + @Test(expected = AuthFailureError.class) + public void resultContainsIntent() throws Exception { + Intent intent = new Intent(); + Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)) + .thenReturn(mFuture); + when(mFuture.getResult()).thenReturn(bundle); + when(mFuture.isDone()).thenReturn(true); + when(mFuture.isCancelled()).thenReturn(false); + mAuthenticator.getAuthToken(); + } + + @Test(expected = AuthFailureError.class) + public void missingAuthToken() throws Exception { + Bundle bundle = new Bundle(); + when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)) + .thenReturn(mFuture); + when(mFuture.getResult()).thenReturn(bundle); + when(mFuture.isDone()).thenReturn(true); + when(mFuture.isCancelled()).thenReturn(false); + mAuthenticator.getAuthToken(); + } + + @Test + public void invalidateAuthToken() throws Exception { + mAuthenticator.invalidateAuthToken("monkey"); + verify(mAccountManager).invalidateAuthToken("cooltype", "monkey"); + } + + @Test + public void goodToken() throws Exception { + Bundle bundle = new Bundle(); + bundle.putString(AccountManager.KEY_AUTHTOKEN, "monkey"); + when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)) + .thenReturn(mFuture); + when(mFuture.getResult()).thenReturn(bundle); + when(mFuture.isDone()).thenReturn(true); + when(mFuture.isCancelled()).thenReturn(false); + Assert.assertEquals("monkey", mAuthenticator.getAuthToken()); + } + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + Context context = mock(Context.class); + new AndroidAuthenticator(context, mAccount, "cooltype"); + new AndroidAuthenticator(context, mAccount, "cooltype", true); + Assert.assertSame(mAccount, mAuthenticator.getAccount()); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java b/core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java new file mode 100644 index 0000000..1049ad0 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java @@ -0,0 +1,104 @@ +package com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import com.android.volley.AuthFailureError; +import com.android.volley.Header; +import com.android.volley.Request; +import com.android.volley.mock.TestRequest; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class BaseHttpStackTest { + private static final Request REQUEST = new TestRequest.Get(); + private static final Map ADDITIONAL_HEADERS = Collections.emptyMap(); + + @Mock private InputStream mContent; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void legacyRequestWithoutBody() throws Exception { + BaseHttpStack stack = + new BaseHttpStack() { + @Override + public HttpResponse executeRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + assertSame(REQUEST, request); + assertSame(ADDITIONAL_HEADERS, additionalHeaders); + return new HttpResponse(12345, Collections.
emptyList()); + } + }; + org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS); + assertEquals(12345, resp.getStatusLine().getStatusCode()); + assertEquals(0, resp.getAllHeaders().length); + assertNull(resp.getEntity()); + } + + @Test + public void legacyResponseWithBody() throws Exception { + BaseHttpStack stack = + new BaseHttpStack() { + @Override + public HttpResponse executeRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + assertSame(REQUEST, request); + assertSame(ADDITIONAL_HEADERS, additionalHeaders); + return new HttpResponse( + 12345, Collections.
emptyList(), 555, mContent); + } + }; + org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS); + assertEquals(12345, resp.getStatusLine().getStatusCode()); + assertEquals(0, resp.getAllHeaders().length); + assertEquals(555L, resp.getEntity().getContentLength()); + assertSame(mContent, resp.getEntity().getContent()); + } + + @Test + public void legacyResponseHeaders() throws Exception { + BaseHttpStack stack = + new BaseHttpStack() { + @Override + public HttpResponse executeRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + assertSame(REQUEST, request); + assertSame(ADDITIONAL_HEADERS, additionalHeaders); + List
headers = new ArrayList<>(); + headers.add(new Header("HeaderA", "ValueA")); + headers.add(new Header("HeaderB", "ValueB_1")); + headers.add(new Header("HeaderB", "ValueB_2")); + return new HttpResponse(12345, headers); + } + }; + org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS); + assertEquals(12345, resp.getStatusLine().getStatusCode()); + assertEquals(3, resp.getAllHeaders().length); + assertEquals("HeaderA", resp.getAllHeaders()[0].getName()); + assertEquals("ValueA", resp.getAllHeaders()[0].getValue()); + assertEquals("HeaderB", resp.getAllHeaders()[1].getName()); + assertEquals("ValueB_1", resp.getAllHeaders()[1].getValue()); + assertEquals("HeaderB", resp.getAllHeaders()[2].getName()); + assertEquals("ValueB_2", resp.getAllHeaders()[2].getValue()); + assertNull(resp.getEntity()); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java b/core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java new file mode 100644 index 0000000..91d4062 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.AsyncNetwork; +import com.android.volley.AuthFailureError; +import com.android.volley.Cache.Entry; +import com.android.volley.Header; +import com.android.volley.NetworkResponse; +import com.android.volley.NoConnectionError; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.RetryPolicy; +import com.android.volley.ServerError; +import com.android.volley.TimeoutError; +import com.android.volley.VolleyError; +import com.android.volley.mock.MockAsyncStack; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class BasicAsyncNetworkTest { + + @Mock private RetryPolicy mMockRetryPolicy; + @Mock private AsyncNetwork.OnRequestComplete mockCallback; + private ExecutorService executor = MoreExecutors.newDirectExecutorService(); + + @Before + public void setUp() throws Exception { + initMocks(this); + } + + @Test + public void headersAndPostParams() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = + new HttpResponse( + 200, + Collections.
emptyList(), + "foobar".getBytes(StandardCharsets.UTF_8)); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.etag = "foobar"; + entry.lastModified = 1503102002000L; + request.setCacheEntry(entry); + perform(request, httpNetwork).get(); + assertEquals("foo", mockAsyncStack.getLastHeaders().get("requestheader")); + assertEquals("foobar", mockAsyncStack.getLastHeaders().get("If-None-Match")); + assertEquals( + "Sat, 19 Aug 2017 00:20:02 GMT", + mockAsyncStack.getLastHeaders().get("If-Modified-Since")); + assertEquals( + "requestpost=foo&", + new String(mockAsyncStack.getLastPostBody(), StandardCharsets.UTF_8)); + } + + @Test + public void headersAndPostParamsStream() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + ByteArrayInputStream stream = new ByteArrayInputStream("foobar".getBytes("UTF-8")); + HttpResponse fakeResponse = + new HttpResponse(200, Collections.
emptyList(), 6, stream); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.etag = "foobar"; + entry.lastModified = 1503102002000L; + request.setCacheEntry(entry); + perform(request, httpNetwork).get(); + assertEquals("foo", mockAsyncStack.getLastHeaders().get("requestheader")); + assertEquals("foobar", mockAsyncStack.getLastHeaders().get("If-None-Match")); + assertEquals( + "Sat, 19 Aug 2017 00:20:02 GMT", + mockAsyncStack.getLastHeaders().get("If-Modified-Since")); + assertEquals( + "requestpost=foo&", + new String(mockAsyncStack.getLastPostBody(), StandardCharsets.UTF_8)); + } + + @Test + public void notModified() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + List
headers = new ArrayList<>(); + headers.add(new Header("ServerKeyA", "ServerValueA")); + headers.add(new Header("ServerKeyB", "ServerValueB")); + headers.add(new Header("SharedKey", "ServerValueShared")); + headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.allResponseHeaders = new ArrayList<>(); + entry.allResponseHeaders.add(new Header("CachedKeyA", "CachedValueA")); + entry.allResponseHeaders.add(new Header("CachedKeyB", "CachedValueB")); + entry.allResponseHeaders.add(new Header("SharedKey", "CachedValueShared")); + entry.allResponseHeaders.add(new Header("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1")); + entry.allResponseHeaders.add(new Header("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2")); + request.setCacheEntry(entry); + httpNetwork.performRequest(request, mockCallback); + NetworkResponse response = perform(request, httpNetwork).get(); + List
expectedHeaders = new ArrayList<>(); + // Should have all server headers + cache headers that didn't show up in server response. + expectedHeaders.add(new Header("ServerKeyA", "ServerValueA")); + expectedHeaders.add(new Header("ServerKeyB", "ServerValueB")); + expectedHeaders.add(new Header("SharedKey", "ServerValueShared")); + expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + expectedHeaders.add(new Header("CachedKeyA", "CachedValueA")); + expectedHeaders.add(new Header("CachedKeyB", "CachedValueB")); + assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0]))); + } + + @Test + public void notModified_legacyCache() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + List
headers = new ArrayList<>(); + headers.add(new Header("ServerKeyA", "ServerValueA")); + headers.add(new Header("ServerKeyB", "ServerValueB")); + headers.add(new Header("SharedKey", "ServerValueShared")); + headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.responseHeaders = new HashMap<>(); + entry.responseHeaders.put("CachedKeyA", "CachedValueA"); + entry.responseHeaders.put("CachedKeyB", "CachedValueB"); + entry.responseHeaders.put("SharedKey", "CachedValueShared"); + entry.responseHeaders.put("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1"); + entry.responseHeaders.put("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2"); + request.setCacheEntry(entry); + NetworkResponse response = perform(request, httpNetwork).get(); + List
expectedHeaders = new ArrayList<>(); + // Should have all server headers + cache headers that didn't show up in server response. + expectedHeaders.add(new Header("ServerKeyA", "ServerValueA")); + expectedHeaders.add(new Header("ServerKeyB", "ServerValueB")); + expectedHeaders.add(new Header("SharedKey", "ServerValueShared")); + expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + expectedHeaders.add(new Header("CachedKeyA", "CachedValueA")); + expectedHeaders.add(new Header("CachedKeyB", "CachedValueB")); + assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0]))); + } + + @Test + public void socketTimeout() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + mockAsyncStack.setExceptionToThrow(new SocketTimeoutException()); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should retry socket timeouts + verify(mMockRetryPolicy).retry(any(TimeoutError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test + public void noConnectionDefault() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + mockAsyncStack.setExceptionToThrow(new IOException()); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should not retry when there is no connection + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test + public void noConnectionRetry() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + mockAsyncStack.setExceptionToThrow(new IOException()); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryConnectionErrors(true); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should retry when there is no connection + verify(mMockRetryPolicy).retry(any(NoConnectionError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test + public void noConnectionNoRetry() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + mockAsyncStack.setExceptionToThrow(new IOException()); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryConnectionErrors(false); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should not retry when there is no connection + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test + public void unauthorized() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(401, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should retry in case it's an auth failure. + verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test(expected = RuntimeException.class) + public void malformedUrlRequest() throws VolleyError, ExecutionException, InterruptedException { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + mockAsyncStack.setExceptionToThrow(new MalformedURLException()); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + perform(request, httpNetwork).get(); + } + + @Test + public void forbidden() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(403, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should retry in case it's an auth failure. + verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test + public void redirect() throws Exception { + for (int i = 300; i <= 399; i++) { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + if (i != 304) { + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + } else { + verify(mockCallback, never()).onError(any(VolleyError.class)); + verify(mockCallback, times(1)).onSuccess(any(NetworkResponse.class)); + } + // should not retry 300 responses. + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + } + + @Test + public void otherClientError() throws Exception { + for (int i = 400; i <= 499; i++) { + if (i == 401 || i == 403) { + // covered above. + continue; + } + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should not retry other 400 errors. + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + } + + @Test + public void serverError_enableRetries() throws Exception { + for (int i = 500; i <= 599; i++) { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = + new BasicAsyncNetwork.Builder(mockAsyncStack) + .setPool(new ByteArrayPool(4096)) + .build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryServerErrors(true); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should retry all 500 errors + verify(mMockRetryPolicy).retry(any(ServerError.class)); + reset(mMockRetryPolicy, mockCallback); + } + } + + @Test + public void serverError_disableRetries() throws Exception { + for (int i = 500; i <= 599; i++) { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onError(any(VolleyError.class)); + verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); + // should not retry any 500 error w/ HTTP 500 retries turned off (the default). + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + } + + @Test + public void notModifiedShortCircuit() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + List
headers = new ArrayList<>(); + headers.add(new Header("ServerKeyA", "ServerValueA")); + headers.add(new Header("ServerKeyB", "ServerValueB")); + headers.add(new Header("SharedKey", "ServerValueShared")); + headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onSuccess(any(NetworkResponse.class)); + verify(mockCallback, never()).onError(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test + public void performRequestSuccess() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = + new HttpResponse( + 200, + Collections.
emptyList(), + "foobar".getBytes(StandardCharsets.UTF_8)); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + httpNetwork.setBlockingExecutor(executor); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.etag = "foobar"; + entry.lastModified = 1503102002000L; + request.setCacheEntry(entry); + httpNetwork.performRequest(request, mockCallback); + verify(mockCallback, times(1)).onSuccess(any(NetworkResponse.class)); + verify(mockCallback, never()).onError(any(VolleyError.class)); + reset(mMockRetryPolicy, mockCallback); + } + + @Test(expected = IllegalStateException.class) + public void performRequestNeverSetExecutorTest() throws Exception { + MockAsyncStack mockAsyncStack = new MockAsyncStack(); + HttpResponse fakeResponse = new HttpResponse(200, Collections.
emptyList()); + mockAsyncStack.setResponseToReturn(fakeResponse); + BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); + Request request = buildRequest(); + perform(request, httpNetwork).get(); + } + + /** Helper functions */ + private CompletableFuture perform(Request request, AsyncNetwork network) + throws VolleyError { + final CompletableFuture future = new CompletableFuture<>(); + network.performRequest( + request, + new AsyncNetwork.OnRequestComplete() { + @Override + public void onSuccess(NetworkResponse networkResponse) { + future.complete(networkResponse); + } + + @Override + public void onError(VolleyError volleyError) { + future.complete(null); + } + }); + return future; + } + + private static Request buildRequest() { + return new Request(Request.Method.GET, "http://foo", null) { + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected void deliverResponse(String response) {} + + @Override + public Map getHeaders() { + Map result = new HashMap(); + result.put("requestheader", "foo"); + return result; + } + + @Override + public Map getParams() { + Map result = new HashMap(); + result.put("requestpost", "foo"); + return result; + } + }; + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java b/core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java new file mode 100644 index 0000000..3630379 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.volley.AuthFailureError; +import com.android.volley.Cache.Entry; +import com.android.volley.Header; +import com.android.volley.NetworkResponse; +import com.android.volley.NoConnectionError; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.RetryPolicy; +import com.android.volley.ServerError; +import com.android.volley.TimeoutError; +import com.android.volley.VolleyError; +import com.android.volley.mock.MockHttpStack; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class BasicNetworkTest { + + @Mock private Request mMockRequest; + @Mock private RetryPolicy mMockRetryPolicy; + + @Before + public void setUp() throws Exception { + initMocks(this); + } + + @Test + public void headersAndPostParams() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + InputStream responseStream = + new ByteArrayInputStream("foobar".getBytes(StandardCharsets.UTF_8)); + HttpResponse fakeResponse = + new HttpResponse(200, Collections.
emptyList(), 6, responseStream); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.etag = "foobar"; + entry.lastModified = 1503102002000L; + request.setCacheEntry(entry); + httpNetwork.performRequest(request); + assertEquals("foo", mockHttpStack.getLastHeaders().get("requestheader")); + assertEquals("foobar", mockHttpStack.getLastHeaders().get("If-None-Match")); + assertEquals( + "Sat, 19 Aug 2017 00:20:02 GMT", + mockHttpStack.getLastHeaders().get("If-Modified-Since")); + assertEquals( + "requestpost=foo&", + new String(mockHttpStack.getLastPostBody(), StandardCharsets.UTF_8)); + } + + @Test + public void notModified() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + List
headers = new ArrayList<>(); + headers.add(new Header("ServerKeyA", "ServerValueA")); + headers.add(new Header("ServerKeyB", "ServerValueB")); + headers.add(new Header("SharedKey", "ServerValueShared")); + headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.allResponseHeaders = new ArrayList<>(); + entry.allResponseHeaders.add(new Header("CachedKeyA", "CachedValueA")); + entry.allResponseHeaders.add(new Header("CachedKeyB", "CachedValueB")); + entry.allResponseHeaders.add(new Header("SharedKey", "CachedValueShared")); + entry.allResponseHeaders.add(new Header("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1")); + entry.allResponseHeaders.add(new Header("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2")); + request.setCacheEntry(entry); + NetworkResponse response = httpNetwork.performRequest(request); + List
expectedHeaders = new ArrayList<>(); + // Should have all server headers + cache headers that didn't show up in server response. + expectedHeaders.add(new Header("ServerKeyA", "ServerValueA")); + expectedHeaders.add(new Header("ServerKeyB", "ServerValueB")); + expectedHeaders.add(new Header("SharedKey", "ServerValueShared")); + expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + expectedHeaders.add(new Header("CachedKeyA", "CachedValueA")); + expectedHeaders.add(new Header("CachedKeyB", "CachedValueB")); + assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0]))); + } + + @Test + public void notModified_legacyCache() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + List
headers = new ArrayList<>(); + headers.add(new Header("ServerKeyA", "ServerValueA")); + headers.add(new Header("ServerKeyB", "ServerValueB")); + headers.add(new Header("SharedKey", "ServerValueShared")); + headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + Entry entry = new Entry(); + entry.responseHeaders = new HashMap<>(); + entry.responseHeaders.put("CachedKeyA", "CachedValueA"); + entry.responseHeaders.put("CachedKeyB", "CachedValueB"); + entry.responseHeaders.put("SharedKey", "CachedValueShared"); + entry.responseHeaders.put("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1"); + entry.responseHeaders.put("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2"); + request.setCacheEntry(entry); + NetworkResponse response = httpNetwork.performRequest(request); + List
expectedHeaders = new ArrayList<>(); + // Should have all server headers + cache headers that didn't show up in server response. + expectedHeaders.add(new Header("ServerKeyA", "ServerValueA")); + expectedHeaders.add(new Header("ServerKeyB", "ServerValueB")); + expectedHeaders.add(new Header("SharedKey", "ServerValueShared")); + expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); + expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); + expectedHeaders.add(new Header("CachedKeyA", "CachedValueA")); + expectedHeaders.add(new Header("CachedKeyB", "CachedValueB")); + assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0]))); + } + + @Test + public void socketTimeout() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + mockHttpStack.setExceptionToThrow(new SocketTimeoutException()); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry socket timeouts + verify(mMockRetryPolicy).retry(any(TimeoutError.class)); + } + + @Test + public void noConnectionDefault() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + mockHttpStack.setExceptionToThrow(new IOException()); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry when there is no connection + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + } + + @Test + public void noConnectionRetry() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + mockHttpStack.setExceptionToThrow(new IOException()); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryConnectionErrors(true); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry when there is no connection + verify(mMockRetryPolicy).retry(any(NoConnectionError.class)); + reset(mMockRetryPolicy); + } + + @Test + public void noConnectionNoRetry() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + mockHttpStack.setExceptionToThrow(new IOException()); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryConnectionErrors(false); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry when there is no connection + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + } + + @Test + public void unauthorized() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + HttpResponse fakeResponse = new HttpResponse(401, Collections.
emptyList()); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry in case it's an auth failure. + verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); + } + + @Test + public void forbidden() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + HttpResponse fakeResponse = new HttpResponse(403, Collections.
emptyList()); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry in case it's an auth failure. + verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); + } + + @Test + public void redirect() throws Exception { + for (int i = 300; i <= 399; i++) { + MockHttpStack mockHttpStack = new MockHttpStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry 300 responses. + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy); + } + } + + @Test + public void otherClientError() throws Exception { + for (int i = 400; i <= 499; i++) { + if (i == 401 || i == 403) { + // covered above. + continue; + } + MockHttpStack mockHttpStack = new MockHttpStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry other 400 errors. + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy); + } + } + + @Test + public void serverError_enableRetries() throws Exception { + for (int i = 500; i <= 599; i++) { + MockHttpStack mockHttpStack = new MockHttpStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack, new ByteArrayPool(4096)); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryServerErrors(true); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry all 500 errors + verify(mMockRetryPolicy).retry(any(ServerError.class)); + reset(mMockRetryPolicy); + } + } + + @Test + public void serverError_disableRetries() throws Exception { + for (int i = 500; i <= 599; i++) { + MockHttpStack mockHttpStack = new MockHttpStack(); + HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry any 500 error w/ HTTP 500 retries turned off (the default). + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy); + } + } + + private static Request buildRequest() { + return new Request(Request.Method.GET, "http://foo", null) { + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected void deliverResponse(String response) {} + + @Override + public Map getHeaders() { + Map result = new HashMap(); + result.put("requestheader", "foo"); + return result; + } + + @Override + public Map getParams() { + Map result = new HashMap(); + result.put("requestpost", "foo"); + return result; + } + }; + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java b/core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java new file mode 100644 index 0000000..62da207 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class ByteArrayPoolTest { + @Test + public void reusesBuffer() { + ByteArrayPool pool = new ByteArrayPool(32); + + byte[] buf1 = pool.getBuf(16); + byte[] buf2 = pool.getBuf(16); + + pool.returnBuf(buf1); + pool.returnBuf(buf2); + + byte[] buf3 = pool.getBuf(16); + byte[] buf4 = pool.getBuf(16); + assertTrue(buf3 == buf1 || buf3 == buf2); + assertTrue(buf4 == buf1 || buf4 == buf2); + assertTrue(buf3 != buf4); + } + + @Test + public void obeysSizeLimit() { + ByteArrayPool pool = new ByteArrayPool(32); + + byte[] buf1 = pool.getBuf(16); + byte[] buf2 = pool.getBuf(16); + byte[] buf3 = pool.getBuf(16); + + pool.returnBuf(buf1); + pool.returnBuf(buf2); + pool.returnBuf(buf3); + + byte[] buf4 = pool.getBuf(16); + byte[] buf5 = pool.getBuf(16); + byte[] buf6 = pool.getBuf(16); + + assertTrue(buf4 == buf2 || buf4 == buf3); + assertTrue(buf5 == buf2 || buf5 == buf3); + assertTrue(buf4 != buf5); + assertTrue(buf6 != buf1 && buf6 != buf2 && buf6 != buf3); + } + + @Test + public void returnsBufferWithRightSize() { + ByteArrayPool pool = new ByteArrayPool(32); + + byte[] buf1 = pool.getBuf(16); + pool.returnBuf(buf1); + + byte[] buf2 = pool.getBuf(17); + assertNotSame(buf2, buf1); + + byte[] buf3 = pool.getBuf(15); + assertSame(buf3, buf1); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/CacheTest.java b/core/src/test/java/com/android/volley/toolbox/CacheTest.java new file mode 100644 index 0000000..22dae22 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/CacheTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Cache; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CacheTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(Cache.class.getMethod("get", String.class)); + assertNotNull(Cache.class.getMethod("put", String.class, Cache.Entry.class)); + assertNotNull(Cache.class.getMethod("initialize")); + assertNotNull(Cache.class.getMethod("invalidate", String.class, boolean.class)); + assertNotNull(Cache.class.getMethod("remove", String.class)); + assertNotNull(Cache.class.getMethod("clear")); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java new file mode 100644 index 0000000..db6e491 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java @@ -0,0 +1,646 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.emptyArray; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import com.android.volley.Cache; +import com.android.volley.Header; +import com.android.volley.toolbox.DiskBasedCache.CacheHeader; +import com.android.volley.toolbox.DiskBasedCache.CountingInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Random; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 16) +public class DiskBasedCacheTest { + + private static final int MAX_SIZE = 1024 * 1024; + + private Cache cache; + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Rule public ExpectedException exception = ExpectedException.none(); + + @Before + public void setup() throws IOException { + // Initialize empty cache + cache = new DiskBasedCache(temporaryFolder.getRoot(), MAX_SIZE); + cache.initialize(); + } + + @After + public void teardown() { + cache = null; + } + + @Test + public void testEmptyInitialize() { + assertThat(cache.get("key"), is(nullValue())); + } + + @Test + public void testPutGetZeroBytes() { + Cache.Entry entry = new Cache.Entry(); + entry.data = new byte[0]; + entry.serverDate = 1234567L; + entry.lastModified = 13572468L; + entry.ttl = 9876543L; + entry.softTtl = 8765432L; + entry.etag = "etag"; + entry.responseHeaders = new HashMap<>(); + entry.responseHeaders.put("fruit", "banana"); + entry.responseHeaders.put("color", "yellow"); + cache.put("my-magical-key", entry); + + assertThatEntriesAreEqual(cache.get("my-magical-key"), entry); + assertThat(cache.get("unknown-key"), is(nullValue())); + } + + @Test + public void testPutRemoveGet() { + Cache.Entry entry = randomData(511); + cache.put("key", entry); + + assertThatEntriesAreEqual(cache.get("key"), entry); + + cache.remove("key"); + assertThat(cache.get("key"), is(nullValue())); + assertThat(listCachedFiles(), is(emptyArray())); + } + + @Test + public void testPutClearGet() { + Cache.Entry entry = randomData(511); + cache.put("key", entry); + + assertThatEntriesAreEqual(cache.get("key"), entry); + + cache.clear(); + assertThat(cache.get("key"), is(nullValue())); + assertThat(listCachedFiles(), is(emptyArray())); + } + + @Test + public void testReinitialize() { + Cache.Entry entry = randomData(1023); + cache.put("key", entry); + + Cache copy = new DiskBasedCache(temporaryFolder.getRoot(), MAX_SIZE); + copy.initialize(); + + assertThatEntriesAreEqual(copy.get("key"), entry); + } + + @Test + public void testInvalidate() { + Cache.Entry entry = randomData(32); + entry.softTtl = 8765432L; + entry.ttl = 9876543L; + cache.put("key", entry); + + cache.invalidate("key", false); + entry.softTtl = 0; // expired + assertThatEntriesAreEqual(cache.get("key"), entry); + } + + @Test + public void testInvalidateFullExpire() { + Cache.Entry entry = randomData(32); + entry.softTtl = 8765432L; + entry.ttl = 9876543L; + cache.put("key", entry); + + cache.invalidate("key", true); + entry.softTtl = 0; // expired + entry.ttl = 0; // expired + assertThatEntriesAreEqual(cache.get("key"), entry); + } + + @Test + public void testTooLargeEntry() { + Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("oversize")); + cache.put("oversize", entry); + + assertThat(cache.get("oversize"), is(nullValue())); + } + + @Test + public void testMaxSizeEntry() { + Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("maxsize") - 1); + cache.put("maxsize", entry); + + assertThatEntriesAreEqual(cache.get("maxsize"), entry); + } + + @Test + public void testTrimAtThreshold() { + // Start with the largest possible entry. + Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("maxsize") - 1); + cache.put("maxsize", entry); + + assertThatEntriesAreEqual(cache.get("maxsize"), entry); + + // Now any new entry should cause the first one to be cleared. + entry = randomData(0); + cache.put("bit", entry); + + assertThat(cache.get("goodsize"), is(nullValue())); + assertThatEntriesAreEqual(cache.get("bit"), entry); + } + + @Test + public void testTrimWithMultipleEvictions_underHysteresisThreshold() { + Cache.Entry entry1 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry1") - 1); + cache.put("entry1", entry1); + Cache.Entry entry2 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry2") - 1); + cache.put("entry2", entry2); + Cache.Entry entry3 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry3") - 1); + cache.put("entry3", entry3); + + assertThatEntriesAreEqual(cache.get("entry1"), entry1); + assertThatEntriesAreEqual(cache.get("entry2"), entry2); + assertThatEntriesAreEqual(cache.get("entry3"), entry3); + + Cache.Entry entry = + randomData( + (int) (DiskBasedCache.HYSTERESIS_FACTOR * MAX_SIZE) + - getEntrySizeOnDisk("max")); + cache.put("max", entry); + + assertThat(cache.get("entry1"), is(nullValue())); + assertThat(cache.get("entry2"), is(nullValue())); + assertThat(cache.get("entry3"), is(nullValue())); + assertThatEntriesAreEqual(cache.get("max"), entry); + } + + @Test + public void testTrimWithMultipleEvictions_atHysteresisThreshold() { + Cache.Entry entry1 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry1") - 1); + cache.put("entry1", entry1); + Cache.Entry entry2 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry2") - 1); + cache.put("entry2", entry2); + Cache.Entry entry3 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry3") - 1); + cache.put("entry3", entry3); + + assertThatEntriesAreEqual(cache.get("entry1"), entry1); + assertThatEntriesAreEqual(cache.get("entry2"), entry2); + assertThatEntriesAreEqual(cache.get("entry3"), entry3); + + Cache.Entry entry = + randomData( + (int) (DiskBasedCache.HYSTERESIS_FACTOR * MAX_SIZE) + - getEntrySizeOnDisk("max") + + 1); + cache.put("max", entry); + + assertThat(cache.get("entry1"), is(nullValue())); + assertThat(cache.get("entry2"), is(nullValue())); + assertThat(cache.get("entry3"), is(nullValue())); + assertThat(cache.get("max"), is(nullValue())); + } + + @Test + public void testTrimWithPartialEvictions() { + Cache.Entry entry1 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry1") - 1); + cache.put("entry1", entry1); + Cache.Entry entry2 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry2") - 1); + cache.put("entry2", entry2); + Cache.Entry entry3 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry3") - 1); + cache.put("entry3", entry3); + + assertThatEntriesAreEqual(cache.get("entry1"), entry1); + assertThatEntriesAreEqual(cache.get("entry2"), entry2); + assertThatEntriesAreEqual(cache.get("entry3"), entry3); + + Cache.Entry entry4 = randomData((MAX_SIZE - getEntrySizeOnDisk("entry4") - 1) / 2); + cache.put("entry4", entry4); + + assertThat(cache.get("entry1"), is(nullValue())); + assertThat(cache.get("entry2"), is(nullValue())); + assertThatEntriesAreEqual(cache.get("entry3"), entry3); + assertThatEntriesAreEqual(cache.get("entry4"), entry4); + } + + @Test + public void testLargeEntryDoesntClearCache() { + // Writing a large entry to an empty cache should succeed + Cache.Entry largeEntry = randomData(MAX_SIZE - getEntrySizeOnDisk("largeEntry") - 1); + cache.put("largeEntry", largeEntry); + + assertThatEntriesAreEqual(cache.get("largeEntry"), largeEntry); + + // Reset and fill up ~half the cache. + cache.clear(); + Cache.Entry entry = randomData(MAX_SIZE / 2 - getEntrySizeOnDisk("entry") - 1); + cache.put("entry", entry); + + assertThatEntriesAreEqual(cache.get("entry"), entry); + + // Writing the large entry should no-op, because otherwise the pruning algorithm would clear + // the whole cache, since the large entry is above the hysteresis threshold. + cache.put("largeEntry", largeEntry); + + assertThat(cache.get("largeEntry"), is(nullValue())); + assertThatEntriesAreEqual(cache.get("entry"), entry); + } + + @Test + @SuppressWarnings("TryFinallyCanBeTryWithResources") + public void testGetBadMagic() throws IOException { + // Cache something + Cache.Entry entry = randomData(1023); + cache.put("key", entry); + assertThatEntriesAreEqual(cache.get("key"), entry); + + // Overwrite the magic header + File cacheFolder = temporaryFolder.getRoot(); + File file = cacheFolder.listFiles()[0]; + FileOutputStream fos = new FileOutputStream(file); + try { + DiskBasedCache.writeInt(fos, 0); // overwrite magic + } finally { + //noinspection ThrowFromFinallyBlock + fos.close(); + } + + assertThat(cache.get("key"), is(nullValue())); + assertThat(listCachedFiles(), is(emptyArray())); + } + + @Test + @SuppressWarnings("TryFinallyCanBeTryWithResources") + public void testGetWrongKey() throws IOException { + // Cache something + Cache.Entry entry = randomData(1023); + cache.put("key", entry); + assertThatEntriesAreEqual(cache.get("key"), entry); + + // Access the cached file + File cacheFolder = temporaryFolder.getRoot(); + File file = cacheFolder.listFiles()[0]; + FileOutputStream fos = new FileOutputStream(file); + try { + // Overwrite with a different key + CacheHeader wrongHeader = new CacheHeader("bad", entry); + wrongHeader.writeHeader(fos); + } finally { + //noinspection ThrowFromFinallyBlock + fos.close(); + } + + // key is gone, but file is still there + assertThat(cache.get("key"), is(nullValue())); + assertThat(listCachedFiles(), is(arrayWithSize(1))); + + // Note: file is now a zombie because its key does not map to its name + } + + @Test + public void testStreamToBytesNegativeLength() throws IOException { + byte[] data = new byte[1]; + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(data), data.length); + exception.expect(IOException.class); + DiskBasedCache.streamToBytes(cis, -1); + } + + @Test + public void testStreamToBytesExcessiveLength() throws IOException { + byte[] data = new byte[1]; + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(data), data.length); + exception.expect(IOException.class); + DiskBasedCache.streamToBytes(cis, 2); + } + + @Test + public void testStreamToBytesOverflow() throws IOException { + byte[] data = new byte[0]; + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(data), 0x100000000L); + exception.expect(IOException.class); + DiskBasedCache.streamToBytes(cis, 0x100000000L); // int value is 0 + } + + @Test + public void testReadHeaderListWithNegativeSize() throws IOException { + // If a cached header list is corrupted and begins with a negative size, + // verify that readHeaderList will throw an IOException. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeInt(baos, -1); // negative size + CountingInputStream cis = + new CountingInputStream( + new ByteArrayInputStream(baos.toByteArray()), Integer.MAX_VALUE); + // Expect IOException due to negative size + exception.expect(IOException.class); + DiskBasedCache.readHeaderList(cis); + } + + @Test + public void testReadHeaderListWithGinormousSize() throws IOException { + // If a cached header list is corrupted and begins with 2GB size, verify + // that readHeaderList will throw EOFException rather than OutOfMemoryError. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeInt(baos, Integer.MAX_VALUE); // 2GB size + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size()); + // Expect EOFException when end of stream is reached + exception.expect(EOFException.class); + DiskBasedCache.readHeaderList(cis); + } + + @Test + public void testFileIsDeletedWhenWriteHeaderFails() throws IOException { + // Create DataOutputStream that throws IOException + OutputStream mockedOutputStream = spy(OutputStream.class); + doThrow(IOException.class).when(mockedOutputStream).write(anyInt()); + + // Create read-only copy that fails to write anything + DiskBasedCache readonly = spy((DiskBasedCache) cache); + doReturn(mockedOutputStream).when(readonly).createOutputStream(any(File.class)); + + // Attempt to write + readonly.put("key", randomData(1111)); + + // write is called at least once because each linked stream flushes when closed + verify(mockedOutputStream, atLeastOnce()).write(anyInt()); + assertThat(readonly.get("key"), is(nullValue())); + assertThat(listCachedFiles(), is(emptyArray())); + + // Note: original cache will try (without success) to read from file + assertThat(cache.get("key"), is(nullValue())); + } + + @Test + public void testIOExceptionInInitialize() throws IOException { + // Cache a few kilobytes + cache.put("kilobyte", randomData(1024)); + cache.put("kilobyte2", randomData(1024)); + cache.put("kilobyte3", randomData(1024)); + + // Create DataInputStream that throws IOException + InputStream mockedInputStream = spy(InputStream.class); + //noinspection ResultOfMethodCallIgnored + doThrow(IOException.class).when(mockedInputStream).read(); + + // Create broken cache that fails to read anything + DiskBasedCache broken = spy(new DiskBasedCache(temporaryFolder.getRoot())); + doReturn(mockedInputStream).when(broken).createInputStream(any(File.class)); + + // Attempt to initialize + broken.initialize(); + + // Everything is gone + assertThat(broken.get("kilobyte"), is(nullValue())); + assertThat(broken.get("kilobyte2"), is(nullValue())); + assertThat(broken.get("kilobyte3"), is(nullValue())); + assertThat(listCachedFiles(), is(emptyArray())); + + // Verify that original cache can cope with missing files + assertThat(cache.get("kilobyte"), is(nullValue())); + assertThat(cache.get("kilobyte2"), is(nullValue())); + assertThat(cache.get("kilobyte3"), is(nullValue())); + } + + @Test + public void testManyResponseHeaders() { + Cache.Entry entry = new Cache.Entry(); + entry.data = new byte[0]; + entry.responseHeaders = new HashMap<>(); + for (int i = 0; i < 0xFFFF; i++) { + entry.responseHeaders.put(Integer.toString(i), ""); + } + cache.put("key", entry); + } + + @Test + @SuppressWarnings("TryFinallyCanBeTryWithResources") + public void testCountingInputStreamByteCount() throws IOException { + // Write some bytes + ByteArrayOutputStream out = new ByteArrayOutputStream(); + //noinspection ThrowFromFinallyBlock + try { + DiskBasedCache.writeInt(out, 1); + DiskBasedCache.writeLong(out, -1L); + DiskBasedCache.writeString(out, "hamburger"); + } finally { + //noinspection ThrowFromFinallyBlock + out.close(); + } + long bytesWritten = out.size(); + + // Read the bytes and compare the counts + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(out.toByteArray()), bytesWritten); + try { + assertThat(cis.bytesRemaining(), is(bytesWritten)); + assertThat(cis.bytesRead(), is(0L)); + assertThat(DiskBasedCache.readInt(cis), is(1)); + assertThat(DiskBasedCache.readLong(cis), is(-1L)); + assertThat(DiskBasedCache.readString(cis), is("hamburger")); + assertThat(cis.bytesRead(), is(bytesWritten)); + assertThat(cis.bytesRemaining(), is(0L)); + } finally { + //noinspection ThrowFromFinallyBlock + cis.close(); + } + } + + /* Serialization tests */ + + @Test + public void testEmptyReadThrowsEOF() throws IOException { + ByteArrayInputStream empty = new ByteArrayInputStream(new byte[] {}); + exception.expect(EOFException.class); + DiskBasedCache.readInt(empty); + } + + @Test + public void serializeInt() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeInt(baos, 0); + DiskBasedCache.writeInt(baos, 19791214); + DiskBasedCache.writeInt(baos, -20050711); + DiskBasedCache.writeInt(baos, Integer.MIN_VALUE); + DiskBasedCache.writeInt(baos, Integer.MAX_VALUE); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + assertEquals(DiskBasedCache.readInt(bais), 0); + assertEquals(DiskBasedCache.readInt(bais), 19791214); + assertEquals(DiskBasedCache.readInt(bais), -20050711); + assertEquals(DiskBasedCache.readInt(bais), Integer.MIN_VALUE); + assertEquals(DiskBasedCache.readInt(bais), Integer.MAX_VALUE); + } + + @Test + public void serializeLong() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeLong(baos, 0); + DiskBasedCache.writeLong(baos, 31337); + DiskBasedCache.writeLong(baos, -4160); + DiskBasedCache.writeLong(baos, 4295032832L); + DiskBasedCache.writeLong(baos, -4314824046L); + DiskBasedCache.writeLong(baos, Long.MIN_VALUE); + DiskBasedCache.writeLong(baos, Long.MAX_VALUE); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + assertEquals(DiskBasedCache.readLong(bais), 0); + assertEquals(DiskBasedCache.readLong(bais), 31337); + assertEquals(DiskBasedCache.readLong(bais), -4160); + assertEquals(DiskBasedCache.readLong(bais), 4295032832L); + assertEquals(DiskBasedCache.readLong(bais), -4314824046L); + assertEquals(DiskBasedCache.readLong(bais), Long.MIN_VALUE); + assertEquals(DiskBasedCache.readLong(bais), Long.MAX_VALUE); + } + + @Test + public void serializeString() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeString(baos, ""); + DiskBasedCache.writeString(baos, "This is a string."); + DiskBasedCache.writeString(baos, "ファイカス"); + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size()); + assertEquals(DiskBasedCache.readString(cis), ""); + assertEquals(DiskBasedCache.readString(cis), "This is a string."); + assertEquals(DiskBasedCache.readString(cis), "ファイカス"); + } + + @Test + public void serializeHeaders() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + List
empty = new ArrayList<>(); + DiskBasedCache.writeHeaderList(empty, baos); + DiskBasedCache.writeHeaderList(null, baos); + List
twoThings = new ArrayList<>(); + twoThings.add(new Header("first", "thing")); + twoThings.add(new Header("second", "item")); + DiskBasedCache.writeHeaderList(twoThings, baos); + List
emptyKey = new ArrayList<>(); + emptyKey.add(new Header("", "value")); + DiskBasedCache.writeHeaderList(emptyKey, baos); + List
emptyValue = new ArrayList<>(); + emptyValue.add(new Header("key", "")); + DiskBasedCache.writeHeaderList(emptyValue, baos); + List
sameKeys = new ArrayList<>(); + sameKeys.add(new Header("key", "value")); + sameKeys.add(new Header("key", "value2")); + DiskBasedCache.writeHeaderList(sameKeys, baos); + CountingInputStream cis = + new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size()); + assertEquals(DiskBasedCache.readHeaderList(cis), empty); + assertEquals(DiskBasedCache.readHeaderList(cis), empty); // null reads back empty + assertEquals(DiskBasedCache.readHeaderList(cis), twoThings); + assertEquals(DiskBasedCache.readHeaderList(cis), emptyKey); + assertEquals(DiskBasedCache.readHeaderList(cis), emptyValue); + assertEquals(DiskBasedCache.readHeaderList(cis), sameKeys); + } + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(DiskBasedCache.class.getConstructor(File.class, int.class)); + assertNotNull( + DiskBasedCache.class.getConstructor(DiskBasedCache.FileSupplier.class, int.class)); + assertNotNull(DiskBasedCache.class.getConstructor(File.class)); + assertNotNull(DiskBasedCache.class.getConstructor(DiskBasedCache.FileSupplier.class)); + + assertNotNull(DiskBasedCache.class.getMethod("getFileForKey", String.class)); + } + + @Test + public void initializeIfRootDirectoryDeleted() { + temporaryFolder.delete(); + + Cache.Entry entry = randomData(101); + cache.put("key1", entry); + + assertThat(cache.get("key1"), is(nullValue())); + + // confirm that we can now store entries + cache.put("key2", entry); + assertThatEntriesAreEqual(cache.get("key2"), entry); + } + + /* Test helpers */ + + private void assertThatEntriesAreEqual(Cache.Entry actual, Cache.Entry expected) { + assertThat(actual.data, is(equalTo(expected.data))); + assertThat(actual.etag, is(equalTo(expected.etag))); + assertThat(actual.lastModified, is(equalTo(expected.lastModified))); + assertThat(actual.responseHeaders, is(equalTo(expected.responseHeaders))); + assertThat(actual.serverDate, is(equalTo(expected.serverDate))); + assertThat(actual.softTtl, is(equalTo(expected.softTtl))); + assertThat(actual.ttl, is(equalTo(expected.ttl))); + } + + private Cache.Entry randomData(int length) { + Cache.Entry entry = new Cache.Entry(); + byte[] data = new byte[length]; + new Random(42).nextBytes(data); // explicit seed for reproducible results + entry.data = data; + return entry; + } + + private File[] listCachedFiles() { + return temporaryFolder.getRoot().listFiles(); + } + + private int getEntrySizeOnDisk(String key) { + // Header size is: + // 4 bytes for magic int + // 8 + len(key) bytes for key (long length) + // 8 bytes for etag (long length + 0 characters) + // 32 bytes for serverDate, lastModified, ttl, and softTtl longs + // 4 bytes for length of header list int + // == 56 + len(key) bytes total. + return 56 + key.length(); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java b/core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java new file mode 100644 index 0000000..2a451dc --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.android.volley.Request.Method; +import com.android.volley.mock.TestRequest; +import com.android.volley.toolbox.HttpClientStack.HttpPatch; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpTrace; +import org.apache.http.client.methods.HttpUriRequest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class HttpClientStackTest { + + @Test + public void createDeprecatedGetRequest() throws Exception { + TestRequest.DeprecatedGet request = new TestRequest.DeprecatedGet(); + assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpGet); + } + + @Test + public void createDeprecatedPostRequest() throws Exception { + TestRequest.DeprecatedPost request = new TestRequest.DeprecatedPost(); + assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPost); + } + + @Test + public void createGetRequest() throws Exception { + TestRequest.Get request = new TestRequest.Get(); + assertEquals(request.getMethod(), Method.GET); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpGet); + } + + @Test + public void createPostRequest() throws Exception { + TestRequest.Post request = new TestRequest.Post(); + assertEquals(request.getMethod(), Method.POST); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPost); + } + + @Test + public void createPostRequestWithBody() throws Exception { + TestRequest.PostWithBody request = new TestRequest.PostWithBody(); + assertEquals(request.getMethod(), Method.POST); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPost); + } + + @Test + public void createPutRequest() throws Exception { + TestRequest.Put request = new TestRequest.Put(); + assertEquals(request.getMethod(), Method.PUT); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPut); + } + + @Test + public void createPutRequestWithBody() throws Exception { + TestRequest.PutWithBody request = new TestRequest.PutWithBody(); + assertEquals(request.getMethod(), Method.PUT); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPut); + } + + @Test + public void createDeleteRequest() throws Exception { + TestRequest.Delete request = new TestRequest.Delete(); + assertEquals(request.getMethod(), Method.DELETE); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpDelete); + } + + @Test + public void createHeadRequest() throws Exception { + TestRequest.Head request = new TestRequest.Head(); + assertEquals(request.getMethod(), Method.HEAD); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpHead); + } + + @Test + public void createOptionsRequest() throws Exception { + TestRequest.Options request = new TestRequest.Options(); + assertEquals(request.getMethod(), Method.OPTIONS); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpOptions); + } + + @Test + public void createTraceRequest() throws Exception { + TestRequest.Trace request = new TestRequest.Trace(); + assertEquals(request.getMethod(), Method.TRACE); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpTrace); + } + + @Test + public void createPatchRequest() throws Exception { + TestRequest.Patch request = new TestRequest.Patch(); + assertEquals(request.getMethod(), Method.PATCH); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPatch); + } + + @Test + public void createPatchRequestWithBody() throws Exception { + TestRequest.PatchWithBody request = new TestRequest.PatchWithBody(); + assertEquals(request.getMethod(), Method.PATCH); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPatch); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java new file mode 100644 index 0000000..7780c3e --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.android.volley.Cache; +import com.android.volley.Header; +import com.android.volley.NetworkResponse; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class HttpHeaderParserTest { + + private static long ONE_MINUTE_MILLIS = 1000L * 60; + private static long ONE_HOUR_MILLIS = 1000L * 60 * 60; + private static long ONE_DAY_MILLIS = ONE_HOUR_MILLIS * 24; + private static long ONE_WEEK_MILLIS = ONE_DAY_MILLIS * 7; + + private NetworkResponse response; + private Map headers; + + @Before + public void setUp() throws Exception { + headers = new HashMap(); + response = new NetworkResponse(0, null, headers, false); + } + + @Test + public void parseCacheHeaders_noHeaders() { + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEquals(0, entry.serverDate); + assertEquals(0, entry.lastModified); + assertEquals(0, entry.ttl); + assertEquals(0, entry.softTtl); + } + + @Test + public void parseCacheHeaders_nullHeaders() { + response = new NetworkResponse(0, null, null, false); + assertNull(HttpHeaderParser.parseCacheHeaders(response)); + } + + @Test + public void parseCacheHeaders_headersSet() { + headers.put("MyCustomHeader", "42"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNotNull(entry.responseHeaders); + assertEquals(1, entry.responseHeaders.size()); + assertEquals("42", entry.responseHeaders.get("MyCustomHeader")); + } + + @Test + public void parseCacheHeaders_etag() { + headers.put("ETag", "Yow!"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertEquals("Yow!", entry.etag); + } + + @Test + public void parseCacheHeaders_normalExpire() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Last-Modified", rfc1123Date(now - ONE_DAY_MILLIS)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS); + assertEqualsWithin(entry.lastModified, (now - ONE_DAY_MILLIS), ONE_MINUTE_MILLIS); + assertTrue(entry.softTtl >= (now + ONE_HOUR_MILLIS)); + assertTrue(entry.ttl == entry.softTtl); + } + + @Test + public void parseCacheHeaders_expiresInPast() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now - ONE_HOUR_MILLIS)); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS); + assertEquals(0, entry.ttl); + assertEquals(0, entry.softTtl); + } + + @Test + public void parseCacheHeaders_serverRelative() { + + long now = System.currentTimeMillis(); + // Set "current" date as one hour in the future + headers.put("Date", rfc1123Date(now + ONE_HOUR_MILLIS)); + // TTL four hours in the future, so should be three hours from now + headers.put("Expires", rfc1123Date(now + 4 * ONE_HOUR_MILLIS)); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertEqualsWithin(now + 3 * ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + @Test + public void parseCacheHeaders_cacheControlOverridesExpires() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headers.put("Cache-Control", "public, max-age=86400"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + @Test + public void testParseCacheHeaders_staleWhileRevalidate() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + + // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day + // - stale-while-revalidate (entry.ttl) indicates that the asset may + // continue to be served stale for up to additional 7 days + headers.put("Cache-Control", "max-age=86400, stale-while-revalidate=604800"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS); + assertEqualsWithin(now + ONE_DAY_MILLIS + ONE_WEEK_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + } + + @Test + public void parseCacheHeaders_cacheControlNoCache() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headers.put("Cache-Control", "no-cache"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNull(entry); + } + + @Test + public void parseCacheHeaders_cacheControlMustRevalidateNoMaxAge() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headers.put("Cache-Control", "must-revalidate"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + @Test + public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAge() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headers.put("Cache-Control", "must-revalidate, max-age=3600"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now + ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + @Test + public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAgeAndStale() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + + // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day + // - stale-while-revalidate (entry.ttl) indicates that the asset may + // continue to be served stale for up to additional 7 days, but this is + // ignored in this case because of the must-revalidate header. + headers.put( + "Cache-Control", "must-revalidate, max-age=86400, stale-while-revalidate=604800"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + private void assertEqualsWithin(long expected, long value, long fudgeFactor) { + long diff = Math.abs(expected - value); + assertTrue(diff < fudgeFactor); + } + + private static String rfc1123Date(long millis) { + DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH); + return df.format(new Date(millis)); + } + + // -------------------------- + + @Test + public void parseCharset() { + // Like the ones we usually see + headers.put("Content-Type", "text/plain; charset=utf-8"); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); + + // Charset specified, ignore default charset + headers.put("Content-Type", "text/plain; charset=utf-8"); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "ISO-8859-1")); + + // Extra whitespace + headers.put("Content-Type", "text/plain; charset=utf-8 "); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); + + // Extra parameters + headers.put("Content-Type", "text/plain; charset=utf-8; frozzle=bar"); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); + + // No Content-Type header + headers.clear(); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + + // No Content-Type header, use default charset + headers.clear(); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "utf-8")); + + // Empty value + headers.put("Content-Type", "text/plain; charset="); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + + // None specified + headers.put("Content-Type", "text/plain"); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + + // None charset specified, use default charset + headers.put("Content-Type", "application/json"); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "utf-8")); + + // None specified, extra semicolon + headers.put("Content-Type", "text/plain;"); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + + // No headers, use default charset + assertEquals("utf-8", HttpHeaderParser.parseCharset(null, "utf-8")); + } + + @Test + public void parseCaseInsensitive() { + long now = System.currentTimeMillis(); + + List
headers = new ArrayList<>(); + headers.add(new Header("eTAG", "Yow!")); + headers.add(new Header("DATE", rfc1123Date(now))); + headers.add(new Header("expires", rfc1123Date(now + ONE_HOUR_MILLIS))); + headers.add(new Header("cache-control", "public, max-age=86400")); + headers.add(new Header("content-type", "text/plain")); + + NetworkResponse response = new NetworkResponse(0, null, false, 0, headers); + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertEquals("Yow!", entry.etag); + assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + assertEquals( + "ISO-8859-1", HttpHeaderParser.parseCharset(HttpHeaderParser.toHeaderMap(headers))); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java b/core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java new file mode 100644 index 0000000..6794af8 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java @@ -0,0 +1,192 @@ +package com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import com.android.volley.Request; +import com.android.volley.RetryPolicy; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.http.Header; +import org.apache.http.HttpRequest; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpUriRequest; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; + +/** Tests to validate that HttpStack implementations conform with expected behavior. */ +@RunWith(RobolectricTestRunner.class) +public class HttpStackConformanceTest { + @Mock private RetryPolicy mMockRetryPolicy; + @Mock private Request mMockRequest; + + @Mock private HttpURLConnection mMockConnection; + @Mock private OutputStream mMockOutputStream; + @Spy private HurlStack mHurlStack = new HurlStack(); + + @Mock private HttpClient mMockHttpClient; + private HttpClientStack mHttpClientStack; + + private final TestCase[] mTestCases = + new TestCase[] { + // TestCase for HurlStack. + new TestCase() { + @Override + public HttpStack getStack() { + return mHurlStack; + } + + @Override + public void setOutputHeaderMap(final Map outputHeaderMap) { + doAnswer( + new Answer() { + @Override + public Void answer(InvocationOnMock invocation) { + outputHeaderMap.put( + invocation.getArgument(0), + invocation.getArgument(1)); + return null; + } + }) + .when(mMockConnection) + .setRequestProperty(anyString(), anyString()); + doAnswer( + new Answer>>() { + @Override + public Map> answer( + InvocationOnMock invocation) { + Map> result = new HashMap<>(); + for (Map.Entry entry : + outputHeaderMap.entrySet()) { + result.put( + entry.getKey(), + Collections.singletonList( + entry.getValue())); + } + return result; + } + }) + .when(mMockConnection) + .getRequestProperties(); + } + }, + + // TestCase for HttpClientStack. + new TestCase() { + @Override + public HttpStack getStack() { + return mHttpClientStack; + } + + @Override + public void setOutputHeaderMap(final Map outputHeaderMap) { + try { + doAnswer( + new Answer() { + @Override + public Void answer(InvocationOnMock invocation) + throws Throwable { + HttpRequest request = invocation.getArgument(0); + for (Header header : request.getAllHeaders()) { + if (outputHeaderMap.containsKey( + header.getName())) { + fail( + "Multiple values for header " + + header.getName()); + } + outputHeaderMap.put( + header.getName(), + header.getValue()); + } + return null; + } + }) + .when(mMockHttpClient) + .execute(any(HttpUriRequest.class)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + }; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mHttpClientStack = spy(new HttpClientStack(mMockHttpClient)); + + doReturn(mMockConnection).when(mHurlStack).createConnection(any(URL.class)); + doReturn(mMockOutputStream).when(mMockConnection).getOutputStream(); + when(mMockRequest.getUrl()).thenReturn("http://127.0.0.1"); + when(mMockRequest.getRetryPolicy()).thenReturn(mMockRetryPolicy); + } + + @Test + public void headerPrecedence() throws Exception { + Map additionalHeaders = new HashMap<>(); + additionalHeaders.put("A", "AddlA"); + additionalHeaders.put("B", "AddlB"); + + Map requestHeaders = new HashMap<>(); + requestHeaders.put("A", "RequestA"); + requestHeaders.put("C", "RequestC"); + when(mMockRequest.getHeaders()).thenReturn(requestHeaders); + + when(mMockRequest.getMethod()).thenReturn(Request.Method.POST); + when(mMockRequest.getBody()).thenReturn(new byte[0]); + when(mMockRequest.getBodyContentType()).thenReturn("BodyContentType"); + + for (TestCase testCase : mTestCases) { + // Test once without a Content-Type header in getHeaders(). + Map combinedHeaders = new HashMap<>(); + testCase.setOutputHeaderMap(combinedHeaders); + + testCase.getStack().performRequest(mMockRequest, additionalHeaders); + + Map expectedHeaders = new HashMap<>(); + expectedHeaders.put("A", "RequestA"); + expectedHeaders.put("B", "AddlB"); + expectedHeaders.put("C", "RequestC"); + expectedHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "BodyContentType"); + + assertEquals(expectedHeaders, combinedHeaders); + + // Reset and test again with a Content-Type header in getHeaders(). + combinedHeaders.clear(); + + requestHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "RequestContentType"); + expectedHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "RequestContentType"); + + testCase.getStack().performRequest(mMockRequest, additionalHeaders); + assertEquals(expectedHeaders, combinedHeaders); + + // Clear the Content-Type header for the next TestCase. + requestHeaders.remove(HttpHeaderParser.HEADER_CONTENT_TYPE); + } + } + + private interface TestCase { + HttpStack getStack(); + + void setOutputHeaderMap(Map outputHeaderMap); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/HurlStackTest.java b/core/src/test/java/com/android/volley/toolbox/HurlStackTest.java new file mode 100644 index 0000000..7508244 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/HurlStackTest.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.android.volley.Header; +import com.android.volley.Request; +import com.android.volley.Request.Method; +import com.android.volley.mock.TestRequest; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class HurlStackTest { + + @Mock private HttpURLConnection mMockConnection; + private HurlStack mHurlStack; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(mMockConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + + mHurlStack = + new HurlStack() { + @Override + protected HttpURLConnection createConnection(URL url) { + return mMockConnection; + } + + @Override + protected InputStream createInputStream( + Request request, HttpURLConnection connection) { + return new MonitoringInputStream( + super.createInputStream(request, connection)); + } + + @Override + protected OutputStream createOutputStream( + Request request, HttpURLConnection connection, int length) + throws IOException { + if (request instanceof MonitoredRequest) { + return new MonitoringOutputStream( + super.createOutputStream(request, connection, length), + (MonitoredRequest) request, + length); + } + return super.createOutputStream(request, connection, length); + } + }; + } + + @Test + public void connectionForDeprecatedGetRequest() throws Exception { + TestRequest.DeprecatedGet request = new TestRequest.DeprecatedGet(); + assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection, never()).setRequestMethod(anyString()); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForDeprecatedPostRequest() throws Exception { + TestRequest.DeprecatedPost request = new TestRequest.DeprecatedPost(); + assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("POST"); + verify(mMockConnection).setDoOutput(true); + } + + @Test + public void connectionForGetRequest() throws Exception { + TestRequest.Get request = new TestRequest.Get(); + assertEquals(request.getMethod(), Method.GET); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("GET"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForPostRequest() throws Exception { + TestRequest.Post request = new TestRequest.Post(); + assertEquals(request.getMethod(), Method.POST); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("POST"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForPostWithBodyRequest() throws Exception { + TestRequest.PostWithBody request = new TestRequest.PostWithBody(); + assertEquals(request.getMethod(), Method.POST); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("POST"); + verify(mMockConnection).setDoOutput(true); + } + + @Test + public void connectionForPutRequest() throws Exception { + TestRequest.Put request = new TestRequest.Put(); + assertEquals(request.getMethod(), Method.PUT); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("PUT"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForPutWithBodyRequest() throws Exception { + TestRequest.PutWithBody request = new TestRequest.PutWithBody(); + assertEquals(request.getMethod(), Method.PUT); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("PUT"); + verify(mMockConnection).setDoOutput(true); + } + + @Test + public void connectionForDeleteRequest() throws Exception { + TestRequest.Delete request = new TestRequest.Delete(); + assertEquals(request.getMethod(), Method.DELETE); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("DELETE"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForHeadRequest() throws Exception { + TestRequest.Head request = new TestRequest.Head(); + assertEquals(request.getMethod(), Method.HEAD); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("HEAD"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForOptionsRequest() throws Exception { + TestRequest.Options request = new TestRequest.Options(); + assertEquals(request.getMethod(), Method.OPTIONS); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("OPTIONS"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForTraceRequest() throws Exception { + TestRequest.Trace request = new TestRequest.Trace(); + assertEquals(request.getMethod(), Method.TRACE); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("TRACE"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForPatchRequest() throws Exception { + TestRequest.Patch request = new TestRequest.Patch(); + assertEquals(request.getMethod(), Method.PATCH); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("PATCH"); + verify(mMockConnection, never()).setDoOutput(true); + } + + @Test + public void connectionForPatchWithBodyRequest() throws Exception { + TestRequest.PatchWithBody request = new TestRequest.PatchWithBody(); + assertEquals(request.getMethod(), Method.PATCH); + + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); + verify(mMockConnection).setRequestMethod("PATCH"); + verify(mMockConnection).setDoOutput(true); + } + + @Test + public void executeRequestClosesConnection_connectionError() throws Exception { + when(mMockConnection.getResponseCode()).thenThrow(new SocketTimeoutException()); + try { + mHurlStack.executeRequest( + new TestRequest.Get(), Collections.emptyMap()); + fail("Should have thrown exception"); + } catch (IOException e) { + verify(mMockConnection).disconnect(); + } + } + + @Test + public void executeRequestClosesConnection_invalidResponseCode() throws Exception { + when(mMockConnection.getResponseCode()).thenReturn(-1); + try { + mHurlStack.executeRequest( + new TestRequest.Get(), Collections.emptyMap()); + fail("Should have thrown exception"); + } catch (IOException e) { + verify(mMockConnection).disconnect(); + } + } + + @Test + public void executeRequestClosesConnection_noResponseBody() throws Exception { + when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NO_CONTENT); + mHurlStack.executeRequest(new TestRequest.Get(), Collections.emptyMap()); + verify(mMockConnection).disconnect(); + } + + @Test + public void executeRequestClosesConnection_hasResponseBody() throws Exception { + when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mMockConnection.getInputStream()) + .thenReturn(new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8))); + HttpResponse response = + mHurlStack.executeRequest( + new TestRequest.Get(), Collections.emptyMap()); + // Shouldn't be disconnected until the stream is consumed. + verify(mMockConnection, never()).disconnect(); + response.getContent().close(); + verify(mMockConnection).disconnect(); + } + + @Test + public void convertHeaders() { + Map> headers = new HashMap<>(); + headers.put(null, Collections.singletonList("Ignored")); + headers.put("HeaderA", Collections.singletonList("ValueA")); + List values = new ArrayList<>(); + values.add("ValueB_1"); + values.add("ValueB_2"); + headers.put("HeaderB", values); + List
result = HurlStack.convertHeaders(headers); + List
expected = new ArrayList<>(); + expected.add(new Header("HeaderA", "ValueA")); + expected.add(new Header("HeaderB", "ValueB_1")); + expected.add(new Header("HeaderB", "ValueB_2")); + assertEquals(expected, result); + } + + @Test + public void interceptResponseStream() throws Exception { + when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mMockConnection.getInputStream()) + .thenReturn(new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8))); + HttpResponse response = + mHurlStack.executeRequest( + new TestRequest.Get(), Collections.emptyMap()); + assertTrue(response.getContent() instanceof MonitoringInputStream); + } + + @Test + public void interceptRequestStream() throws Exception { + MonitoredRequest request = new MonitoredRequest(); + mHurlStack.executeRequest(request, Collections.emptyMap()); + assertTrue(request.totalRequestBytes > 0); + assertEquals(request.totalRequestBytes, request.requestBytesRead); + } + + private static class MonitoringInputStream extends FilterInputStream { + private MonitoringInputStream(InputStream in) { + super(in); + } + } + + private static class MonitoringOutputStream extends FilterOutputStream { + private MonitoredRequest request; + + private MonitoringOutputStream(OutputStream out, MonitoredRequest request, int length) { + super(out); + this.request = request; + this.request.totalRequestBytes = length; + } + + @Override + public void write(int b) throws IOException { + this.request.requestBytesRead++; + out.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + this.request.requestBytesRead += len; + out.write(b, off, len); + } + } + + private static class MonitoredRequest extends TestRequest.PostWithBody { + int requestBytesRead = 0; + int totalRequestBytes = 0; + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java b/core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java new file mode 100644 index 0000000..59a0b1b --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.Bitmap; +import android.widget.ImageView; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ImageLoaderTest { + private RequestQueue mRequestQueue; + private ImageLoader.ImageCache mImageCache; + private ImageLoader mImageLoader; + + @Before + public void setUp() { + mRequestQueue = mock(RequestQueue.class); + mImageCache = mock(ImageLoader.ImageCache.class); + mImageLoader = new ImageLoader(mRequestQueue, mImageCache); + } + + @Test + public void isCachedChecksCache() throws Exception { + when(mImageCache.getBitmap(anyString())).thenReturn(null); + Assert.assertFalse(mImageLoader.isCached("http://foo", 0, 0)); + } + + @Test + public void getWithCacheHit() throws Exception { + Bitmap bitmap = Bitmap.createBitmap(1, 1, null); + ImageLoader.ImageListener listener = mock(ImageLoader.ImageListener.class); + when(mImageCache.getBitmap(anyString())).thenReturn(bitmap); + ImageLoader.ImageContainer ic = mImageLoader.get("http://foo", listener); + Assert.assertSame(bitmap, ic.getBitmap()); + verify(listener).onResponse(ic, true); + } + + @Test + public void getWithCacheMiss() throws Exception { + when(mImageCache.getBitmap(anyString())).thenReturn(null); + ImageLoader.ImageListener listener = mock(ImageLoader.ImageListener.class); + // Ask for the image to be loaded. + mImageLoader.get("http://foo", listener); + // Second pass to test deduping logic. + mImageLoader.get("http://foo", listener); + // Response callback should be called both times. + verify(listener, times(2)).onResponse(any(ImageLoader.ImageContainer.class), eq(true)); + // But request should be enqueued only once. + verify(mRequestQueue, times(1)).add(Mockito.>any()); + } + + @Test + public void publicMethods() throws Exception { + // Catch API breaking changes. + ImageLoader.getImageListener(null, -1, -1); + mImageLoader.setBatchedResponseDelay(1000); + + assertNotNull( + ImageLoader.class.getConstructor(RequestQueue.class, ImageLoader.ImageCache.class)); + + assertNotNull( + ImageLoader.class.getMethod( + "getImageListener", ImageView.class, int.class, int.class)); + assertNotNull(ImageLoader.class.getMethod("isCached", String.class, int.class, int.class)); + assertNotNull( + ImageLoader.class.getMethod( + "isCached", String.class, int.class, int.class, ImageView.ScaleType.class)); + assertNotNull( + ImageLoader.class.getMethod("get", String.class, ImageLoader.ImageListener.class)); + assertNotNull( + ImageLoader.class.getMethod( + "get", + String.class, + ImageLoader.ImageListener.class, + int.class, + int.class)); + assertNotNull( + ImageLoader.class.getMethod( + "get", + String.class, + ImageLoader.ImageListener.class, + int.class, + int.class, + ImageView.ScaleType.class)); + assertNotNull(ImageLoader.class.getMethod("setBatchedResponseDelay", int.class)); + + assertNotNull( + ImageLoader.ImageListener.class.getMethod( + "onResponse", ImageLoader.ImageContainer.class, boolean.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java b/core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java new file mode 100644 index 0000000..6b50319 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import com.android.volley.NetworkResponse; +import com.android.volley.Response; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowBitmapFactory; + +@RunWith(RobolectricTestRunner.class) +public class ImageRequestTest { + + @Test + public void parseNetworkResponse_resizing() throws Exception { + // This is a horrible hack but Robolectric doesn't have a way to provide + // width and height hints for decodeByteArray. It works because the byte array + // "file:fake" is ASCII encodable and thus the name in Robolectric's fake + // bitmap creator survives as-is, and provideWidthAndHeightHints puts + // "file:" + name in its lookaside map. I write all this because it will + // probably break mysteriously at some point and I feel terrible about your + // having to debug it. + byte[] jpegBytes = "file:fake".getBytes(StandardCharsets.UTF_8); + ShadowBitmapFactory.provideWidthAndHeightHints("fake", 1024, 500); + NetworkResponse jpeg = new NetworkResponse(jpegBytes); + + // Scale the image uniformly (maintain the image's aspect ratio) so that + // both dimensions (width and height) of the image will be equal to or + // less than the corresponding dimension of the view. + ScaleType scalteType = ScaleType.CENTER_INSIDE; + + // Exact sizes + verifyResize(jpeg, 512, 250, scalteType, 512, 250); // exactly half + verifyResize(jpeg, 511, 249, scalteType, 509, 249); // just under half + verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); // larger + verifyResize(jpeg, 500, 500, scalteType, 500, 244); // keep same ratio + + // Specify only width, preserve aspect ratio + verifyResize(jpeg, 512, 0, scalteType, 512, 250); + verifyResize(jpeg, 800, 0, scalteType, 800, 390); + verifyResize(jpeg, 1024, 0, scalteType, 1024, 500); + + // Specify only height, preserve aspect ratio + verifyResize(jpeg, 0, 250, scalteType, 512, 250); + verifyResize(jpeg, 0, 391, scalteType, 800, 391); + verifyResize(jpeg, 0, 500, scalteType, 1024, 500); + + // No resize + verifyResize(jpeg, 0, 0, scalteType, 1024, 500); + + // Scale the image uniformly (maintain the image's aspect ratio) so that + // both dimensions (width and height) of the image will be equal to or + // larger than the corresponding dimension of the view. + scalteType = ScaleType.CENTER_CROP; + + // Exact sizes + verifyResize(jpeg, 512, 250, scalteType, 512, 250); + verifyResize(jpeg, 511, 249, scalteType, 511, 249); + verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); + verifyResize(jpeg, 500, 500, scalteType, 1024, 500); + + // Specify only width + verifyResize(jpeg, 512, 0, scalteType, 512, 250); + verifyResize(jpeg, 800, 0, scalteType, 800, 390); + verifyResize(jpeg, 1024, 0, scalteType, 1024, 500); + + // Specify only height + verifyResize(jpeg, 0, 250, scalteType, 512, 250); + verifyResize(jpeg, 0, 391, scalteType, 800, 391); + verifyResize(jpeg, 0, 500, scalteType, 1024, 500); + + // No resize + verifyResize(jpeg, 0, 0, scalteType, 1024, 500); + + // Scale in X and Y independently, so that src matches dst exactly. This + // may change the aspect ratio of the src. + scalteType = ScaleType.FIT_XY; + + // Exact sizes + verifyResize(jpeg, 512, 250, scalteType, 512, 250); + verifyResize(jpeg, 511, 249, scalteType, 511, 249); + verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); + verifyResize(jpeg, 500, 500, scalteType, 500, 500); + + // Specify only width + verifyResize(jpeg, 512, 0, scalteType, 512, 500); + verifyResize(jpeg, 800, 0, scalteType, 800, 500); + verifyResize(jpeg, 1024, 0, scalteType, 1024, 500); + + // Specify only height + verifyResize(jpeg, 0, 250, scalteType, 1024, 250); + verifyResize(jpeg, 0, 391, scalteType, 1024, 391); + verifyResize(jpeg, 0, 500, scalteType, 1024, 500); + + // No resize + verifyResize(jpeg, 0, 0, scalteType, 1024, 500); + } + + private void verifyResize( + NetworkResponse networkResponse, + int maxWidth, + int maxHeight, + ScaleType scaleType, + int expectedWidth, + int expectedHeight) { + ImageRequest request = + new ImageRequest("", null, maxWidth, maxHeight, scaleType, Config.RGB_565, null); + Response response = request.parseNetworkResponse(networkResponse); + assertNotNull(response); + assertTrue(response.isSuccess()); + Bitmap bitmap = response.result; + assertNotNull(bitmap); + assertEquals(expectedWidth, bitmap.getWidth()); + assertEquals(expectedHeight, bitmap.getHeight()); + } + + @Test + public void findBestSampleSize() { + // desired == actual == 1 + assertEquals(1, ImageRequest.findBestSampleSize(100, 150, 100, 150)); + + // exactly half == 2 + assertEquals(2, ImageRequest.findBestSampleSize(280, 160, 140, 80)); + + // just over half == 1 + assertEquals(1, ImageRequest.findBestSampleSize(1000, 800, 501, 401)); + + // just under 1/4 == 4 + assertEquals(4, ImageRequest.findBestSampleSize(100, 200, 24, 50)); + } + + private static byte[] readInputStream(InputStream in) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int count; + while ((count = in.read(buffer)) != -1) { + bytes.write(buffer, 0, count); + } + in.close(); + return bytes.toByteArray(); + } + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull( + ImageRequest.class.getConstructor( + String.class, + Response.Listener.class, + int.class, + int.class, + Bitmap.Config.class, + Response.ErrorListener.class)); + assertNotNull( + ImageRequest.class.getConstructor( + String.class, + Response.Listener.class, + int.class, + int.class, + ImageView.ScaleType.class, + Bitmap.Config.class, + Response.ErrorListener.class)); + assertEquals(ImageRequest.DEFAULT_IMAGE_TIMEOUT_MS, 1000); + assertEquals(ImageRequest.DEFAULT_IMAGE_MAX_RETRIES, 2); + assertEquals(ImageRequest.DEFAULT_IMAGE_BACKOFF_MULT, 2f, 0); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java b/core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java new file mode 100644 index 0000000..70bb2ea --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.android.volley.NetworkResponse; +import com.android.volley.Response; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class JsonRequestCharsetTest { + + /** String in Czech - "Retezec v cestine." */ + private static final String TEXT_VALUE = "\u0158et\u011bzec v \u010de\u0161tin\u011b."; + + private static final String TEXT_NAME = "text"; + private static final int TEXT_INDEX = 0; + + /** + * Copyright symbol has different encoding in utf-8 and ISO-8859-1, and it doesn't exists in + * ISO-8859-2 + */ + private static final String COPY_VALUE = "\u00a9"; + + private static final String COPY_NAME = "copyright"; + private static final int COPY_INDEX = 1; + + @Test + public void defaultCharsetJsonObject() throws Exception { + // UTF-8 is default charset for JSON + byte[] data = jsonObjectString().getBytes(Charset.forName("UTF-8")); + NetworkResponse network = new NetworkResponse(data); + JsonObjectRequest objectRequest = new JsonObjectRequest("", null, null, null); + Response objectResponse = objectRequest.parseNetworkResponse(network); + + assertNotNull(objectResponse); + assertTrue(objectResponse.isSuccess()); + assertEquals(TEXT_VALUE, objectResponse.result.getString(TEXT_NAME)); + assertEquals(COPY_VALUE, objectResponse.result.getString(COPY_NAME)); + } + + @Test + public void defaultCharsetJsonArray() throws Exception { + // UTF-8 is default charset for JSON + byte[] data = jsonArrayString().getBytes(Charset.forName("UTF-8")); + NetworkResponse network = new NetworkResponse(data); + JsonArrayRequest arrayRequest = new JsonArrayRequest("", null, null); + Response arrayResponse = arrayRequest.parseNetworkResponse(network); + + assertNotNull(arrayResponse); + assertTrue(arrayResponse.isSuccess()); + assertEquals(TEXT_VALUE, arrayResponse.result.getString(TEXT_INDEX)); + assertEquals(COPY_VALUE, arrayResponse.result.getString(COPY_INDEX)); + } + + @Test + public void specifiedCharsetJsonObject() throws Exception { + byte[] data = jsonObjectString().getBytes(Charset.forName("ISO-8859-1")); + Map headers = new HashMap(); + headers.put("Content-Type", "application/json; charset=iso-8859-1"); + NetworkResponse network = new NetworkResponse(data, headers); + JsonObjectRequest objectRequest = new JsonObjectRequest("", null, null, null); + Response objectResponse = objectRequest.parseNetworkResponse(network); + + assertNotNull(objectResponse); + assertTrue(objectResponse.isSuccess()); + // don't check the text in Czech, ISO-8859-1 doesn't support some Czech characters + assertEquals(COPY_VALUE, objectResponse.result.getString(COPY_NAME)); + } + + @Test + public void specifiedCharsetJsonArray() throws Exception { + byte[] data = jsonArrayString().getBytes(Charset.forName("ISO-8859-2")); + Map headers = new HashMap(); + headers.put("Content-Type", "application/json; charset=iso-8859-2"); + NetworkResponse network = new NetworkResponse(data, headers); + JsonArrayRequest arrayRequest = new JsonArrayRequest("", null, null); + Response arrayResponse = arrayRequest.parseNetworkResponse(network); + + assertNotNull(arrayResponse); + assertTrue(arrayResponse.isSuccess()); + assertEquals(TEXT_VALUE, arrayResponse.result.getString(TEXT_INDEX)); + // don't check the copyright symbol, ISO-8859-2 doesn't have it, but it has Czech characters + } + + private static String jsonObjectString() throws Exception { + JSONObject json = new JSONObject().put(TEXT_NAME, TEXT_VALUE).put(COPY_NAME, COPY_VALUE); + return json.toString(); + } + + private static String jsonArrayString() throws Exception { + JSONArray json = new JSONArray().put(TEXT_INDEX, TEXT_VALUE).put(COPY_INDEX, COPY_VALUE); + return json.toString(); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java b/core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java new file mode 100644 index 0000000..44c0ad9 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Response; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class JsonRequestTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull( + JsonRequest.class.getConstructor( + String.class, + String.class, + Response.Listener.class, + Response.ErrorListener.class)); + assertNotNull( + JsonRequest.class.getConstructor( + int.class, + String.class, + String.class, + Response.Listener.class, + Response.ErrorListener.class)); + + assertNotNull( + JsonArrayRequest.class.getConstructor( + String.class, Response.Listener.class, Response.ErrorListener.class)); + assertNotNull( + JsonArrayRequest.class.getConstructor( + int.class, + String.class, + JSONArray.class, + Response.Listener.class, + Response.ErrorListener.class)); + + assertNotNull( + JsonObjectRequest.class.getConstructor( + String.class, + JSONObject.class, + Response.Listener.class, + Response.ErrorListener.class)); + assertNotNull( + JsonObjectRequest.class.getConstructor( + int.class, + String.class, + JSONObject.class, + Response.Listener.class, + Response.ErrorListener.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java b/core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java new file mode 100644 index 0000000..fd2073e --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.ViewGroup.LayoutParams; +import android.widget.ImageView.ScaleType; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class NetworkImageViewTest { + private NetworkImageView mNIV; + private MockImageLoader mMockImageLoader; + + @Before + public void setUp() throws Exception { + mMockImageLoader = new MockImageLoader(); + mNIV = new NetworkImageView(RuntimeEnvironment.application); + } + + @Test + public void setImageUrl_requestsImage() { + mNIV.setLayoutParams( + new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + mNIV.setImageUrl("http://foo", mMockImageLoader); + assertEquals("http://foo", mMockImageLoader.lastRequestUrl); + assertEquals(0, mMockImageLoader.lastMaxWidth); + assertEquals(0, mMockImageLoader.lastMaxHeight); + } + + // public void testSetImageUrl_setsMaxSize() { + // // TODO: Not sure how to make getWidth() return something from an + // // instrumentation test. Write this test once it's figured out. + // } + + private static class MockImageLoader extends ImageLoader { + public MockImageLoader() { + super(null, null); + } + + public String lastRequestUrl; + public int lastMaxWidth; + public int lastMaxHeight; + + @Override + public ImageContainer get( + String requestUrl, + ImageListener imageListener, + int maxWidth, + int maxHeight, + ScaleType scaleType) { + lastRequestUrl = requestUrl; + lastMaxWidth = maxWidth; + lastMaxHeight = maxHeight; + return null; + } + } + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(NetworkImageView.class.getConstructor(Context.class)); + assertNotNull(NetworkImageView.class.getConstructor(Context.class, AttributeSet.class)); + assertNotNull( + NetworkImageView.class.getConstructor( + Context.class, AttributeSet.class, int.class)); + + assertNotNull( + NetworkImageView.class.getMethod("setImageUrl", String.class, ImageLoader.class)); + assertNotNull(NetworkImageView.class.getMethod("setDefaultImageDrawable", Drawable.class)); + assertNotNull(NetworkImageView.class.getMethod("setDefaultImageBitmap", Bitmap.class)); + assertNotNull(NetworkImageView.class.getMethod("setDefaultImageResId", int.class)); + assertNotNull(NetworkImageView.class.getMethod("setErrorImageDrawable", Drawable.class)); + assertNotNull(NetworkImageView.class.getMethod("setErrorImageBitmap", Bitmap.class)); + assertNotNull(NetworkImageView.class.getMethod("setErrorImageResId", int.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java b/core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java new file mode 100644 index 0000000..266edcd --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Arrays; +import org.junit.Test; + +public class PoolingByteArrayOutputStreamTest { + @Test + public void pooledOneBuffer() throws IOException { + ByteArrayPool pool = new ByteArrayPool(32768); + writeOneBuffer(pool); + writeOneBuffer(pool); + writeOneBuffer(pool); + } + + @Test + public void pooledIndividualWrites() throws IOException { + ByteArrayPool pool = new ByteArrayPool(32768); + writeBytesIndividually(pool); + writeBytesIndividually(pool); + writeBytesIndividually(pool); + } + + @Test + public void unpooled() throws IOException { + ByteArrayPool pool = new ByteArrayPool(0); + writeOneBuffer(pool); + writeOneBuffer(pool); + writeOneBuffer(pool); + } + + @Test + public void unpooledIndividualWrites() throws IOException { + ByteArrayPool pool = new ByteArrayPool(0); + writeBytesIndividually(pool); + writeBytesIndividually(pool); + writeBytesIndividually(pool); + } + + private void writeOneBuffer(ByteArrayPool pool) throws IOException { + byte[] data = new byte[16384]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (i & 0xff); + } + PoolingByteArrayOutputStream os = new PoolingByteArrayOutputStream(pool); + os.write(data); + + assertTrue(Arrays.equals(data, os.toByteArray())); + } + + private void writeBytesIndividually(ByteArrayPool pool) { + byte[] data = new byte[16384]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (i & 0xff); + } + PoolingByteArrayOutputStream os = new PoolingByteArrayOutputStream(pool); + for (int i = 0; i < data.length; i++) { + os.write(data[i]); + } + + assertTrue(Arrays.equals(data, os.toByteArray())); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java b/core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java new file mode 100644 index 0000000..5b5c975 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Request; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class RequestFutureTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(RequestFuture.class.getMethod("newFuture")); + assertNotNull(RequestFuture.class.getMethod("setRequest", Request.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java b/core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java new file mode 100644 index 0000000..1899b71 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Cache; +import com.android.volley.Network; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.ResponseDelivery; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class RequestQueueTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull( + RequestQueue.class.getConstructor( + Cache.class, Network.class, int.class, ResponseDelivery.class)); + assertNotNull(RequestQueue.class.getConstructor(Cache.class, Network.class, int.class)); + assertNotNull(RequestQueue.class.getConstructor(Cache.class, Network.class)); + + assertNotNull(RequestQueue.class.getMethod("start")); + assertNotNull(RequestQueue.class.getMethod("stop")); + assertNotNull(RequestQueue.class.getMethod("getSequenceNumber")); + assertNotNull(RequestQueue.class.getMethod("getCache")); + assertNotNull(RequestQueue.class.getMethod("cancelAll", RequestQueue.RequestFilter.class)); + assertNotNull(RequestQueue.class.getMethod("cancelAll", Object.class)); + assertNotNull(RequestQueue.class.getMethod("add", Request.class)); + assertNotNull(RequestQueue.class.getDeclaredMethod("finish", Request.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/RequestTest.java b/core/src/test/java/com/android/volley/toolbox/RequestTest.java new file mode 100644 index 0000000..0911ad6 --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/RequestTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Cache; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.RetryPolicy; +import com.android.volley.VolleyError; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class RequestTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull( + Request.class.getConstructor( + int.class, String.class, Response.ErrorListener.class)); + + assertNotNull(Request.class.getMethod("getMethod")); + assertNotNull(Request.class.getMethod("setTag", Object.class)); + assertNotNull(Request.class.getMethod("getTag")); + assertNotNull(Request.class.getMethod("getErrorListener")); + assertNotNull(Request.class.getMethod("getTrafficStatsTag")); + assertNotNull(Request.class.getMethod("setRetryPolicy", RetryPolicy.class)); + assertNotNull(Request.class.getMethod("addMarker", String.class)); + assertNotNull(Request.class.getDeclaredMethod("finish", String.class)); + assertNotNull(Request.class.getMethod("setRequestQueue", RequestQueue.class)); + assertNotNull(Request.class.getMethod("setSequence", int.class)); + assertNotNull(Request.class.getMethod("getSequence")); + assertNotNull(Request.class.getMethod("getUrl")); + assertNotNull(Request.class.getMethod("getCacheKey")); + assertNotNull(Request.class.getMethod("setCacheEntry", Cache.Entry.class)); + assertNotNull(Request.class.getMethod("getCacheEntry")); + assertNotNull(Request.class.getMethod("cancel")); + assertNotNull(Request.class.getMethod("isCanceled")); + assertNotNull(Request.class.getMethod("getHeaders")); + assertNotNull(Request.class.getDeclaredMethod("getParams")); + assertNotNull(Request.class.getDeclaredMethod("getParamsEncoding")); + assertNotNull(Request.class.getMethod("getBodyContentType")); + assertNotNull(Request.class.getMethod("getBody")); + assertNotNull(Request.class.getMethod("setShouldCache", boolean.class)); + assertNotNull(Request.class.getMethod("shouldCache")); + assertNotNull(Request.class.getMethod("getPriority")); + assertNotNull(Request.class.getMethod("getTimeoutMs")); + assertNotNull(Request.class.getMethod("getRetryPolicy")); + assertNotNull(Request.class.getMethod("markDelivered")); + assertNotNull(Request.class.getMethod("hasHadResponseDelivered")); + assertNotNull( + Request.class.getDeclaredMethod("parseNetworkResponse", NetworkResponse.class)); + assertNotNull(Request.class.getDeclaredMethod("parseNetworkError", VolleyError.class)); + assertNotNull(Request.class.getDeclaredMethod("deliverResponse", Object.class)); + assertNotNull(Request.class.getMethod("deliverError", VolleyError.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/ResponseTest.java b/core/src/test/java/com/android/volley/toolbox/ResponseTest.java new file mode 100644 index 0000000..44438fa --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/ResponseTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Cache; +import com.android.volley.NetworkResponse; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ResponseTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(Response.class.getMethod("success", Object.class, Cache.Entry.class)); + assertNotNull(Response.class.getMethod("error", VolleyError.class)); + assertNotNull(Response.class.getMethod("isSuccess")); + + assertNotNull(Response.Listener.class.getDeclaredMethod("onResponse", Object.class)); + + assertNotNull( + Response.ErrorListener.class.getDeclaredMethod( + "onErrorResponse", VolleyError.class)); + + assertNotNull( + NetworkResponse.class.getConstructor( + int.class, byte[].class, Map.class, boolean.class, long.class)); + assertNotNull( + NetworkResponse.class.getConstructor( + int.class, byte[].class, Map.class, boolean.class)); + assertNotNull(NetworkResponse.class.getConstructor(byte[].class)); + assertNotNull(NetworkResponse.class.getConstructor(byte[].class, Map.class)); + } +} diff --git a/core/src/test/java/com/android/volley/toolbox/StringRequestTest.java b/core/src/test/java/com/android/volley/toolbox/StringRequestTest.java new file mode 100644 index 0000000..0ecb06b --- /dev/null +++ b/core/src/test/java/com/android/volley/toolbox/StringRequestTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import static org.junit.Assert.assertNotNull; + +import com.android.volley.Response; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class StringRequestTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull( + StringRequest.class.getConstructor( + String.class, Response.Listener.class, Response.ErrorListener.class)); + assertNotNull( + StringRequest.class.getConstructor( + int.class, + String.class, + Response.Listener.class, + Response.ErrorListener.class)); + } +} diff --git a/core/src/test/java/com/android/volley/utils/CacheTestUtils.java b/core/src/test/java/com/android/volley/utils/CacheTestUtils.java new file mode 100644 index 0000000..5980712 --- /dev/null +++ b/core/src/test/java/com/android/volley/utils/CacheTestUtils.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.utils; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import com.android.volley.Cache; +import java.util.Random; + +public class CacheTestUtils { + + /** + * Makes a random cache entry. + * + * @param data Data to use, or null to use random data + * @param isExpired Whether the TTLs should be set such that this entry is expired + * @param needsRefresh Whether the TTLs should be set such that this entry needs refresh + */ + public static Cache.Entry makeRandomCacheEntry( + byte[] data, boolean isExpired, boolean needsRefresh) { + Random random = new Random(); + Cache.Entry entry = new Cache.Entry(); + if (data != null) { + entry.data = data; + } else { + entry.data = new byte[random.nextInt(1024)]; + } + entry.etag = String.valueOf(random.nextLong()); + entry.lastModified = random.nextLong(); + entry.ttl = isExpired ? 0 : Long.MAX_VALUE; + entry.softTtl = needsRefresh ? 0 : Long.MAX_VALUE; + return entry; + } + + /** + * Like {@link #makeRandomCacheEntry(byte[], boolean, boolean)} but defaults to an unexpired + * entry. + */ + public static Cache.Entry makeRandomCacheEntry(byte[] data) { + return makeRandomCacheEntry(data, false, false); + } + + public static void assertThatEntriesAreEqual(Cache.Entry actual, Cache.Entry expected) { + assertNotNull(actual); + assertThat(actual.data, is(equalTo(expected.data))); + assertThat(actual.etag, is(equalTo(expected.etag))); + assertThat(actual.lastModified, is(equalTo(expected.lastModified))); + assertThat(actual.responseHeaders, is(equalTo(expected.responseHeaders))); + assertThat(actual.serverDate, is(equalTo(expected.serverDate))); + assertThat(actual.softTtl, is(equalTo(expected.softTtl))); + assertThat(actual.ttl, is(equalTo(expected.ttl))); + } + + public static Cache.Entry randomData(int length) { + Cache.Entry entry = new Cache.Entry(); + byte[] data = new byte[length]; + new Random(42).nextBytes(data); // explicit seed for reproducible results + entry.data = data; + return entry; + } + + public static int getEntrySizeOnDisk(String key) { + // Header size is: + // 4 bytes for magic int + // 8 + len(key) bytes for key (long length) + // 8 bytes for etag (long length + 0 characters) + // 32 bytes for serverDate, lastModified, ttl, and softTtl longs + // 4 bytes for length of header list int + // == 56 + len(key) bytes total. + return 56 + key.length(); + } +} diff --git a/core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java b/core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java new file mode 100644 index 0000000..67e5923 --- /dev/null +++ b/core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.utils; + +import com.android.volley.ExecutorDelivery; +import java.util.concurrent.Executor; + +/** + * A ResponseDelivery for testing that immediately delivers responses instead of posting back to the + * main thread. + */ +public class ImmediateResponseDelivery extends ExecutorDelivery { + + public ImmediateResponseDelivery() { + super( + new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }); + } +} diff --git a/core/src/test/resources/org.robolectric.Config.properties b/core/src/test/resources/org.robolectric.Config.properties new file mode 100644 index 0000000..9daf692 --- /dev/null +++ b/core/src/test/resources/org.robolectric.Config.properties @@ -0,0 +1 @@ +manifest=src/main/AndroidManifest.xml diff --git a/cronet/build.gradle b/cronet/build.gradle new file mode 100644 index 0000000..5ee53d6 --- /dev/null +++ b/cronet/build.gradle @@ -0,0 +1,24 @@ +dependencies { + implementation project(":core") + implementation "androidx.annotation:annotation:1.0.1" + compileOnly "org.chromium.net:cronet-embedded:76.3809.111" + + testImplementation project(":testing") + testImplementation "org.chromium.net:cronet-embedded:76.3809.111" + testImplementation "junit:junit:4.12" + testImplementation "org.mockito:mockito-core:2.19.0" + testImplementation "org.robolectric:robolectric:3.4.2" +} + +publishing { + publications { + library(MavenPublication) { + artifactId 'volley-cronet' + pom { + name = 'Volley Cronet' + description = 'Cronet support for Volley.' + } + artifact "$buildDir/outputs/aar/cronet-release.aar" + } + } +} diff --git a/cronet/src/main/AndroidManifest.xml b/cronet/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0dec093 --- /dev/null +++ b/cronet/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/cronet/src/main/java/com/android/volley/cronet/CronetHttpStack.java b/cronet/src/main/java/com/android/volley/cronet/CronetHttpStack.java new file mode 100644 index 0000000..874029b --- /dev/null +++ b/cronet/src/main/java/com/android/volley/cronet/CronetHttpStack.java @@ -0,0 +1,634 @@ +/* + * 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. + * + *

WARNING: This API is experimental and subject to breaking changes. Please see + * https://github.com/google/volley/wiki/Asynchronous-Volley for more details. + */ +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 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

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 extends RequestTask { + UrlRequest.Builder builder; + String url; + Map additionalHeaders; + OnRequestComplete callback; + Request request; + + SetUpRequestTask( + Request request, + String url, + UrlRequest.Builder builder, + Map 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
getHeaders(List> headersList) { + List
headers = new ArrayList<>(); + for (Map.Entry 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 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 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 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. + * + *

When enabled, for each request dispatched to the network, a roughly-equivalent cURL + * command will be logged to logcat. + * + *

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}. + * + *

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. + * + *

Allows customization of the logging performed when cURL logging is enabled. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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 mHeaders = + new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + private String mHttpMethod; + @Nullable private byte[] mBody; + + /** + * Return the headers to be used for the request. + * + *

The returned map is case-insensitive. + */ + TreeMap getHeaders() { + return mHeaders; + } + + /** Apply all the headers in the given map to the request. */ + void putAllHeaders(Map 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 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/cronet/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java b/cronet/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java new file mode 100644 index 0000000..cedb6ff --- /dev/null +++ b/cronet/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.cronet; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.android.volley.Header; +import com.android.volley.cronet.CronetHttpStack.CurlCommandLogger; +import com.android.volley.mock.TestRequest; +import com.android.volley.toolbox.AsyncHttpStack.OnRequestComplete; +import com.android.volley.toolbox.UrlRewriter; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import org.chromium.net.CronetEngine; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class CronetHttpStackTest { + @Mock private CurlCommandLogger mMockCurlCommandLogger; + @Mock private OnRequestComplete mMockOnRequestComplete; + @Mock private UrlRewriter mMockUrlRewriter; + + // A fake would be ideal here, but Cronet doesn't (yet) provide one, and at the moment we aren't + // exercising the full response flow. + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private CronetEngine mMockCronetEngine; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void curlLogging_disabled() { + CronetHttpStack stack = + createStack( + new Consumer() { + @Override + public void accept(CronetHttpStack.Builder builder) { + // Default parameters should not enable cURL logging. + } + }); + + stack.executeRequest( + new TestRequest.Get(), ImmutableMap.of(), mMockOnRequestComplete); + + verify(mMockCurlCommandLogger, never()).logCurlCommand(anyString()); + } + + @Test + public void curlLogging_simpleTextRequest() { + CronetHttpStack stack = + createStack( + new Consumer() { + @Override + public void accept(CronetHttpStack.Builder builder) { + builder.setCurlLoggingEnabled(true); + } + }); + + stack.executeRequest( + new TestRequest.Get(), ImmutableMap.of(), mMockOnRequestComplete); + + ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); + verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); + assertEquals("curl -X GET \"http://foo.com\"", curlCommandCaptor.getValue()); + } + + @Test + public void curlLogging_rewrittenUrl() { + CronetHttpStack stack = + createStack( + new Consumer() { + @Override + public void accept(CronetHttpStack.Builder builder) { + builder.setCurlLoggingEnabled(true) + .setUrlRewriter(mMockUrlRewriter); + } + }); + when(mMockUrlRewriter.rewriteUrl("http://foo.com")).thenReturn("http://bar.com"); + + stack.executeRequest( + new TestRequest.Get(), ImmutableMap.of(), mMockOnRequestComplete); + + ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); + verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); + assertEquals("curl -X GET \"http://bar.com\"", curlCommandCaptor.getValue()); + } + + @Test + public void curlLogging_headers_withoutTokens() { + CronetHttpStack stack = + createStack( + new Consumer() { + @Override + public void accept(CronetHttpStack.Builder builder) { + builder.setCurlLoggingEnabled(true); + } + }); + + stack.executeRequest( + new TestRequest.Delete() { + @Override + public Map getHeaders() { + return ImmutableMap.of( + "SomeHeader", "SomeValue", + "Authorization", "SecretToken"); + } + }, + ImmutableMap.of("SomeOtherHeader", "SomeValue"), + mMockOnRequestComplete); + + ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); + verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); + // NOTE: Header order is stable because the implementation uses a TreeMap. + assertEquals( + "curl -X DELETE --header \"Authorization: [REDACTED]\" " + + "--header \"SomeHeader: SomeValue\" " + + "--header \"SomeOtherHeader: SomeValue\" \"http://foo.com\"", + curlCommandCaptor.getValue()); + } + + @Test + public void curlLogging_headers_withTokens() { + CronetHttpStack stack = + createStack( + new Consumer() { + @Override + public void accept(CronetHttpStack.Builder builder) { + builder.setCurlLoggingEnabled(true) + .setLogAuthTokensInCurlCommands(true); + } + }); + + stack.executeRequest( + new TestRequest.Delete() { + @Override + public Map getHeaders() { + return ImmutableMap.of( + "SomeHeader", "SomeValue", + "Authorization", "SecretToken"); + } + }, + ImmutableMap.of("SomeOtherHeader", "SomeValue"), + mMockOnRequestComplete); + + ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); + verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); + // NOTE: Header order is stable because the implementation uses a TreeMap. + assertEquals( + "curl -X DELETE --header \"Authorization: SecretToken\" " + + "--header \"SomeHeader: SomeValue\" " + + "--header \"SomeOtherHeader: SomeValue\" \"http://foo.com\"", + curlCommandCaptor.getValue()); + } + + @Test + public void curlLogging_textRequest() { + CronetHttpStack stack = + createStack( + new Consumer() { + @Override + public void accept(CronetHttpStack.Builder builder) { + builder.setCurlLoggingEnabled(true); + } + }); + + stack.executeRequest( + new TestRequest.PostWithBody() { + @Override + public byte[] getBody() { + try { + return "hello".getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + @Override + public String getBodyContentType() { + return "text/plain; charset=UTF-8"; + } + }, + ImmutableMap.of(), + mMockOnRequestComplete); + + ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); + verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); + assertEquals( + "curl -X POST " + + "--header \"Content-Type: text/plain; charset=UTF-8\" \"http://foo.com\" " + + "--data-ascii \"hello\"", + curlCommandCaptor.getValue()); + } + + @Test + public void curlLogging_gzipTextRequest() { + CronetHttpStack stack = + createStack( + new Consumer() { + @Override + public void accept(CronetHttpStack.Builder builder) { + builder.setCurlLoggingEnabled(true); + } + }); + + stack.executeRequest( + new TestRequest.PostWithBody() { + @Override + public byte[] getBody() { + return new byte[] {1, 2, 3, 4, 5}; + } + + @Override + public String getBodyContentType() { + return "text/plain"; + } + + @Override + public Map getHeaders() { + return ImmutableMap.of("Content-Encoding", "gzip, identity"); + } + }, + ImmutableMap.of(), + mMockOnRequestComplete); + + ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); + verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); + assertEquals( + "echo 'AQIDBAU=' | base64 -d > /tmp/$$.bin; curl -X POST " + + "--header \"Content-Encoding: gzip, identity\" " + + "--header \"Content-Type: text/plain\" \"http://foo.com\" " + + "--data-binary @/tmp/$$.bin", + curlCommandCaptor.getValue()); + } + + @Test + public void curlLogging_binaryRequest() { + CronetHttpStack stack = + createStack( + new Consumer() { + @Override + public void accept(CronetHttpStack.Builder builder) { + builder.setCurlLoggingEnabled(true); + } + }); + + stack.executeRequest( + new TestRequest.PostWithBody() { + @Override + public byte[] getBody() { + return new byte[] {1, 2, 3, 4, 5}; + } + + @Override + public String getBodyContentType() { + return "application/octet-stream"; + } + }, + ImmutableMap.of(), + mMockOnRequestComplete); + + ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); + verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); + assertEquals( + "echo 'AQIDBAU=' | base64 -d > /tmp/$$.bin; curl -X POST " + + "--header \"Content-Type: application/octet-stream\" \"http://foo.com\" " + + "--data-binary @/tmp/$$.bin", + curlCommandCaptor.getValue()); + } + + @Test + public void curlLogging_largeRequest() { + CronetHttpStack stack = + createStack( + new Consumer() { + @Override + public void accept(CronetHttpStack.Builder builder) { + builder.setCurlLoggingEnabled(true); + } + }); + + stack.executeRequest( + new TestRequest.PostWithBody() { + @Override + public byte[] getBody() { + return new byte[2048]; + } + + @Override + public String getBodyContentType() { + return "application/octet-stream"; + } + }, + ImmutableMap.of(), + mMockOnRequestComplete); + + ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); + verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); + assertEquals( + "curl -X POST " + + "--header \"Content-Type: application/octet-stream\" \"http://foo.com\" " + + "[REQUEST BODY TOO LARGE TO INCLUDE]", + curlCommandCaptor.getValue()); + } + + @Test + public void getHeadersEmptyTest() { + List> list = new ArrayList<>(); + List

actual = CronetHttpStack.getHeaders(list); + List
expected = new ArrayList<>(); + assertEquals(expected, actual); + } + + @Test + public void getHeadersNonEmptyTest() { + Map headers = new HashMap<>(); + for (int i = 1; i < 5; i++) { + headers.put("key" + i, "value" + i); + } + List> list = new ArrayList<>(headers.entrySet()); + List
actual = CronetHttpStack.getHeaders(list); + List
expected = new ArrayList<>(); + for (int i = 1; i < 5; i++) { + expected.add(new Header("key" + i, "value" + i)); + } + assertHeaderListsEqual(expected, actual); + } + + private void assertHeaderListsEqual(List
expected, List
actual) { + assertEquals(expected.size(), actual.size()); + for (int i = 0; i < expected.size(); i++) { + assertEquals(expected.get(i).getName(), actual.get(i).getName()); + assertEquals(expected.get(i).getValue(), actual.get(i).getValue()); + } + } + + private CronetHttpStack createStack(Consumer stackEditor) { + CronetHttpStack.Builder builder = + new CronetHttpStack.Builder(RuntimeEnvironment.application) + .setCronetEngine(mMockCronetEngine) + .setCurlCommandLogger(mMockCurlCommandLogger); + stackEditor.accept(builder); + CronetHttpStack stack = builder.build(); + stack.setBlockingExecutor(MoreExecutors.newDirectExecutorService()); + stack.setNonBlockingExecutor(MoreExecutors.newDirectExecutorService()); + return stack; + } +} diff --git a/publish-snapshot-on-commit.sh b/publish-snapshot-on-commit.sh index 0d0e034..ba02b79 100755 --- a/publish-snapshot-on-commit.sh +++ b/publish-snapshot-on-commit.sh @@ -1,13 +1,15 @@ set -eu -if [ "$TRAVIS_REPO_SLUG" == "google/volley" ] && \ - [ "$TRAVIS_PULL_REQUEST" == "false" ] && \ - [ "$TRAVIS_BRANCH" == "master" ]; then - echo -e "Publishing snapshot build to OJO...\n" +GITHUB_BRANCH=${GITHUB_REF#refs/heads/} - ./gradlew artifactoryPublish +if [ "$GITHUB_REPOSITORY" == "google/volley" ] && \ + [ "$GITHUB_EVENT_NAME" == "push" ] && \ + [ "$GITHUB_BRANCH" == "master" ]; then + echo -e "Publishing snapshot build...\n" - echo -e "Published snapshot build to OJO" + ./gradlew publish + + echo -e "Published snapshot build" else echo -e "Not publishing snapshot" -fi \ No newline at end of file +fi diff --git a/publish.gradle b/publish.gradle new file mode 100644 index 0000000..429df4d --- /dev/null +++ b/publish.gradle @@ -0,0 +1,72 @@ +apply plugin: 'maven-publish' + +task sourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.srcDirs +} + +task javadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) +} + +afterEvaluate { + javadoc.classpath += files(android.libraryVariants.collect { variant -> + variant.getJavaCompile().classpath.files + }) +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives javadocJar + archives sourcesJar +} + +publishing { + publications { + library(MavenPublication) { + groupId 'com.android.volley' + version project.version + pom { + name = 'Volley' + url = 'https://github.com/google/volley' + packaging 'aar' + licenses { + license { + name = "The Apache License, Version 2.0" + url = "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + scm { + connection = 'scm:git:git://github.com/google/volley.git' + developerConnection = 'scm:git:ssh://git@github.com/google/volley.git' + url = 'https://github.com/google/volley' + } + developers { + developer { + name = 'The Volley Team' + email = 'noreply+volley@google.com' + } + } + } + + // Release AAR, Sources, and JavaDoc + artifact sourcesJar + artifact javadocJar + } + } + + repositories { + maven { + url = "https://oss.sonatype.org/content/repositories/snapshots/" + credentials { + username = System.env.OSSRH_DEPLOY_USERNAME + password = System.env.OSSRH_DEPLOY_PASSWORD + } + } + } +} diff --git a/rules.gradle b/rules.gradle deleted file mode 100644 index e0aef80..0000000 --- a/rules.gradle +++ /dev/null @@ -1,36 +0,0 @@ -// See build.gradle for an explanation of what this file is. - -apply plugin: 'com.android.library' - -android { - useLibrary 'org.apache.http.legacy' - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 - } - - defaultConfig { - consumerProguardFiles 'consumer-proguard-rules.pro' - } -} - -tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:unchecked" << "-Werror" -} - -dependencies { - implementation "androidx.annotation:annotation:1.0.1" - compileOnly "org.chromium.net:cronet-embedded:76.3809.111" -} - -// Check if the android plugin version supports unit testing. -if (configurations.findByName("testImplementation")) { - dependencies { - testImplementation "org.chromium.net:cronet-embedded:76.3809.111" - testImplementation "junit:junit:4.12" - testImplementation "org.hamcrest:hamcrest-library:1.3" - testImplementation "org.mockito:mockito-core:2.19.0" - testImplementation "org.robolectric:robolectric:3.4.2" - } -} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..ace4000 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = 'volley' +include 'core' +include 'cronet' +include 'testing' diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml deleted file mode 100644 index ba3a2a7..0000000 --- a/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/main/java/com/android/volley/AsyncCache.java b/src/main/java/com/android/volley/AsyncCache.java deleted file mode 100644 index 3cddb4b..0000000 --- a/src/main/java/com/android/volley/AsyncCache.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 deleted file mode 100644 index ad19c03..0000000 --- a/src/main/java/com/android/volley/AsyncNetwork.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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 response = new AtomicReference<>(); - final AtomicReference 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. - * - *

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. - * - *

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. - * - *

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 deleted file mode 100644 index 3754866..0000000 --- a/src/main/java/com/android/volley/AsyncRequestQueue.java +++ /dev/null @@ -1,626 +0,0 @@ -/* - * 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. - * - *

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. - * - *

{@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. - * - *

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 - void beginRequest(Request 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 - void sendRequestOverNetwork(Request request) { - mNonBlockingExecutor.execute(new NetworkTask<>(request)); - } - - /** Runnable that gets an entry from the cache. */ - private class CacheTask extends RequestTask { - CacheTask(Request 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 extends RequestTask { - Cache.Entry entry; - - CacheParseTask(Request 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 extends RequestTask { - VolleyError volleyError; - - ParseErrorTask(Request 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 extends RequestTask { - NetworkTask(Request 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 extends RequestTask { - NetworkResponse networkResponse; - - NetworkParseTask(Request 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 extends RequestTask { - Response response; - - CachePutTask(Request 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 taskQueue); - - abstract ExecutorService createBlockingExecutor(BlockingQueue taskQueue); - - abstract ScheduledExecutorService createNonBlockingScheduledExecutor(); - } - - /** Provides a BlockingQueue to be used to create executors. */ - private static PriorityBlockingQueue getBlockingQueue() { - return new PriorityBlockingQueue<>( - /* initialCapacity= */ 11, - new Comparator() { - @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 taskQueue) { - return getNewThreadPoolExecutor( - /* maximumPoolSize= */ 1, - /* threadNameSuffix= */ "Non-BlockingExecutor", - taskQueue); - } - - @Override - public ExecutorService createBlockingExecutor(BlockingQueue 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 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/AuthFailureError.java b/src/main/java/com/android/volley/AuthFailureError.java deleted file mode 100644 index fc6417e..0000000 --- a/src/main/java/com/android/volley/AuthFailureError.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import android.content.Intent; - -/** Error indicating that there was an authentication failure when performing a Request. */ -@SuppressWarnings("serial") -public class AuthFailureError extends VolleyError { - /** An intent that can be used to resolve this exception. (Brings up the password dialog.) */ - private Intent mResolutionIntent; - - public AuthFailureError() {} - - public AuthFailureError(Intent intent) { - mResolutionIntent = intent; - } - - public AuthFailureError(NetworkResponse response) { - super(response); - } - - public AuthFailureError(String message) { - super(message); - } - - public AuthFailureError(String message, Exception reason) { - super(message, reason); - } - - public Intent getResolutionIntent() { - return mResolutionIntent; - } - - @Override - public String getMessage() { - if (mResolutionIntent != null) { - return "User needs to (re)enter credentials."; - } - return super.getMessage(); - } -} diff --git a/src/main/java/com/android/volley/Cache.java b/src/main/java/com/android/volley/Cache.java deleted file mode 100644 index b8908ac..0000000 --- a/src/main/java/com/android/volley/Cache.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import androidx.annotation.Nullable; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** An interface for a cache keyed by a String with a byte array as data. */ -public interface Cache { - /** - * Retrieves an entry from the cache. - * - * @param key Cache key - * @return An {@link Entry} or null in the event of a cache miss - */ - @Nullable - Entry get(String key); - - /** - * Adds or replaces an entry to the cache. - * - * @param key Cache key - * @param entry Data to store and metadata for cache coherency, TTL, etc. - */ - void put(String key, Entry entry); - - /** - * Performs any potentially long-running actions needed to initialize the cache; will be called - * from a worker thread. - */ - void initialize(); - - /** - * Invalidates an entry in the cache. - * - * @param key Cache key - * @param fullExpire True to fully expire the entry, false to soft expire - */ - void invalidate(String key, boolean fullExpire); - - /** - * Removes an entry from the cache. - * - * @param key Cache key - */ - void remove(String key); - - /** Empties the cache. */ - void clear(); - - /** Data and metadata for an entry returned by the cache. */ - class Entry { - /** The data returned from cache. */ - public byte[] data; - - /** ETag for cache coherency. */ - public String etag; - - /** Date of this response as reported by the server. */ - public long serverDate; - - /** The last modified date for the requested object. */ - public long lastModified; - - /** TTL for this record. */ - public long ttl; - - /** Soft TTL for this record. */ - public long softTtl; - - /** - * Response headers as received from server; must be non-null. Should not be mutated - * directly. - * - *

Note that if the server returns two headers with the same (case-insensitive) name, - * this map will only contain the one of them. {@link #allResponseHeaders} may contain all - * headers if the {@link Cache} implementation supports it. - */ - public Map responseHeaders = Collections.emptyMap(); - - /** - * All response headers. May be null depending on the {@link Cache} implementation. Should - * not be mutated directly. - */ - public List

allResponseHeaders; - - /** True if the entry is expired. */ - public boolean isExpired() { - return this.ttl < System.currentTimeMillis(); - } - - /** True if a refresh is needed from the original data source. */ - public boolean refreshNeeded() { - return this.softTtl < System.currentTimeMillis(); - } - } -} diff --git a/src/main/java/com/android/volley/CacheDispatcher.java b/src/main/java/com/android/volley/CacheDispatcher.java deleted file mode 100644 index 1bfc0ea..0000000 --- a/src/main/java/com/android/volley/CacheDispatcher.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import android.os.Process; -import androidx.annotation.VisibleForTesting; -import java.util.concurrent.BlockingQueue; - -/** - * Provides a thread for performing cache triage on a queue of requests. - * - *

Requests added to the specified cache queue are resolved from cache. Any deliverable response - * is posted back to the caller via a {@link ResponseDelivery}. Cache misses and responses that - * require refresh are enqueued on the specified network queue for processing by a {@link - * NetworkDispatcher}. - */ -public class CacheDispatcher extends Thread { - - private static final boolean DEBUG = VolleyLog.DEBUG; - - /** The queue of requests coming in for triage. */ - private final BlockingQueue> mCacheQueue; - - /** The queue of requests going out to the network. */ - private final BlockingQueue> mNetworkQueue; - - /** The cache to read from. */ - private final Cache mCache; - - /** For posting responses. */ - private final ResponseDelivery mDelivery; - - /** Used for telling us to die. */ - private volatile boolean mQuit = false; - - /** Manage list of waiting requests and de-duplicate requests with same cache key. */ - private final WaitingRequestManager mWaitingRequestManager; - - /** - * Creates a new cache triage dispatcher thread. You must call {@link #start()} in order to - * begin processing. - * - * @param cacheQueue Queue of incoming requests for triage - * @param networkQueue Queue to post requests that require network to - * @param cache Cache interface to use for resolution - * @param delivery Delivery interface to use for posting responses - */ - public CacheDispatcher( - BlockingQueue> cacheQueue, - BlockingQueue> networkQueue, - Cache cache, - ResponseDelivery delivery) { - mCacheQueue = cacheQueue; - mNetworkQueue = networkQueue; - mCache = cache; - mDelivery = delivery; - mWaitingRequestManager = new WaitingRequestManager(this, networkQueue, delivery); - } - - /** - * Forces this dispatcher to quit immediately. If any requests are still in the queue, they are - * not guaranteed to be processed. - */ - public void quit() { - mQuit = true; - interrupt(); - } - - @Override - public void run() { - if (DEBUG) VolleyLog.v("start new dispatcher"); - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - - // Make a blocking call to initialize the cache. - mCache.initialize(); - - while (true) { - try { - processRequest(); - } catch (InterruptedException e) { - // We may have been interrupted because it was time to quit. - if (mQuit) { - Thread.currentThread().interrupt(); - return; - } - VolleyLog.e( - "Ignoring spurious interrupt of CacheDispatcher thread; " - + "use quit() to terminate it"); - } - } - } - - // Extracted to its own method to ensure locals have a constrained liveness scope by the GC. - // This is needed to avoid keeping previous request references alive for an indeterminate amount - // of time. Update consumer-proguard-rules.pro when modifying this. See also - // https://github.com/google/volley/issues/114 - private void processRequest() throws InterruptedException { - // Get a request from the cache triage queue, blocking until - // at least one is available. - final Request request = mCacheQueue.take(); - processRequest(request); - } - - @VisibleForTesting - void processRequest(final Request request) throws InterruptedException { - request.addMarker("cache-queue-take"); - request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED); - - try { - // If the request has been canceled, don't bother dispatching it. - if (request.isCanceled()) { - request.finish("cache-discard-canceled"); - return; - } - - // Attempt to retrieve this item from cache. - Cache.Entry entry = mCache.get(request.getCacheKey()); - if (entry == null) { - request.addMarker("cache-miss"); - // Cache miss; send off to the network dispatcher. - if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { - mNetworkQueue.put(request); - } - return; - } - - // If it is completely expired, just send it to the network. - if (entry.isExpired()) { - request.addMarker("cache-hit-expired"); - request.setCacheEntry(entry); - if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { - mNetworkQueue.put(request); - } - return; - } - - // We have a cache hit; parse its data for delivery back to the request. - request.addMarker("cache-hit"); - Response response = - request.parseNetworkResponse( - new NetworkResponse(entry.data, entry.responseHeaders)); - request.addMarker("cache-hit-parsed"); - - if (!response.isSuccess()) { - request.addMarker("cache-parsing-failed"); - mCache.invalidate(request.getCacheKey(), true); - request.setCacheEntry(null); - if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { - mNetworkQueue.put(request); - } - return; - } - if (!entry.refreshNeeded()) { - // Completely unexpired cache hit. Just deliver the response. - mDelivery.postResponse(request, response); - } else { - // Soft-expired cache hit. We can deliver the cached response, - // but we need to also send the request to the network for - // refreshing. - request.addMarker("cache-hit-refresh-needed"); - request.setCacheEntry(entry); - // Mark the response as intermediate. - response.intermediate = true; - - if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { - // Post the intermediate response back to the user and have - // the delivery then forward the request along to the network. - mDelivery.postResponse( - request, - response, - new Runnable() { - @Override - public void run() { - try { - mNetworkQueue.put(request); - } catch (InterruptedException e) { - // Restore the interrupted status - Thread.currentThread().interrupt(); - } - } - }); - } else { - // request has been added to list of waiting requests - // to receive the network response from the first request once it returns. - mDelivery.postResponse(request, response); - } - } - } finally { - request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED); - } - } -} diff --git a/src/main/java/com/android/volley/ClientError.java b/src/main/java/com/android/volley/ClientError.java deleted file mode 100644 index 521b76f..0000000 --- a/src/main/java/com/android/volley/ClientError.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -/** - * Indicates that the server responded with an error response indicating that the client has erred. - * - *

For backwards compatibility, extends ServerError which used to be thrown for all server - * errors, including 4xx error codes indicating a client error. - */ -@SuppressWarnings("serial") -public class ClientError extends ServerError { - public ClientError(NetworkResponse networkResponse) { - super(networkResponse); - } - - public ClientError() { - super(); - } -} diff --git a/src/main/java/com/android/volley/DefaultRetryPolicy.java b/src/main/java/com/android/volley/DefaultRetryPolicy.java deleted file mode 100644 index 4be6b50..0000000 --- a/src/main/java/com/android/volley/DefaultRetryPolicy.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -/** Default retry policy for requests. */ -public class DefaultRetryPolicy implements RetryPolicy { - /** The current timeout in milliseconds. */ - private int mCurrentTimeoutMs; - - /** The current retry count. */ - private int mCurrentRetryCount; - - /** The maximum number of attempts. */ - private final int mMaxNumRetries; - - /** The backoff multiplier for the policy. */ - private final float mBackoffMultiplier; - - /** The default socket timeout in milliseconds */ - public static final int DEFAULT_TIMEOUT_MS = 2500; - - /** The default number of retries */ - public static final int DEFAULT_MAX_RETRIES = 1; - - /** The default backoff multiplier */ - public static final float DEFAULT_BACKOFF_MULT = 1f; - - /** Constructs a new retry policy using the default timeouts. */ - public DefaultRetryPolicy() { - this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT); - } - - /** - * Constructs a new retry policy. - * - * @param initialTimeoutMs The initial timeout for the policy. - * @param maxNumRetries The maximum number of retries. - * @param backoffMultiplier Backoff multiplier for the policy. - */ - public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) { - mCurrentTimeoutMs = initialTimeoutMs; - mMaxNumRetries = maxNumRetries; - mBackoffMultiplier = backoffMultiplier; - } - - /** Returns the current timeout. */ - @Override - public int getCurrentTimeout() { - return mCurrentTimeoutMs; - } - - /** Returns the current retry count. */ - @Override - public int getCurrentRetryCount() { - return mCurrentRetryCount; - } - - /** Returns the backoff multiplier for the policy. */ - public float getBackoffMultiplier() { - return mBackoffMultiplier; - } - - /** - * Prepares for the next retry by applying a backoff to the timeout. - * - * @param error The error code of the last attempt. - */ - @Override - public void retry(VolleyError error) throws VolleyError { - mCurrentRetryCount++; - mCurrentTimeoutMs += (int) (mCurrentTimeoutMs * mBackoffMultiplier); - if (!hasAttemptRemaining()) { - throw error; - } - } - - /** Returns true if this policy has attempts remaining, false otherwise. */ - protected boolean hasAttemptRemaining() { - return mCurrentRetryCount <= mMaxNumRetries; - } -} diff --git a/src/main/java/com/android/volley/ExecutorDelivery.java b/src/main/java/com/android/volley/ExecutorDelivery.java deleted file mode 100644 index fd992f9..0000000 --- a/src/main/java/com/android/volley/ExecutorDelivery.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import android.os.Handler; -import java.util.concurrent.Executor; - -/** Delivers responses and errors. */ -public class ExecutorDelivery implements ResponseDelivery { - /** Used for posting responses, typically to the main thread. */ - private final Executor mResponsePoster; - - /** - * Creates a new response delivery interface. - * - * @param handler {@link Handler} to post responses on - */ - public ExecutorDelivery(final Handler handler) { - // Make an Executor that just wraps the handler. - mResponsePoster = - new Executor() { - @Override - public void execute(Runnable command) { - handler.post(command); - } - }; - } - - /** - * Creates a new response delivery interface, mockable version for testing. - * - * @param executor For running delivery tasks - */ - public ExecutorDelivery(Executor executor) { - mResponsePoster = executor; - } - - @Override - public void postResponse(Request request, Response response) { - postResponse(request, response, null); - } - - @Override - public void postResponse(Request request, Response response, Runnable runnable) { - request.markDelivered(); - request.addMarker("post-response"); - mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable)); - } - - @Override - public void postError(Request request, VolleyError error) { - request.addMarker("post-error"); - Response response = Response.error(error); - mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null)); - } - - /** A Runnable used for delivering network responses to a listener on the main thread. */ - @SuppressWarnings("rawtypes") - private static class ResponseDeliveryRunnable implements Runnable { - private final Request mRequest; - private final Response mResponse; - private final Runnable mRunnable; - - public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) { - mRequest = request; - mResponse = response; - mRunnable = runnable; - } - - @SuppressWarnings("unchecked") - @Override - public void run() { - // NOTE: If cancel() is called off the thread that we're currently running in (by - // default, the main thread), we cannot guarantee that deliverResponse()/deliverError() - // won't be called, since it may be canceled after we check isCanceled() but before we - // deliver the response. Apps concerned about this guarantee must either call cancel() - // from the same thread or implement their own guarantee about not invoking their - // listener after cancel() has been called. - - // If this request has canceled, finish it and don't deliver. - if (mRequest.isCanceled()) { - mRequest.finish("canceled-at-delivery"); - return; - } - - // Deliver a normal response or error, depending. - if (mResponse.isSuccess()) { - mRequest.deliverResponse(mResponse.result); - } else { - mRequest.deliverError(mResponse.error); - } - - // If this is an intermediate response, add a marker, otherwise we're done - // and the request can be finished. - if (mResponse.intermediate) { - mRequest.addMarker("intermediate-response"); - } else { - mRequest.finish("done"); - } - - // If we have been provided a post-delivery runnable, run it. - if (mRunnable != null) { - mRunnable.run(); - } - } - } -} diff --git a/src/main/java/com/android/volley/Header.java b/src/main/java/com/android/volley/Header.java deleted file mode 100644 index cd9c6ec..0000000 --- a/src/main/java/com/android/volley/Header.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.volley; - -import android.text.TextUtils; - -/** An HTTP header. */ -public final class Header { - private final String mName; - private final String mValue; - - public Header(String name, String value) { - mName = name; - mValue = value; - } - - public final String getName() { - return mName; - } - - public final String getValue() { - return mValue; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Header header = (Header) o; - - return TextUtils.equals(mName, header.mName) && TextUtils.equals(mValue, header.mValue); - } - - @Override - public int hashCode() { - int result = mName.hashCode(); - result = 31 * result + mValue.hashCode(); - return result; - } - - @Override - public String toString() { - return "Header[name=" + mName + ",value=" + mValue + "]"; - } -} diff --git a/src/main/java/com/android/volley/Network.java b/src/main/java/com/android/volley/Network.java deleted file mode 100644 index 16d5858..0000000 --- a/src/main/java/com/android/volley/Network.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -/** An interface for performing requests. */ -public interface Network { - /** - * Performs the specified request. - * - * @param request Request to process - * @return A {@link NetworkResponse} with data and caching metadata; will never be null - * @throws VolleyError on errors - */ - NetworkResponse performRequest(Request request) throws VolleyError; -} diff --git a/src/main/java/com/android/volley/NetworkDispatcher.java b/src/main/java/com/android/volley/NetworkDispatcher.java deleted file mode 100644 index 06057c3..0000000 --- a/src/main/java/com/android/volley/NetworkDispatcher.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import android.annotation.TargetApi; -import android.net.TrafficStats; -import android.os.Build; -import android.os.Process; -import android.os.SystemClock; -import androidx.annotation.VisibleForTesting; -import java.util.concurrent.BlockingQueue; - -/** - * Provides a thread for performing network dispatch from a queue of requests. - * - *

Requests added to the specified queue are processed from the network via a specified {@link - * Network} interface. Responses are committed to cache, if eligible, using a specified {@link - * Cache} interface. Valid responses and errors are posted back to the caller via a {@link - * ResponseDelivery}. - */ -public class NetworkDispatcher extends Thread { - - /** The queue of requests to service. */ - private final BlockingQueue> mQueue; - /** The network interface for processing requests. */ - private final Network mNetwork; - /** The cache to write to. */ - private final Cache mCache; - /** For posting responses and errors. */ - private final ResponseDelivery mDelivery; - /** Used for telling us to die. */ - private volatile boolean mQuit = false; - - /** - * Creates a new network dispatcher thread. You must call {@link #start()} in order to begin - * processing. - * - * @param queue Queue of incoming requests for triage - * @param network Network interface to use for performing requests - * @param cache Cache interface to use for writing responses to cache - * @param delivery Delivery interface to use for posting responses - */ - public NetworkDispatcher( - BlockingQueue> queue, - Network network, - Cache cache, - ResponseDelivery delivery) { - mQueue = queue; - mNetwork = network; - mCache = cache; - mDelivery = delivery; - } - - /** - * Forces this dispatcher to quit immediately. If any requests are still in the queue, they are - * not guaranteed to be processed. - */ - public void quit() { - mQuit = true; - interrupt(); - } - - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - private void addTrafficStatsTag(Request request) { - // Tag the request (if API >= 14) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - TrafficStats.setThreadStatsTag(request.getTrafficStatsTag()); - } - } - - @Override - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - while (true) { - try { - processRequest(); - } catch (InterruptedException e) { - // We may have been interrupted because it was time to quit. - if (mQuit) { - Thread.currentThread().interrupt(); - return; - } - VolleyLog.e( - "Ignoring spurious interrupt of NetworkDispatcher thread; " - + "use quit() to terminate it"); - } - } - } - - // Extracted to its own method to ensure locals have a constrained liveness scope by the GC. - // This is needed to avoid keeping previous request references alive for an indeterminate amount - // of time. Update consumer-proguard-rules.pro when modifying this. See also - // https://github.com/google/volley/issues/114 - private void processRequest() throws InterruptedException { - // Take a request from the queue. - Request request = mQueue.take(); - processRequest(request); - } - - @VisibleForTesting - void processRequest(Request request) { - long startTimeMs = SystemClock.elapsedRealtime(); - request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); - try { - request.addMarker("network-queue-take"); - - // If the request was cancelled already, do not perform the - // network request. - if (request.isCanceled()) { - request.finish("network-discard-cancelled"); - request.notifyListenerResponseNotUsable(); - return; - } - - addTrafficStatsTag(request); - - // Perform the network request. - NetworkResponse networkResponse = mNetwork.performRequest(request); - request.addMarker("network-http-complete"); - - // If the server returned 304 AND we delivered a response already, - // we're done -- don't deliver a second identical response. - if (networkResponse.notModified && request.hasHadResponseDelivered()) { - request.finish("not-modified"); - request.notifyListenerResponseNotUsable(); - return; - } - - // Parse the response here on the worker thread. - Response response = request.parseNetworkResponse(networkResponse); - request.addMarker("network-parse-complete"); - - // Write to cache if applicable. - // TODO: Only update cache metadata instead of entire record for 304s. - if (request.shouldCache() && response.cacheEntry != null) { - mCache.put(request.getCacheKey(), response.cacheEntry); - request.addMarker("network-cache-written"); - } - - // Post the response back. - request.markDelivered(); - mDelivery.postResponse(request, response); - request.notifyListenerResponseReceived(response); - } catch (VolleyError volleyError) { - volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); - parseAndDeliverNetworkError(request, volleyError); - request.notifyListenerResponseNotUsable(); - } catch (Exception e) { - VolleyLog.e(e, "Unhandled exception %s", e.toString()); - VolleyError volleyError = new VolleyError(e); - volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); - mDelivery.postError(request, volleyError); - request.notifyListenerResponseNotUsable(); - } finally { - request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED); - } - } - - private void parseAndDeliverNetworkError(Request request, VolleyError error) { - error = request.parseNetworkError(error); - mDelivery.postError(request, error); - } -} diff --git a/src/main/java/com/android/volley/NetworkError.java b/src/main/java/com/android/volley/NetworkError.java deleted file mode 100644 index 6b2b19f..0000000 --- a/src/main/java/com/android/volley/NetworkError.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -/** Indicates that there was a network error when performing a Volley request. */ -@SuppressWarnings("serial") -public class NetworkError extends VolleyError { - public NetworkError() { - super(); - } - - public NetworkError(Throwable cause) { - super(cause); - } - - public NetworkError(NetworkResponse networkResponse) { - super(networkResponse); - } -} diff --git a/src/main/java/com/android/volley/NetworkResponse.java b/src/main/java/com/android/volley/NetworkResponse.java deleted file mode 100644 index cfbc371..0000000 --- a/src/main/java/com/android/volley/NetworkResponse.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import androidx.annotation.Nullable; -import java.net.HttpURLConnection; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -/** Data and headers returned from {@link Network#performRequest(Request)}. */ -public class NetworkResponse { - - /** - * Creates a new network response. - * - * @param statusCode the HTTP status code - * @param data Response body - * @param headers Headers returned with this response, or null for none - * @param notModified True if the server returned a 304 and the data was already in cache - * @param networkTimeMs Round-trip network time to receive network response - * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor - * cannot handle server responses containing multiple headers with the same name. This - * constructor may be removed in a future release of Volley. - */ - @Deprecated - public NetworkResponse( - int statusCode, - byte[] data, - @Nullable Map headers, - boolean notModified, - long networkTimeMs) { - this(statusCode, data, headers, toAllHeaderList(headers), notModified, networkTimeMs); - } - - /** - * Creates a new network response. - * - * @param statusCode the HTTP status code - * @param data Response body - * @param notModified True if the server returned a 304 and the data was already in cache - * @param networkTimeMs Round-trip network time to receive network response - * @param allHeaders All headers returned with this response, or null for none - */ - public NetworkResponse( - int statusCode, - byte[] data, - boolean notModified, - long networkTimeMs, - @Nullable List

allHeaders) { - this(statusCode, data, toHeaderMap(allHeaders), allHeaders, notModified, networkTimeMs); - } - - /** - * Creates a new network response. - * - * @param statusCode the HTTP status code - * @param data Response body - * @param headers Headers returned with this response, or null for none - * @param notModified True if the server returned a 304 and the data was already in cache - * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor - * cannot handle server responses containing multiple headers with the same name. This - * constructor may be removed in a future release of Volley. - */ - @Deprecated - public NetworkResponse( - int statusCode, - byte[] data, - @Nullable Map headers, - boolean notModified) { - this(statusCode, data, headers, notModified, /* networkTimeMs= */ 0); - } - - /** - * Creates a new network response for an OK response with no headers. - * - * @param data Response body - */ - public NetworkResponse(byte[] data) { - this( - HttpURLConnection.HTTP_OK, - data, - /* notModified= */ false, - /* networkTimeMs= */ 0, - Collections.
emptyList()); - } - - /** - * Creates a new network response for an OK response. - * - * @param data Response body - * @param headers Headers returned with this response, or null for none - * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor - * cannot handle server responses containing multiple headers with the same name. This - * constructor may be removed in a future release of Volley. - */ - @Deprecated - public NetworkResponse(byte[] data, @Nullable Map headers) { - this( - HttpURLConnection.HTTP_OK, - data, - headers, - /* notModified= */ false, - /* networkTimeMs= */ 0); - } - - private NetworkResponse( - int statusCode, - byte[] data, - @Nullable Map headers, - @Nullable List
allHeaders, - boolean notModified, - long networkTimeMs) { - this.statusCode = statusCode; - this.data = data; - this.headers = headers; - if (allHeaders == null) { - this.allHeaders = null; - } else { - this.allHeaders = Collections.unmodifiableList(allHeaders); - } - this.notModified = notModified; - this.networkTimeMs = networkTimeMs; - } - - /** The HTTP status code. */ - public final int statusCode; - - /** Raw data from this response. */ - public final byte[] data; - - /** - * Response headers. - * - *

This map is case-insensitive. It should not be mutated directly. - * - *

Note that if the server returns two headers with the same (case-insensitive) name, this - * map will only contain the last one. Use {@link #allHeaders} to inspect all headers returned - * by the server. - */ - @Nullable public final Map headers; - - /** All response headers. Must not be mutated directly. */ - @Nullable public final List

allHeaders; - - /** True if the server returned a 304 (Not Modified). */ - public final boolean notModified; - - /** Network roundtrip time in milliseconds. */ - public final long networkTimeMs; - - @Nullable - private static Map toHeaderMap(@Nullable List
allHeaders) { - if (allHeaders == null) { - return null; - } - if (allHeaders.isEmpty()) { - return Collections.emptyMap(); - } - Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - // Later elements in the list take precedence. - for (Header header : allHeaders) { - headers.put(header.getName(), header.getValue()); - } - return headers; - } - - @Nullable - private static List
toAllHeaderList(@Nullable Map headers) { - if (headers == null) { - return null; - } - if (headers.isEmpty()) { - return Collections.emptyList(); - } - List
allHeaders = new ArrayList<>(headers.size()); - for (Map.Entry header : headers.entrySet()) { - allHeaders.add(new Header(header.getKey(), header.getValue())); - } - return allHeaders; - } -} diff --git a/src/main/java/com/android/volley/NoConnectionError.java b/src/main/java/com/android/volley/NoConnectionError.java deleted file mode 100644 index 185eb35..0000000 --- a/src/main/java/com/android/volley/NoConnectionError.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -/** Error indicating that no connection could be established when performing a Volley request. */ -@SuppressWarnings("serial") -public class NoConnectionError extends NetworkError { - public NoConnectionError() { - super(); - } - - public NoConnectionError(Throwable reason) { - super(reason); - } -} diff --git a/src/main/java/com/android/volley/ParseError.java b/src/main/java/com/android/volley/ParseError.java deleted file mode 100644 index 04a9d58..0000000 --- a/src/main/java/com/android/volley/ParseError.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -/** Indicates that the server's response could not be parsed. */ -@SuppressWarnings("serial") -public class ParseError extends VolleyError { - public ParseError() {} - - public ParseError(NetworkResponse networkResponse) { - super(networkResponse); - } - - public ParseError(Throwable cause) { - super(cause); - } -} diff --git a/src/main/java/com/android/volley/Request.java b/src/main/java/com/android/volley/Request.java deleted file mode 100644 index b60dc74..0000000 --- a/src/main/java/com/android/volley/Request.java +++ /dev/null @@ -1,719 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import android.net.TrafficStats; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.text.TextUtils; -import androidx.annotation.CallSuper; -import androidx.annotation.GuardedBy; -import androidx.annotation.Nullable; -import com.android.volley.VolleyLog.MarkerLog; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Collections; -import java.util.Map; - -/** - * Base class for all network requests. - * - * @param The type of parsed response this request expects. - */ -public abstract class Request implements Comparable> { - - /** Default encoding for POST or PUT parameters. See {@link #getParamsEncoding()}. */ - private static final String DEFAULT_PARAMS_ENCODING = "UTF-8"; - - /** Supported request methods. */ - public interface Method { - int DEPRECATED_GET_OR_POST = -1; - int GET = 0; - int POST = 1; - int PUT = 2; - int DELETE = 3; - int HEAD = 4; - int OPTIONS = 5; - int TRACE = 6; - int PATCH = 7; - } - - /** Callback to notify when the network request returns. */ - /* package */ interface NetworkRequestCompleteListener { - - /** Callback when a network response has been received. */ - void onResponseReceived(Request request, Response response); - - /** Callback when request returns from network without valid response. */ - void onNoUsableResponseReceived(Request request); - } - - /** An event log tracing the lifetime of this request; for debugging. */ - private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null; - - /** - * Request method of this request. Currently supports GET, POST, PUT, DELETE, HEAD, OPTIONS, - * TRACE, and PATCH. - */ - private final int mMethod; - - /** URL of this request. */ - private final String mUrl; - - /** Default tag for {@link TrafficStats}. */ - private final int mDefaultTrafficStatsTag; - - /** Lock to guard state which can be mutated after a request is added to the queue. */ - private final Object mLock = new Object(); - - /** Listener interface for errors. */ - @Nullable - @GuardedBy("mLock") - private Response.ErrorListener mErrorListener; - - /** Sequence number of this request, used to enforce FIFO ordering. */ - private Integer mSequence; - - /** The request queue this request is associated with. */ - private RequestQueue mRequestQueue; - - /** Whether or not responses to this request should be cached. */ - // TODO(#190): Turn this off by default for anything other than GET requests. - private boolean mShouldCache = true; - - /** Whether or not this request has been canceled. */ - @GuardedBy("mLock") - private boolean mCanceled = false; - - /** Whether or not a response has been delivered for this request yet. */ - @GuardedBy("mLock") - private boolean mResponseDelivered = false; - - /** Whether the request should be retried in the event of an HTTP 5xx (server) error. */ - private boolean mShouldRetryServerErrors = false; - - /** Whether the request should be retried in the event of a {@link NoConnectionError}. */ - private boolean mShouldRetryConnectionErrors = false; - - /** The retry policy for this request. */ - private RetryPolicy mRetryPolicy; - - /** - * When a request can be retrieved from cache but must be refreshed from the network, the cache - * entry will be stored here so that in the event of a "Not Modified" response, we can be sure - * it hasn't been evicted from cache. - */ - @Nullable private Cache.Entry mCacheEntry = null; - - /** An opaque token tagging this request; used for bulk cancellation. */ - private Object mTag; - - /** Listener that will be notified when a response has been delivered. */ - @GuardedBy("mLock") - private NetworkRequestCompleteListener mRequestCompleteListener; - - /** - * Creates a new request with the given URL and error listener. Note that the normal response - * listener is not provided here as delivery of responses is provided by subclasses, who have a - * better idea of how to deliver an already-parsed response. - * - * @deprecated Use {@link #Request(int, String, com.android.volley.Response.ErrorListener)}. - */ - @Deprecated - public Request(String url, Response.ErrorListener listener) { - this(Method.DEPRECATED_GET_OR_POST, url, listener); - } - - /** - * Creates a new request with the given method (one of the values from {@link Method}), URL, and - * error listener. Note that the normal response listener is not provided here as delivery of - * responses is provided by subclasses, who have a better idea of how to deliver an - * already-parsed response. - */ - public Request(int method, String url, @Nullable Response.ErrorListener listener) { - mMethod = method; - mUrl = url; - mErrorListener = listener; - setRetryPolicy(new DefaultRetryPolicy()); - - mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url); - } - - /** Return the method for this request. Can be one of the values in {@link Method}. */ - public int getMethod() { - return mMethod; - } - - /** - * Set a tag on this request. Can be used to cancel all requests with this tag by {@link - * RequestQueue#cancelAll(Object)}. - * - * @return This Request object to allow for chaining. - */ - public Request setTag(Object tag) { - mTag = tag; - return this; - } - - /** - * Returns this request's tag. - * - * @see Request#setTag(Object) - */ - public Object getTag() { - return mTag; - } - - /** @return this request's {@link com.android.volley.Response.ErrorListener}. */ - @Nullable - public Response.ErrorListener getErrorListener() { - synchronized (mLock) { - return mErrorListener; - } - } - - /** @return A tag for use with {@link TrafficStats#setThreadStatsTag(int)} */ - public int getTrafficStatsTag() { - return mDefaultTrafficStatsTag; - } - - /** @return The hashcode of the URL's host component, or 0 if there is none. */ - private static int findDefaultTrafficStatsTag(String url) { - if (!TextUtils.isEmpty(url)) { - Uri uri = Uri.parse(url); - if (uri != null) { - String host = uri.getHost(); - if (host != null) { - return host.hashCode(); - } - } - } - return 0; - } - - /** - * Sets the retry policy for this request. - * - * @return This Request object to allow for chaining. - */ - public Request setRetryPolicy(RetryPolicy retryPolicy) { - mRetryPolicy = retryPolicy; - return this; - } - - /** Adds an event to this request's event log; for debugging. */ - public void addMarker(String tag) { - if (MarkerLog.ENABLED) { - mEventLog.add(tag, Thread.currentThread().getId()); - } - } - - /** - * Notifies the request queue that this request has finished (successfully or with error). - * - *

Also dumps all events from this request's event log; for debugging. - */ - void finish(final String tag) { - if (mRequestQueue != null) { - mRequestQueue.finish(this); - } - if (MarkerLog.ENABLED) { - final long threadId = Thread.currentThread().getId(); - if (Looper.myLooper() != Looper.getMainLooper()) { - // If we finish marking off of the main thread, we need to - // actually do it on the main thread to ensure correct ordering. - Handler mainThread = new Handler(Looper.getMainLooper()); - mainThread.post( - new Runnable() { - @Override - public void run() { - mEventLog.add(tag, threadId); - mEventLog.finish(Request.this.toString()); - } - }); - return; - } - - mEventLog.add(tag, threadId); - mEventLog.finish(this.toString()); - } - } - - void sendEvent(@RequestQueue.RequestEvent int event) { - if (mRequestQueue != null) { - mRequestQueue.sendRequestEvent(this, event); - } - } - - /** - * Associates this request with the given queue. The request queue will be notified when this - * request has finished. - * - * @return This Request object to allow for chaining. - */ - public Request setRequestQueue(RequestQueue requestQueue) { - mRequestQueue = requestQueue; - return this; - } - - /** - * Sets the sequence number of this request. Used by {@link RequestQueue}. - * - * @return This Request object to allow for chaining. - */ - public final Request setSequence(int sequence) { - mSequence = sequence; - return this; - } - - /** Returns the sequence number of this request. */ - public final int getSequence() { - if (mSequence == null) { - throw new IllegalStateException("getSequence called before setSequence"); - } - return mSequence; - } - - /** Returns the URL of this request. */ - public String getUrl() { - return mUrl; - } - - /** Returns the cache key for this request. By default, this is the URL. */ - public String getCacheKey() { - String url = getUrl(); - // If this is a GET request, just use the URL as the key. - // For callers using DEPRECATED_GET_OR_POST, we assume the method is GET, which matches - // legacy behavior where all methods had the same cache key. We can't determine which method - // will be used because doing so requires calling getPostBody() which is expensive and may - // throw AuthFailureError. - // TODO(#190): Remove support for non-GET methods. - int method = getMethod(); - if (method == Method.GET || method == Method.DEPRECATED_GET_OR_POST) { - return url; - } - return Integer.toString(method) + '-' + url; - } - - /** - * Annotates this request with an entry retrieved for it from cache. Used for cache coherency - * support. - * - * @return This Request object to allow for chaining. - */ - public Request setCacheEntry(Cache.Entry entry) { - mCacheEntry = entry; - return this; - } - - /** Returns the annotated cache entry, or null if there isn't one. */ - @Nullable - public Cache.Entry getCacheEntry() { - return mCacheEntry; - } - - /** - * Mark this request as canceled. - * - *

No callback will be delivered as long as either: - * - *

    - *
  • This method is called on the same thread as the {@link ResponseDelivery} is running on. - * By default, this is the main thread. - *
  • The request subclass being used overrides cancel() and ensures that it does not invoke - * the listener in {@link #deliverResponse} after cancel() has been called in a - * thread-safe manner. - *
- * - *

There are no guarantees if both of these conditions aren't met. - */ - @CallSuper - public void cancel() { - synchronized (mLock) { - mCanceled = true; - mErrorListener = null; - } - } - - /** Returns true if this request has been canceled. */ - public boolean isCanceled() { - synchronized (mLock) { - return mCanceled; - } - } - - /** - * Returns a list of extra HTTP headers to go along with this request. Can throw {@link - * AuthFailureError} as authentication may be required to provide these values. - * - * @throws AuthFailureError In the event of auth failure - */ - public Map getHeaders() throws AuthFailureError { - return Collections.emptyMap(); - } - - /** - * Returns a Map of POST parameters to be used for this request, or null if a simple GET should - * be used. Can throw {@link AuthFailureError} as authentication may be required to provide - * these values. - * - *

Note that only one of getPostParams() and getPostBody() can return a non-null value. - * - * @throws AuthFailureError In the event of auth failure - * @deprecated Use {@link #getParams()} instead. - */ - @Deprecated - @Nullable - protected Map getPostParams() throws AuthFailureError { - return getParams(); - } - - /** - * Returns which encoding should be used when converting POST parameters returned by {@link - * #getPostParams()} into a raw POST body. - * - *

This controls both encodings: - * - *

    - *
  1. The string encoding used when converting parameter names and values into bytes prior to - * URL encoding them. - *
  2. The string encoding used when converting the URL encoded parameters into a raw byte - * array. - *
- * - * @deprecated Use {@link #getParamsEncoding()} instead. - */ - @Deprecated - protected String getPostParamsEncoding() { - return getParamsEncoding(); - } - - /** @deprecated Use {@link #getBodyContentType()} instead. */ - @Deprecated - public String getPostBodyContentType() { - return getBodyContentType(); - } - - /** - * Returns the raw POST body to be sent. - * - * @throws AuthFailureError In the event of auth failure - * @deprecated Use {@link #getBody()} instead. - */ - @Deprecated - public byte[] getPostBody() throws AuthFailureError { - // Note: For compatibility with legacy clients of volley, this implementation must remain - // here instead of simply calling the getBody() function because this function must - // call getPostParams() and getPostParamsEncoding() since legacy clients would have - // overridden these two member functions for POST requests. - Map postParams = getPostParams(); - if (postParams != null && postParams.size() > 0) { - return encodeParameters(postParams, getPostParamsEncoding()); - } - return null; - } - - /** - * Returns a Map of parameters to be used for a POST or PUT request. Can throw {@link - * AuthFailureError} as authentication may be required to provide these values. - * - *

Note that you can directly override {@link #getBody()} for custom data. - * - * @throws AuthFailureError in the event of auth failure - */ - @Nullable - protected Map getParams() throws AuthFailureError { - return null; - } - - /** - * Returns which encoding should be used when converting POST or PUT parameters returned by - * {@link #getParams()} into a raw POST or PUT body. - * - *

This controls both encodings: - * - *

    - *
  1. The string encoding used when converting parameter names and values into bytes prior to - * URL encoding them. - *
  2. The string encoding used when converting the URL encoded parameters into a raw byte - * array. - *
- */ - protected String getParamsEncoding() { - return DEFAULT_PARAMS_ENCODING; - } - - /** Returns the content type of the POST or PUT body. */ - public String getBodyContentType() { - return "application/x-www-form-urlencoded; charset=" + getParamsEncoding(); - } - - /** - * Returns the raw POST or PUT body to be sent. - * - *

By default, the body consists of the request parameters in - * application/x-www-form-urlencoded format. When overriding this method, consider overriding - * {@link #getBodyContentType()} as well to match the new body format. - * - * @throws AuthFailureError in the event of auth failure - */ - public byte[] getBody() throws AuthFailureError { - Map params = getParams(); - if (params != null && params.size() > 0) { - return encodeParameters(params, getParamsEncoding()); - } - return null; - } - - /** Converts params into an application/x-www-form-urlencoded encoded string. */ - private byte[] encodeParameters(Map params, String paramsEncoding) { - StringBuilder encodedParams = new StringBuilder(); - try { - for (Map.Entry entry : params.entrySet()) { - if (entry.getKey() == null || entry.getValue() == null) { - throw new IllegalArgumentException( - String.format( - "Request#getParams() or Request#getPostParams() returned a map " - + "containing a null key or value: (%s, %s). All keys " - + "and values must be non-null.", - entry.getKey(), entry.getValue())); - } - encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding)); - encodedParams.append('='); - encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding)); - encodedParams.append('&'); - } - return encodedParams.toString().getBytes(paramsEncoding); - } catch (UnsupportedEncodingException uee) { - throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee); - } - } - - /** - * Set whether or not responses to this request should be cached. - * - * @return This Request object to allow for chaining. - */ - public final Request setShouldCache(boolean shouldCache) { - mShouldCache = shouldCache; - return this; - } - - /** Returns true if responses to this request should be cached. */ - public final boolean shouldCache() { - return mShouldCache; - } - - /** - * Sets whether or not the request should be retried in the event of an HTTP 5xx (server) error. - * - * @return This Request object to allow for chaining. - */ - public final Request setShouldRetryServerErrors(boolean shouldRetryServerErrors) { - mShouldRetryServerErrors = shouldRetryServerErrors; - return this; - } - - /** - * Returns true if this request should be retried in the event of an HTTP 5xx (server) error. - */ - public final boolean shouldRetryServerErrors() { - return mShouldRetryServerErrors; - } - - /** - * Sets whether or not the request should be retried in the event that no connection could be - * established. - * - * @return This Request object to allow for chaining. - */ - public final Request setShouldRetryConnectionErrors(boolean shouldRetryConnectionErrors) { - mShouldRetryConnectionErrors = shouldRetryConnectionErrors; - return this; - } - - /** - * Returns true if this request should be retried in the event that no connection could be - * established. - */ - public final boolean shouldRetryConnectionErrors() { - return mShouldRetryConnectionErrors; - } - - /** - * Priority values. Requests will be processed from higher priorities to lower priorities, in - * FIFO order. - */ - public enum Priority { - LOW, - NORMAL, - HIGH, - IMMEDIATE - } - - /** Returns the {@link Priority} of this request; {@link Priority#NORMAL} by default. */ - public Priority getPriority() { - return Priority.NORMAL; - } - - /** - * Returns the socket timeout in milliseconds per retry attempt. (This value can be changed per - * retry attempt if a backoff is specified via backoffTimeout()). If there are no retry attempts - * remaining, this will cause delivery of a {@link TimeoutError} error. - */ - public final int getTimeoutMs() { - return getRetryPolicy().getCurrentTimeout(); - } - - /** Returns the retry policy that should be used for this request. */ - public RetryPolicy getRetryPolicy() { - return mRetryPolicy; - } - - /** - * Mark this request as having a response delivered on it. This can be used later in the - * request's lifetime for suppressing identical responses. - */ - public void markDelivered() { - synchronized (mLock) { - mResponseDelivered = true; - } - } - - /** Returns true if this request has had a response delivered for it. */ - public boolean hasHadResponseDelivered() { - synchronized (mLock) { - return mResponseDelivered; - } - } - - /** - * Subclasses must implement this to parse the raw network response and return an appropriate - * response type. This method will be called from a worker thread. The response will not be - * delivered if you return null. - * - * @param response Response from the network - * @return The parsed response, or null in the case of an error - */ - protected abstract Response parseNetworkResponse(NetworkResponse response); - - /** - * Subclasses can override this method to parse 'networkError' and return a more specific error. - * - *

The default implementation just returns the passed 'networkError'. - * - * @param volleyError the error retrieved from the network - * @return an NetworkError augmented with additional information - */ - protected VolleyError parseNetworkError(VolleyError volleyError) { - return volleyError; - } - - /** - * Subclasses must implement this to perform delivery of the parsed response to their listeners. - * The given response is guaranteed to be non-null; responses that fail to parse are not - * delivered. - * - * @param response The parsed response returned by {@link - * #parseNetworkResponse(NetworkResponse)} - */ - protected abstract void deliverResponse(T response); - - /** - * Delivers error message to the ErrorListener that the Request was initialized with. - * - * @param error Error details - */ - public void deliverError(VolleyError error) { - Response.ErrorListener listener; - synchronized (mLock) { - listener = mErrorListener; - } - if (listener != null) { - listener.onErrorResponse(error); - } - } - - /** - * {@link NetworkRequestCompleteListener} that will receive callbacks when the request returns - * from the network. - */ - /* package */ void setNetworkRequestCompleteListener( - NetworkRequestCompleteListener requestCompleteListener) { - synchronized (mLock) { - mRequestCompleteListener = requestCompleteListener; - } - } - - /** - * Notify NetworkRequestCompleteListener that a valid response has been received which can be - * used for other, waiting requests. - * - * @param response received from the network - */ - /* package */ void notifyListenerResponseReceived(Response response) { - NetworkRequestCompleteListener listener; - synchronized (mLock) { - listener = mRequestCompleteListener; - } - if (listener != null) { - listener.onResponseReceived(this, response); - } - } - - /** - * Notify NetworkRequestCompleteListener that the network request did not result in a response - * which can be used for other, waiting requests. - */ - /* package */ void notifyListenerResponseNotUsable() { - NetworkRequestCompleteListener listener; - synchronized (mLock) { - listener = mRequestCompleteListener; - } - if (listener != null) { - listener.onNoUsableResponseReceived(this); - } - } - - /** - * Our comparator sorts from high to low priority, and secondarily by sequence number to provide - * FIFO ordering. - */ - @Override - public int compareTo(Request other) { - Priority left = this.getPriority(); - Priority right = other.getPriority(); - - // High-priority requests are "lesser" so they are sorted to the front. - // Equal priorities are sorted by sequence number to provide FIFO ordering. - return left == right ? this.mSequence - other.mSequence : right.ordinal() - left.ordinal(); - } - - @Override - public String toString() { - String trafficStatsTag = "0x" + Integer.toHexString(getTrafficStatsTag()); - return (isCanceled() ? "[X] " : "[ ] ") - + getUrl() - + " " - + trafficStatsTag - + " " - + getPriority() - + " " - + mSequence; - } -} diff --git a/src/main/java/com/android/volley/RequestQueue.java b/src/main/java/com/android/volley/RequestQueue.java deleted file mode 100644 index 6db0b1c..0000000 --- a/src/main/java/com/android/volley/RequestQueue.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.IntDef; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.PriorityBlockingQueue; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * A request dispatch queue with a thread pool of dispatchers. - * - *

Calling {@link #add(Request)} will enqueue the given Request for dispatch, resolving from - * either cache or network on a worker thread, and then delivering a parsed response on the main - * thread. - */ -public class RequestQueue { - - /** Callback interface for completed requests. */ - // TODO: This should not be a generic class, because the request type can't be determined at - // compile time, so all calls to onRequestFinished are unsafe. However, changing this would be - // an API-breaking change. See also: https://github.com/google/volley/pull/109 - @Deprecated // Use RequestEventListener instead. - public interface RequestFinishedListener { - /** Called when a request has finished processing. */ - void onRequestFinished(Request request); - } - - /** Request event types the listeners {@link RequestEventListener} will be notified about. */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - RequestEvent.REQUEST_QUEUED, - RequestEvent.REQUEST_CACHE_LOOKUP_STARTED, - RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED, - RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED, - RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED, - RequestEvent.REQUEST_FINISHED - }) - public @interface RequestEvent { - /** The request was added to the queue. */ - public static final int REQUEST_QUEUED = 0; - /** Cache lookup started for the request. */ - public static final int REQUEST_CACHE_LOOKUP_STARTED = 1; - /** - * Cache lookup finished for the request and cached response is delivered or request is - * queued for network dispatching. - */ - public static final int REQUEST_CACHE_LOOKUP_FINISHED = 2; - /** Network dispatch started for the request. */ - public static final int REQUEST_NETWORK_DISPATCH_STARTED = 3; - /** The network dispatch finished for the request and response (if any) is delivered. */ - public static final int REQUEST_NETWORK_DISPATCH_FINISHED = 4; - /** - * All the work associated with the request is finished and request is removed from all the - * queues. - */ - public static final int REQUEST_FINISHED = 5; - } - - /** Callback interface for request life cycle events. */ - public interface RequestEventListener { - /** - * Called on every request lifecycle event. Can be called from different threads. The call - * is blocking request processing, so any processing should be kept at minimum or moved to - * another thread. - */ - void onRequestEvent(Request request, @RequestEvent int event); - } - - /** Used for generating monotonically-increasing sequence numbers for requests. */ - private final AtomicInteger mSequenceGenerator = new AtomicInteger(); - - /** - * The set of all requests currently being processed by this RequestQueue. A Request will be in - * this set if it is waiting in any queue or currently being processed by any dispatcher. - */ - private final Set> mCurrentRequests = new HashSet<>(); - - /** The cache triage queue. */ - private final PriorityBlockingQueue> mCacheQueue = new PriorityBlockingQueue<>(); - - /** The queue of requests that are actually going out to the network. */ - private final PriorityBlockingQueue> mNetworkQueue = new PriorityBlockingQueue<>(); - - /** Number of network request dispatcher threads to start. */ - private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4; - - /** Cache interface for retrieving and storing responses. */ - private final Cache mCache; - - /** Network interface for performing requests. */ - private final Network mNetwork; - - /** Response delivery mechanism. */ - private final ResponseDelivery mDelivery; - - /** The network dispatchers. */ - private final NetworkDispatcher[] mDispatchers; - - /** The cache dispatcher. */ - private CacheDispatcher mCacheDispatcher; - - private final List mFinishedListeners = new ArrayList<>(); - - /** Collection of listeners for request life cycle events. */ - private final List mEventListeners = new ArrayList<>(); - - /** - * Creates the worker pool. Processing will not begin until {@link #start()} is called. - * - * @param cache A Cache to use for persisting responses to disk - * @param network A Network interface for performing HTTP requests - * @param threadPoolSize Number of network dispatcher threads to create - * @param delivery A ResponseDelivery interface for posting responses and errors - */ - public RequestQueue( - Cache cache, Network network, int threadPoolSize, ResponseDelivery delivery) { - mCache = cache; - mNetwork = network; - mDispatchers = new NetworkDispatcher[threadPoolSize]; - mDelivery = delivery; - } - - /** - * Creates the worker pool. Processing will not begin until {@link #start()} is called. - * - * @param cache A Cache to use for persisting responses to disk - * @param network A Network interface for performing HTTP requests - * @param threadPoolSize Number of network dispatcher threads to create - */ - public RequestQueue(Cache cache, Network network, int threadPoolSize) { - this( - cache, - network, - threadPoolSize, - new ExecutorDelivery(new Handler(Looper.getMainLooper()))); - } - - /** - * Creates the worker pool. Processing will not begin until {@link #start()} is called. - * - * @param cache A Cache to use for persisting responses to disk - * @param network A Network interface for performing HTTP requests - */ - public RequestQueue(Cache cache, Network network) { - this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE); - } - - /** Starts the dispatchers in this queue. */ - public void start() { - stop(); // Make sure any currently running dispatchers are stopped. - // Create the cache dispatcher and start it. - mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery); - mCacheDispatcher.start(); - - // Create network dispatchers (and corresponding threads) up to the pool size. - for (int i = 0; i < mDispatchers.length; i++) { - NetworkDispatcher networkDispatcher = - new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery); - mDispatchers[i] = networkDispatcher; - networkDispatcher.start(); - } - } - - /** Stops the cache and network dispatchers. */ - public void stop() { - if (mCacheDispatcher != null) { - mCacheDispatcher.quit(); - } - for (final NetworkDispatcher mDispatcher : mDispatchers) { - if (mDispatcher != null) { - mDispatcher.quit(); - } - } - } - - /** Gets a sequence number. */ - public int getSequenceNumber() { - return mSequenceGenerator.incrementAndGet(); - } - - /** Gets the {@link Cache} instance being used. */ - public Cache getCache() { - return mCache; - } - - /** - * A simple predicate or filter interface for Requests, for use by {@link - * RequestQueue#cancelAll(RequestFilter)}. - */ - public interface RequestFilter { - boolean apply(Request request); - } - - /** - * Cancels all requests in this queue for which the given filter applies. - * - * @param filter The filtering function to use - */ - public void cancelAll(RequestFilter filter) { - synchronized (mCurrentRequests) { - for (Request request : mCurrentRequests) { - if (filter.apply(request)) { - request.cancel(); - } - } - } - } - - /** - * Cancels all requests in this queue with the given tag. Tag must be non-null and equality is - * by identity. - */ - public void cancelAll(final Object tag) { - if (tag == null) { - throw new IllegalArgumentException("Cannot cancelAll with a null tag"); - } - cancelAll( - new RequestFilter() { - @Override - public boolean apply(Request request) { - return request.getTag() == tag; - } - }); - } - - /** - * Adds a Request to the dispatch queue. - * - * @param request The request to service - * @return The passed-in request - */ - public Request add(Request request) { - // Tag the request as belonging to this queue and add it to the set of current requests. - request.setRequestQueue(this); - synchronized (mCurrentRequests) { - mCurrentRequests.add(request); - } - - // Process requests in the order they are added. - request.setSequence(getSequenceNumber()); - request.addMarker("add-to-queue"); - sendRequestEvent(request, RequestEvent.REQUEST_QUEUED); - - beginRequest(request); - return request; - } - - void beginRequest(Request request) { - // If the request is uncacheable, skip the cache queue and go straight to the network. - if (!request.shouldCache()) { - sendRequestOverNetwork(request); - } else { - mCacheQueue.add(request); - } - } - - /** - * Called from {@link Request#finish(String)}, indicating that processing of the given request - * has finished. - */ - @SuppressWarnings("unchecked") // see above note on RequestFinishedListener - void finish(Request request) { - // Remove from the set of requests currently being processed. - synchronized (mCurrentRequests) { - mCurrentRequests.remove(request); - } - synchronized (mFinishedListeners) { - for (RequestFinishedListener listener : mFinishedListeners) { - listener.onRequestFinished(request); - } - } - sendRequestEvent(request, RequestEvent.REQUEST_FINISHED); - } - - /** Sends a request life cycle event to the listeners. */ - void sendRequestEvent(Request request, @RequestEvent int event) { - synchronized (mEventListeners) { - for (RequestEventListener listener : mEventListeners) { - listener.onRequestEvent(request, event); - } - } - } - - /** Add a listener for request life cycle events. */ - public void addRequestEventListener(RequestEventListener listener) { - synchronized (mEventListeners) { - mEventListeners.add(listener); - } - } - - /** Remove a listener for request life cycle events. */ - public void removeRequestEventListener(RequestEventListener listener) { - synchronized (mEventListeners) { - mEventListeners.remove(listener); - } - } - - @Deprecated // Use RequestEventListener instead. - public void addRequestFinishedListener(RequestFinishedListener listener) { - synchronized (mFinishedListeners) { - mFinishedListeners.add(listener); - } - } - - /** Remove a RequestFinishedListener. Has no effect if listener was not previously added. */ - @Deprecated // Use RequestEventListener instead. - public void removeRequestFinishedListener(RequestFinishedListener listener) { - synchronized (mFinishedListeners) { - mFinishedListeners.remove(listener); - } - } - - public ResponseDelivery getResponseDelivery() { - return mDelivery; - } - - void sendRequestOverNetwork(Request request) { - mNetworkQueue.add(request); - } -} diff --git a/src/main/java/com/android/volley/RequestTask.java b/src/main/java/com/android/volley/RequestTask.java deleted file mode 100644 index 8eeaf2c..0000000 --- a/src/main/java/com/android/volley/RequestTask.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.android.volley; - -/** Abstract runnable that's a task to be completed by the RequestQueue. */ -public abstract class RequestTask implements Runnable { - final Request mRequest; - - public RequestTask(Request request) { - mRequest = request; - } - - @SuppressWarnings("unchecked") - public int compareTo(RequestTask other) { - return mRequest.compareTo((Request) other.mRequest); - } -} diff --git a/src/main/java/com/android/volley/Response.java b/src/main/java/com/android/volley/Response.java deleted file mode 100644 index 622bdc4..0000000 --- a/src/main/java/com/android/volley/Response.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import androidx.annotation.Nullable; - -/** - * Encapsulates a parsed response for delivery. - * - * @param Parsed type of this response - */ -public class Response { - - /** Callback interface for delivering parsed responses. */ - public interface Listener { - /** Called when a response is received. */ - void onResponse(T response); - } - - /** Callback interface for delivering error responses. */ - public interface ErrorListener { - /** - * Callback method that an error has been occurred with the provided error code and optional - * user-readable message. - */ - void onErrorResponse(VolleyError error); - } - - /** Returns a successful response containing the parsed result. */ - public static Response success(@Nullable T result, @Nullable Cache.Entry cacheEntry) { - return new Response<>(result, cacheEntry); - } - - /** - * Returns a failed response containing the given error code and an optional localized message - * displayed to the user. - */ - public static Response error(VolleyError error) { - return new Response<>(error); - } - - /** Parsed response, can be null; always null in the case of error. */ - @Nullable public final T result; - - /** Cache metadata for this response; null if not cached or in the case of error. */ - @Nullable public final Cache.Entry cacheEntry; - - /** Detailed error information if errorCode != OK. */ - @Nullable public final VolleyError error; - - /** True if this response was a soft-expired one and a second one MAY be coming. */ - public boolean intermediate = false; - - /** Returns whether this response is considered successful. */ - public boolean isSuccess() { - return error == null; - } - - private Response(@Nullable T result, @Nullable Cache.Entry cacheEntry) { - this.result = result; - this.cacheEntry = cacheEntry; - this.error = null; - } - - private Response(VolleyError error) { - this.result = null; - this.cacheEntry = null; - this.error = error; - } -} diff --git a/src/main/java/com/android/volley/ResponseDelivery.java b/src/main/java/com/android/volley/ResponseDelivery.java deleted file mode 100644 index 10aa137..0000000 --- a/src/main/java/com/android/volley/ResponseDelivery.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -public interface ResponseDelivery { - /** Parses a response from the network or cache and delivers it. */ - void postResponse(Request request, Response response); - - /** - * Parses a response from the network or cache and delivers it. The provided Runnable will be - * executed after delivery. - */ - void postResponse(Request request, Response response, Runnable runnable); - - /** Posts an error for the given request. */ - void postError(Request request, VolleyError error); -} diff --git a/src/main/java/com/android/volley/RetryPolicy.java b/src/main/java/com/android/volley/RetryPolicy.java deleted file mode 100644 index 3ef26de..0000000 --- a/src/main/java/com/android/volley/RetryPolicy.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -/** - * Retry policy for a request. - * - *

A retry policy can control two parameters: - * - *

    - *
  • The number of tries. This can be a simple counter or more complex logic based on the type - * of error passed to {@link #retry(VolleyError)}, although {@link #getCurrentRetryCount()} - * should always return the current retry count for logging purposes. - *
  • The request timeout for each try, via {@link #getCurrentTimeout()}. In the common case that - * a request times out before the response has been received from the server, retrying again - * with a longer timeout can increase the likelihood of success (at the expense of causing the - * user to wait longer, especially if the request still fails). - *
- * - *

Note that currently, retries triggered by a retry policy are attempted immediately in sequence - * with no delay between them (although the time between tries may increase if the requests are - * timing out and {@link #getCurrentTimeout()} is returning increasing values). - * - *

By default, Volley uses {@link DefaultRetryPolicy}. - */ -public interface RetryPolicy { - - /** Returns the current timeout (used for logging). */ - int getCurrentTimeout(); - - /** Returns the current retry count (used for logging). */ - int getCurrentRetryCount(); - - /** - * Prepares for the next retry by applying a backoff to the timeout. - * - * @param error The error code of the last attempt. - * @throws VolleyError In the event that the retry could not be performed (for example if we ran - * out of attempts), the passed in error is thrown. - */ - void retry(VolleyError error) throws VolleyError; -} diff --git a/src/main/java/com/android/volley/ServerError.java b/src/main/java/com/android/volley/ServerError.java deleted file mode 100644 index 84b2eb4..0000000 --- a/src/main/java/com/android/volley/ServerError.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -/** Indicates that the server responded with an error response. */ -@SuppressWarnings("serial") -public class ServerError extends VolleyError { - public ServerError(NetworkResponse networkResponse) { - super(networkResponse); - } - - public ServerError() { - super(); - } -} diff --git a/src/main/java/com/android/volley/TimeoutError.java b/src/main/java/com/android/volley/TimeoutError.java deleted file mode 100644 index 227ae08..0000000 --- a/src/main/java/com/android/volley/TimeoutError.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -/** Indicates that the connection or the socket timed out. */ -@SuppressWarnings("serial") -public class TimeoutError extends VolleyError {} diff --git a/src/main/java/com/android/volley/VolleyError.java b/src/main/java/com/android/volley/VolleyError.java deleted file mode 100644 index 45086da..0000000 --- a/src/main/java/com/android/volley/VolleyError.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -/** Exception style class encapsulating Volley errors */ -@SuppressWarnings("serial") -public class VolleyError extends Exception { - public final NetworkResponse networkResponse; - private long networkTimeMs; - - public VolleyError() { - networkResponse = null; - } - - public VolleyError(NetworkResponse response) { - networkResponse = response; - } - - public VolleyError(String exceptionMessage) { - super(exceptionMessage); - networkResponse = null; - } - - public VolleyError(String exceptionMessage, Throwable reason) { - super(exceptionMessage, reason); - networkResponse = null; - } - - public VolleyError(Throwable cause) { - super(cause); - networkResponse = null; - } - - /* package */ void setNetworkTimeMs(long networkTimeMs) { - this.networkTimeMs = networkTimeMs; - } - - public long getNetworkTimeMs() { - return networkTimeMs; - } -} diff --git a/src/main/java/com/android/volley/VolleyLog.java b/src/main/java/com/android/volley/VolleyLog.java deleted file mode 100644 index 8477668..0000000 --- a/src/main/java/com/android/volley/VolleyLog.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import android.os.SystemClock; -import android.util.Log; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -/** - * Logging helper class. - * - *

to see Volley logs call:
- * {@code /platform-tools/adb shell setprop log.tag.Volley VERBOSE} - */ -public class VolleyLog { - public static String TAG = "Volley"; - - public static boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE); - - /** - * {@link Class#getName()} uses reflection and calling it on a potentially hot code path may - * have some cost. To minimize this cost we fetch class name once here and use it later. - */ - private static final String CLASS_NAME = VolleyLog.class.getName(); - - /** - * Customize the log tag for your application, so that other apps using Volley don't mix their - * logs with yours.
- * Enable the log property for your tag before starting your app:
- * {@code adb shell setprop log.tag.<tag>} - */ - public static void setTag(String tag) { - d("Changing log tag to %s", tag); - TAG = tag; - - // Reinitialize the DEBUG "constant" - DEBUG = Log.isLoggable(TAG, Log.VERBOSE); - } - - public static void v(String format, Object... args) { - if (DEBUG) { - Log.v(TAG, buildMessage(format, args)); - } - } - - public static void d(String format, Object... args) { - Log.d(TAG, buildMessage(format, args)); - } - - public static void e(String format, Object... args) { - Log.e(TAG, buildMessage(format, args)); - } - - public static void e(Throwable tr, String format, Object... args) { - Log.e(TAG, buildMessage(format, args), tr); - } - - public static void wtf(String format, Object... args) { - Log.wtf(TAG, buildMessage(format, args)); - } - - public static void wtf(Throwable tr, String format, Object... args) { - Log.wtf(TAG, buildMessage(format, args), tr); - } - - /** - * Formats the caller's provided message and prepends useful info like calling thread ID and - * method name. - */ - private static String buildMessage(String format, Object... args) { - String msg = (args == null) ? format : String.format(Locale.US, format, args); - StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace(); - - String caller = ""; - // Walk up the stack looking for the first caller outside of VolleyLog. - // It will be at least two frames up, so start there. - for (int i = 2; i < trace.length; i++) { - String clazz = trace[i].getClassName(); - if (!clazz.equals(VolleyLog.CLASS_NAME)) { - String callingClass = trace[i].getClassName(); - callingClass = callingClass.substring(callingClass.lastIndexOf('.') + 1); - callingClass = callingClass.substring(callingClass.lastIndexOf('$') + 1); - - caller = callingClass + "." + trace[i].getMethodName(); - break; - } - } - return String.format(Locale.US, "[%d] %s: %s", Thread.currentThread().getId(), caller, msg); - } - - /** A simple event log with records containing a name, thread ID, and timestamp. */ - static class MarkerLog { - public static final boolean ENABLED = VolleyLog.DEBUG; - - /** Minimum duration from first marker to last in an marker log to warrant logging. */ - private static final long MIN_DURATION_FOR_LOGGING_MS = 0; - - private static class Marker { - public final String name; - public final long thread; - public final long time; - - public Marker(String name, long thread, long time) { - this.name = name; - this.thread = thread; - this.time = time; - } - } - - private final List mMarkers = new ArrayList<>(); - private boolean mFinished = false; - - /** Adds a marker to this log with the specified name. */ - public synchronized void add(String name, long threadId) { - if (mFinished) { - throw new IllegalStateException("Marker added to finished log"); - } - - mMarkers.add(new Marker(name, threadId, SystemClock.elapsedRealtime())); - } - - /** - * Closes the log, dumping it to logcat if the time difference between the first and last - * markers is greater than {@link #MIN_DURATION_FOR_LOGGING_MS}. - * - * @param header Header string to print above the marker log. - */ - public synchronized void finish(String header) { - mFinished = true; - - long duration = getTotalDuration(); - if (duration <= MIN_DURATION_FOR_LOGGING_MS) { - return; - } - - long prevTime = mMarkers.get(0).time; - d("(%-4d ms) %s", duration, header); - for (Marker marker : mMarkers) { - long thisTime = marker.time; - d("(+%-4d) [%2d] %s", (thisTime - prevTime), marker.thread, marker.name); - prevTime = thisTime; - } - } - - @Override - protected void finalize() throws Throwable { - // Catch requests that have been collected (and hence end-of-lifed) - // but had no debugging output printed for them. - if (!mFinished) { - finish("Request on the loose"); - e("Marker log finalized without finish() - uncaught exit point for request"); - } - } - - /** Returns the time difference between the first and last events in this log. */ - private long getTotalDuration() { - if (mMarkers.size() == 0) { - return 0; - } - - long first = mMarkers.get(0).time; - long last = mMarkers.get(mMarkers.size() - 1).time; - return last - first; - } - } -} diff --git a/src/main/java/com/android/volley/WaitingRequestManager.java b/src/main/java/com/android/volley/WaitingRequestManager.java deleted file mode 100644 index 682e339..0000000 --- a/src/main/java/com/android/volley/WaitingRequestManager.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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. - * - *

    - *
  • containsKey(cacheKey) indicates that there is a request in flight for the given cache - * key. - *
  • get(cacheKey) returns waiting requests for the given cache key. The in flight request - * is not contained in that list. Is null if no requests are staged. - *
- */ - private final Map>> 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> mNetworkQueue; - - WaitingRequestManager(@NonNull RequestQueue requestQueue) { - mRequestQueue = requestQueue; - mResponseDelivery = mRequestQueue.getResponseDelivery(); - mCacheDispatcher = null; - mNetworkQueue = null; - } - - WaitingRequestManager( - @NonNull CacheDispatcher cacheDispatcher, - @NonNull BlockingQueue> 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> 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> 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> 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 deleted file mode 100644 index f3baace..0000000 --- a/src/main/java/com/android/volley/cronet/CronetHttpStack.java +++ /dev/null @@ -1,631 +0,0 @@ -/* - * 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 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
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 extends RequestTask { - UrlRequest.Builder builder; - String url; - Map additionalHeaders; - OnRequestComplete callback; - Request request; - - SetUpRequestTask( - Request request, - String url, - UrlRequest.Builder builder, - Map 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
getHeaders(List> headersList) { - List
headers = new ArrayList<>(); - for (Map.Entry 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 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 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 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. - * - *

When enabled, for each request dispatched to the network, a roughly-equivalent cURL - * command will be logged to logcat. - * - *

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}. - * - *

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. - * - *

Allows customization of the logging performed when cURL logging is enabled. - * - *

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. - * - *

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. - * - *

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. - * - *

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. - * - *

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 mHeaders = - new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - private String mHttpMethod; - @Nullable private byte[] mBody; - - /** - * Return the headers to be used for the request. - * - *

The returned map is case-insensitive. - */ - TreeMap getHeaders() { - return mHeaders; - } - - /** Apply all the headers in the given map to the request. */ - void putAllHeaders(Map 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 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/AdaptedHttpStack.java b/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java deleted file mode 100644 index c75c25f..0000000 --- a/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.volley.toolbox; - -import com.android.volley.AuthFailureError; -import com.android.volley.Header; -import com.android.volley.Request; -import java.io.IOException; -import java.net.SocketTimeoutException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import org.apache.http.conn.ConnectTimeoutException; - -/** - * {@link BaseHttpStack} implementation wrapping a {@link HttpStack}. - * - *

{@link BasicNetwork} uses this if it is provided a {@link HttpStack} at construction time, - * allowing it to have one implementation based atop {@link BaseHttpStack}. - */ -@SuppressWarnings("deprecation") -class AdaptedHttpStack extends BaseHttpStack { - - private final HttpStack mHttpStack; - - AdaptedHttpStack(HttpStack httpStack) { - mHttpStack = httpStack; - } - - @Override - public HttpResponse executeRequest(Request request, Map additionalHeaders) - throws IOException, AuthFailureError { - org.apache.http.HttpResponse apacheResp; - try { - apacheResp = mHttpStack.performRequest(request, additionalHeaders); - } catch (ConnectTimeoutException e) { - // BasicNetwork won't know that this exception should be retried like a timeout, since - // it's an Apache-specific error, so wrap it in a standard timeout exception. - throw new SocketTimeoutException(e.getMessage()); - } - - int statusCode = apacheResp.getStatusLine().getStatusCode(); - - org.apache.http.Header[] headers = apacheResp.getAllHeaders(); - List

headerList = new ArrayList<>(headers.length); - for (org.apache.http.Header header : headers) { - headerList.add(new Header(header.getName(), header.getValue())); - } - - if (apacheResp.getEntity() == null) { - return new HttpResponse(statusCode, headerList); - } - - long contentLength = apacheResp.getEntity().getContentLength(); - if ((int) contentLength != contentLength) { - throw new IOException("Response too large: " + contentLength); - } - - return new HttpResponse( - statusCode, - headerList, - (int) apacheResp.getEntity().getContentLength(), - apacheResp.getEntity().getContent()); - } -} diff --git a/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java b/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java deleted file mode 100644 index f3381ae..0000000 --- a/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerFuture; -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import androidx.annotation.VisibleForTesting; -import com.android.volley.AuthFailureError; - -/** - * An Authenticator that uses {@link AccountManager} to get auth tokens of a specified type for a - * specified account. - */ -// TODO: Update this to account for runtime permissions -@SuppressLint("MissingPermission") -public class AndroidAuthenticator implements Authenticator { - private final AccountManager mAccountManager; - private final Account mAccount; - private final String mAuthTokenType; - private final boolean mNotifyAuthFailure; - - /** - * Creates a new authenticator. - * - * @param context Context for accessing AccountManager - * @param account Account to authenticate as - * @param authTokenType Auth token type passed to AccountManager - */ - public AndroidAuthenticator(Context context, Account account, String authTokenType) { - this(context, account, authTokenType, /* notifyAuthFailure= */ false); - } - - /** - * Creates a new authenticator. - * - * @param context Context for accessing AccountManager - * @param account Account to authenticate as - * @param authTokenType Auth token type passed to AccountManager - * @param notifyAuthFailure Whether to raise a notification upon auth failure - */ - public AndroidAuthenticator( - Context context, Account account, String authTokenType, boolean notifyAuthFailure) { - this(AccountManager.get(context), account, authTokenType, notifyAuthFailure); - } - - @VisibleForTesting - AndroidAuthenticator( - AccountManager accountManager, - Account account, - String authTokenType, - boolean notifyAuthFailure) { - mAccountManager = accountManager; - mAccount = account; - mAuthTokenType = authTokenType; - mNotifyAuthFailure = notifyAuthFailure; - } - - /** Returns the Account being used by this authenticator. */ - public Account getAccount() { - return mAccount; - } - - /** Returns the Auth Token Type used by this authenticator. */ - public String getAuthTokenType() { - return mAuthTokenType; - } - - // TODO: Figure out what to do about notifyAuthFailure - @SuppressWarnings("deprecation") - @Override - public String getAuthToken() throws AuthFailureError { - AccountManagerFuture future = - mAccountManager.getAuthToken( - mAccount, - mAuthTokenType, - mNotifyAuthFailure, - /* callback= */ null, - /* handler= */ null); - Bundle result; - try { - result = future.getResult(); - } catch (Exception e) { - throw new AuthFailureError("Error while retrieving auth token", e); - } - String authToken = null; - if (future.isDone() && !future.isCancelled()) { - if (result.containsKey(AccountManager.KEY_INTENT)) { - Intent intent = result.getParcelable(AccountManager.KEY_INTENT); - throw new AuthFailureError(intent); - } - authToken = result.getString(AccountManager.KEY_AUTHTOKEN); - } - if (authToken == null) { - throw new AuthFailureError("Got null auth token for type: " + mAuthTokenType); - } - - return authToken; - } - - @Override - public void invalidateAuthToken(String authToken) { - mAccountManager.invalidateAuthToken(mAccount.type, authToken); - } -} diff --git a/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java b/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java deleted file mode 100644 index bafab8c..0000000 --- a/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 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 additionalHeaders) - throws IOException, AuthFailureError { - final CountDownLatch latch = new CountDownLatch(1); - final AtomicReference 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/Authenticator.java b/src/main/java/com/android/volley/toolbox/Authenticator.java deleted file mode 100644 index 2ba43db..0000000 --- a/src/main/java/com/android/volley/toolbox/Authenticator.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import com.android.volley.AuthFailureError; - -/** An interface for interacting with auth tokens. */ -public interface Authenticator { - /** - * Synchronously retrieves an auth token. - * - * @throws AuthFailureError If authentication did not succeed - */ - String getAuthToken() throws AuthFailureError; - - /** Invalidates the provided auth token. */ - void invalidateAuthToken(String authToken); -} diff --git a/src/main/java/com/android/volley/toolbox/BaseHttpStack.java b/src/main/java/com/android/volley/toolbox/BaseHttpStack.java deleted file mode 100644 index 99a9899..0000000 --- a/src/main/java/com/android/volley/toolbox/BaseHttpStack.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.volley.toolbox; - -import com.android.volley.AuthFailureError; -import com.android.volley.Header; -import com.android.volley.Request; -import java.io.IOException; -import java.io.InputStream; -import java.net.SocketTimeoutException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import org.apache.http.ProtocolVersion; -import org.apache.http.StatusLine; -import org.apache.http.entity.BasicHttpEntity; -import org.apache.http.message.BasicHeader; -import org.apache.http.message.BasicHttpResponse; -import org.apache.http.message.BasicStatusLine; - -/** An HTTP stack abstraction. */ -@SuppressWarnings("deprecation") // for HttpStack -public abstract class BaseHttpStack implements HttpStack { - - /** - * Performs an HTTP request with the given parameters. - * - *

A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise, - * and the Content-Type header is set to request.getPostBodyContentType(). - * - * @param request the request to perform - * @param additionalHeaders additional headers to be sent together with {@link - * Request#getHeaders()} - * @return the {@link HttpResponse} - * @throws SocketTimeoutException if the request times out - * @throws IOException if another I/O error occurs during the request - * @throws AuthFailureError if an authentication failure occurs during the request - */ - public abstract HttpResponse executeRequest( - Request request, Map additionalHeaders) - throws IOException, AuthFailureError; - - /** - * @deprecated use {@link #executeRequest} instead to avoid a dependency on the deprecated - * Apache HTTP library. Nothing in Volley's own source calls this method. However, since - * {@link BasicNetwork#mHttpStack} is exposed to subclasses, we provide this implementation - * in case legacy client apps are dependent on that field. This method may be removed in a - * future release of Volley. - */ - @Deprecated - @Override - public final org.apache.http.HttpResponse performRequest( - Request request, Map additionalHeaders) - throws IOException, AuthFailureError { - HttpResponse response = executeRequest(request, additionalHeaders); - - ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1); - StatusLine statusLine = - new BasicStatusLine( - protocolVersion, response.getStatusCode(), /* reasonPhrase= */ ""); - BasicHttpResponse apacheResponse = new BasicHttpResponse(statusLine); - - List headers = new ArrayList<>(); - for (Header header : response.getHeaders()) { - headers.add(new BasicHeader(header.getName(), header.getValue())); - } - apacheResponse.setHeaders(headers.toArray(new org.apache.http.Header[0])); - - InputStream responseStream = response.getContent(); - if (responseStream != null) { - BasicHttpEntity entity = new BasicHttpEntity(); - entity.setContent(responseStream); - entity.setContentLength(response.getContentLength()); - apacheResponse.setEntity(entity); - } - - return apacheResponse; - } -} diff --git a/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java b/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java deleted file mode 100644 index 55892a0..0000000 --- a/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * 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

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 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
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 extends RequestTask { - InputStream inputStream; - HttpResponse httpResponse; - Request request; - OnRequestComplete callback; - long requestStartMs; - List
responseHeaders; - int statusCode; - - ResponseParsingTask( - InputStream inputStream, - HttpResponse httpResponse, - Request request, - OnRequestComplete callback, - long requestStartMs, - List
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 deleted file mode 100644 index 06427fe..0000000 --- a/src/main/java/com/android/volley/toolbox/BasicNetwork.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import android.os.SystemClock; -import com.android.volley.Header; -import com.android.volley.Network; -import com.android.volley.NetworkResponse; -import com.android.volley.Request; -import com.android.volley.VolleyError; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -/** A network performing Volley requests over an {@link HttpStack}. */ -public class BasicNetwork implements Network { - private static final int DEFAULT_POOL_SIZE = 4096; - - /** - * @deprecated Should never have been exposed in the API. This field may be removed in a future - * release of Volley. - */ - @Deprecated protected final HttpStack mHttpStack; - - private final BaseHttpStack mBaseHttpStack; - - protected final ByteArrayPool mPool; - - /** - * @param httpStack HTTP stack to be used - * @deprecated use {@link #BasicNetwork(BaseHttpStack)} instead to avoid depending on Apache - * HTTP. This method may be removed in a future release of Volley. - */ - @Deprecated - public BasicNetwork(HttpStack httpStack) { - // If a pool isn't passed in, then build a small default pool that will give us a lot of - // benefit and not use too much memory. - this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE)); - } - - /** - * @param httpStack HTTP stack to be used - * @param pool a buffer pool that improves GC performance in copy operations - * @deprecated use {@link #BasicNetwork(BaseHttpStack, ByteArrayPool)} instead to avoid - * depending on Apache HTTP. This method may be removed in a future release of Volley. - */ - @Deprecated - public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) { - mHttpStack = httpStack; - mBaseHttpStack = new AdaptedHttpStack(httpStack); - mPool = pool; - } - - /** @param httpStack HTTP stack to be used */ - public BasicNetwork(BaseHttpStack httpStack) { - // If a pool isn't passed in, then build a small default pool that will give us a lot of - // benefit and not use too much memory. - this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE)); - } - - /** - * @param httpStack HTTP stack to be used - * @param pool a buffer pool that improves GC performance in copy operations - */ - public BasicNetwork(BaseHttpStack httpStack, ByteArrayPool pool) { - mBaseHttpStack = httpStack; - // Populate mHttpStack for backwards compatibility, since it is a protected field. However, - // we won't use it directly here, so clients which don't access it directly won't need to - // depend on Apache HTTP. - mHttpStack = httpStack; - mPool = pool; - } - - @Override - public NetworkResponse performRequest(Request request) throws VolleyError { - long requestStart = SystemClock.elapsedRealtime(); - while (true) { - HttpResponse httpResponse = null; - byte[] responseContents = null; - List
responseHeaders = Collections.emptyList(); - try { - // Gather headers. - Map additionalRequestHeaders = - HttpHeaderParser.getCacheHeaders(request.getCacheEntry()); - httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders); - int statusCode = httpResponse.getStatusCode(); - - responseHeaders = httpResponse.getHeaders(); - // Handle cache validation. - if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) { - long requestDuration = SystemClock.elapsedRealtime() - requestStart; - return NetworkUtility.getNotModifiedNetworkResponse( - request, requestDuration, responseHeaders); - } - - // Some responses such as 204s do not have content. We must check. - InputStream inputStream = httpResponse.getContent(); - if (inputStream != null) { - responseContents = - NetworkUtility.inputStreamToBytes( - inputStream, httpResponse.getContentLength(), mPool); - } else { - // Add 0 byte response as a way of honestly representing a - // no-content request. - responseContents = new byte[0]; - } - - // if the request is slow, log it. - long requestLifetime = SystemClock.elapsedRealtime() - requestStart; - NetworkUtility.logSlowRequests( - requestLifetime, request, responseContents, statusCode); - - if (statusCode < 200 || statusCode > 299) { - throw new IOException(); - } - return new NetworkResponse( - statusCode, - responseContents, - /* notModified= */ false, - SystemClock.elapsedRealtime() - requestStart, - responseHeaders); - } catch (IOException e) { - // This will either throw an exception, breaking us from the loop, or will loop - // again and retry the request. - NetworkUtility.handleException( - request, e, requestStart, httpResponse, responseContents); - } - } - } - - /** - * Converts Headers[] to Map<String, String>. - * - * @deprecated Should never have been exposed in the API. This method may be removed in a future - * release of Volley. - */ - @Deprecated - protected static Map convertHeaders(Header[] headers) { - Map result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - for (int i = 0; i < headers.length; i++) { - result.put(headers[i].getName(), headers[i].getValue()); - } - return result; - } -} diff --git a/src/main/java/com/android/volley/toolbox/ByteArrayPool.java b/src/main/java/com/android/volley/toolbox/ByteArrayPool.java deleted file mode 100644 index 0134fa2..0000000 --- a/src/main/java/com/android/volley/toolbox/ByteArrayPool.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * ByteArrayPool is a source and repository of byte[] objects. Its purpose is to supply - * those buffers to consumers who need to use them for a short period of time and then dispose of - * them. Simply creating and disposing such buffers in the conventional manner can considerable heap - * churn and garbage collection delays on Android, which lacks good management of short-lived heap - * objects. It may be advantageous to trade off some memory in the form of a permanently allocated - * pool of buffers in order to gain heap performance improvements; that is what this class does. - * - *

A good candidate user for this class is something like an I/O system that uses large temporary - * byte[] buffers to copy data around. In these use cases, often the consumer wants the - * buffer to be a certain minimum size to ensure good performance (e.g. when copying data chunks off - * of a stream), but doesn't mind if the buffer is larger than the minimum. Taking this into account - * and also to maximize the odds of being able to reuse a recycled buffer, this class is free to - * return buffers larger than the requested size. The caller needs to be able to gracefully deal - * with getting buffers any size over the minimum. - * - *

If there is not a suitably-sized buffer in its recycling pool when a buffer is requested, this - * class will allocate a new buffer and return it. - * - *

This class has no special ownership of buffers it creates; the caller is free to take a buffer - * it receives from this pool, use it permanently, and never return it to the pool; additionally, it - * is not harmful to return to this pool a buffer that was allocated elsewhere, provided there are - * no other lingering references to it. - * - *

This class ensures that the total size of the buffers in its recycling pool never exceeds a - * certain byte limit. When a buffer is returned that would cause the pool to exceed the limit, - * least-recently-used buffers are disposed. - */ -public class ByteArrayPool { - /** The buffer pool, arranged both by last use and by buffer size */ - private final List mBuffersByLastUse = new ArrayList<>(); - - private final List mBuffersBySize = new ArrayList<>(64); - - /** The total size of the buffers in the pool */ - private int mCurrentSize = 0; - - /** - * The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay - * under this limit. - */ - private final int mSizeLimit; - - /** Compares buffers by size */ - protected static final Comparator BUF_COMPARATOR = - new Comparator() { - @Override - public int compare(byte[] lhs, byte[] rhs) { - return lhs.length - rhs.length; - } - }; - - /** @param sizeLimit the maximum size of the pool, in bytes */ - public ByteArrayPool(int sizeLimit) { - mSizeLimit = sizeLimit; - } - - /** - * Returns a buffer from the pool if one is available in the requested size, or allocates a new - * one if a pooled one is not available. - * - * @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be - * larger. - * @return a byte[] buffer is always returned. - */ - public synchronized byte[] getBuf(int len) { - for (int i = 0; i < mBuffersBySize.size(); i++) { - byte[] buf = mBuffersBySize.get(i); - if (buf.length >= len) { - mCurrentSize -= buf.length; - mBuffersBySize.remove(i); - mBuffersByLastUse.remove(buf); - return buf; - } - } - return new byte[len]; - } - - /** - * Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted - * size. - * - * @param buf the buffer to return to the pool. - */ - public synchronized void returnBuf(byte[] buf) { - if (buf == null || buf.length > mSizeLimit) { - return; - } - mBuffersByLastUse.add(buf); - int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR); - if (pos < 0) { - pos = -pos - 1; - } - mBuffersBySize.add(pos, buf); - mCurrentSize += buf.length; - trim(); - } - - /** Removes buffers from the pool until it is under its size limit. */ - private synchronized void trim() { - while (mCurrentSize > mSizeLimit) { - byte[] buf = mBuffersByLastUse.remove(0); - mBuffersBySize.remove(buf); - mCurrentSize -= buf.length; - } - } -} diff --git a/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java b/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java deleted file mode 100644 index 856ef80..0000000 --- a/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import android.os.Handler; -import android.os.Looper; -import com.android.volley.Cache; -import com.android.volley.NetworkResponse; -import com.android.volley.Request; -import com.android.volley.Response; - -/** A synthetic request used for clearing the cache. */ -public class ClearCacheRequest extends Request { - private final Cache mCache; - private final Runnable mCallback; - - /** - * Creates a synthetic request for clearing the cache. - * - * @param cache Cache to clear - * @param callback Callback to make on the main thread once the cache is clear, or null for none - */ - public ClearCacheRequest(Cache cache, Runnable callback) { - super(Method.GET, null, null); - mCache = cache; - mCallback = callback; - } - - @Override - public boolean isCanceled() { - // This is a little bit of a hack, but hey, why not. - mCache.clear(); - if (mCallback != null) { - Handler handler = new Handler(Looper.getMainLooper()); - handler.postAtFrontOfQueue(mCallback); - } - return true; - } - - @Override - public Priority getPriority() { - return Priority.IMMEDIATE; - } - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - return null; - } - - @Override - protected void deliverResponse(Object response) {} -} diff --git a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java deleted file mode 100644 index d4310e0..0000000 --- a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java +++ /dev/null @@ -1,677 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import android.os.SystemClock; -import android.text.TextUtils; -import androidx.annotation.VisibleForTesting; -import com.android.volley.Cache; -import com.android.volley.Header; -import com.android.volley.VolleyLog; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.DataInputStream; -import java.io.EOFException; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Cache implementation that caches files directly onto the hard disk in the specified directory. - * The default disk usage size is 5MB, but is configurable. - * - *

This cache supports the {@link Entry#allResponseHeaders} headers field. - */ -public class DiskBasedCache implements Cache { - - /** Map of the Key, CacheHeader pairs */ - private final Map mEntries = new LinkedHashMap<>(16, .75f, true); - - /** Total amount of space currently used by the cache in bytes. */ - private long mTotalSize = 0; - - /** The supplier for the root directory to use for the cache. */ - private final FileSupplier mRootDirectorySupplier; - - /** The maximum size of the cache in bytes. */ - private final int mMaxCacheSizeInBytes; - - /** Default maximum disk usage in bytes. */ - private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024; - - /** High water mark percentage for the cache */ - @VisibleForTesting static final float HYSTERESIS_FACTOR = 0.9f; - - /** Magic number for current version of cache file format. */ - private static final int CACHE_MAGIC = 0x20150306; - - /** - * Constructs an instance of the DiskBasedCache at the specified directory. - * - * @param rootDirectory The root directory of the cache. - * @param maxCacheSizeInBytes The maximum size of the cache in bytes. Note that the cache may - * briefly exceed this size on disk when writing a new entry that pushes it over the limit - * until the ensuing pruning completes. - */ - public DiskBasedCache(final File rootDirectory, int maxCacheSizeInBytes) { - mRootDirectorySupplier = - new FileSupplier() { - @Override - public File get() { - return rootDirectory; - } - }; - mMaxCacheSizeInBytes = maxCacheSizeInBytes; - } - - /** - * Constructs an instance of the DiskBasedCache at the specified directory. - * - * @param rootDirectorySupplier The supplier for the root directory of the cache. - * @param maxCacheSizeInBytes The maximum size of the cache in bytes. Note that the cache may - * briefly exceed this size on disk when writing a new entry that pushes it over the limit - * until the ensuing pruning completes. - */ - public DiskBasedCache(FileSupplier rootDirectorySupplier, int maxCacheSizeInBytes) { - mRootDirectorySupplier = rootDirectorySupplier; - mMaxCacheSizeInBytes = maxCacheSizeInBytes; - } - - /** - * Constructs an instance of the DiskBasedCache at the specified directory using the default - * maximum cache size of 5MB. - * - * @param rootDirectory The root directory of the cache. - */ - public DiskBasedCache(File rootDirectory) { - this(rootDirectory, DEFAULT_DISK_USAGE_BYTES); - } - - /** - * Constructs an instance of the DiskBasedCache at the specified directory using the default - * maximum cache size of 5MB. - * - * @param rootDirectorySupplier The supplier for the root directory of the cache. - */ - public DiskBasedCache(FileSupplier rootDirectorySupplier) { - this(rootDirectorySupplier, DEFAULT_DISK_USAGE_BYTES); - } - - /** Clears the cache. Deletes all cached files from disk. */ - @Override - public synchronized void clear() { - File[] files = mRootDirectorySupplier.get().listFiles(); - if (files != null) { - for (File file : files) { - file.delete(); - } - } - mEntries.clear(); - mTotalSize = 0; - VolleyLog.d("Cache cleared."); - } - - /** Returns the cache entry with the specified key if it exists, null otherwise. */ - @Override - public synchronized Entry get(String key) { - CacheHeader entry = mEntries.get(key); - // if the entry does not exist, return. - if (entry == null) { - return null; - } - File file = getFileForKey(key); - try { - CountingInputStream cis = - new CountingInputStream( - new BufferedInputStream(createInputStream(file)), file.length()); - try { - CacheHeader entryOnDisk = CacheHeader.readHeader(cis); - if (!TextUtils.equals(key, entryOnDisk.key)) { - // File was shared by two keys and now holds data for a different entry! - VolleyLog.d( - "%s: key=%s, found=%s", file.getAbsolutePath(), key, entryOnDisk.key); - // Remove key whose contents on disk have been replaced. - removeEntry(key); - return null; - } - byte[] data = streamToBytes(cis, cis.bytesRemaining()); - return entry.toCacheEntry(data); - } finally { - // Any IOException thrown here is handled by the below catch block by design. - //noinspection ThrowFromFinallyBlock - cis.close(); - } - } catch (IOException e) { - VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString()); - remove(key); - return null; - } - } - - /** - * Initializes the DiskBasedCache by scanning for all files currently in the specified root - * directory. Creates the root directory if necessary. - */ - @Override - public synchronized void initialize() { - File rootDirectory = mRootDirectorySupplier.get(); - if (!rootDirectory.exists()) { - if (!rootDirectory.mkdirs()) { - VolleyLog.e("Unable to create cache dir %s", rootDirectory.getAbsolutePath()); - } - return; - } - File[] files = rootDirectory.listFiles(); - if (files == null) { - return; - } - for (File file : files) { - try { - long entrySize = file.length(); - CountingInputStream cis = - new CountingInputStream( - new BufferedInputStream(createInputStream(file)), entrySize); - try { - CacheHeader entry = CacheHeader.readHeader(cis); - entry.size = entrySize; - putEntry(entry.key, entry); - } finally { - // Any IOException thrown here is handled by the below catch block by design. - //noinspection ThrowFromFinallyBlock - cis.close(); - } - } catch (IOException e) { - //noinspection ResultOfMethodCallIgnored - file.delete(); - } - } - } - - /** - * Invalidates an entry in the cache. - * - * @param key Cache key - * @param fullExpire True to fully expire the entry, false to soft expire - */ - @Override - public synchronized void invalidate(String key, boolean fullExpire) { - Entry entry = get(key); - if (entry != null) { - entry.softTtl = 0; - if (fullExpire) { - entry.ttl = 0; - } - put(key, entry); - } - } - - /** Puts the entry with the specified key into the cache. */ - @Override - public synchronized void put(String key, Entry entry) { - // If adding this entry would trigger a prune, but pruning would cause the new entry to be - // deleted, then skip writing the entry in the first place, as this is just churn. - // Note that we don't include the cache header overhead in this calculation for simplicity, - // so putting entries which are just below the threshold may still cause this churn. - if (mTotalSize + entry.data.length > mMaxCacheSizeInBytes - && entry.data.length > mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) { - return; - } - File file = getFileForKey(key); - try { - BufferedOutputStream fos = new BufferedOutputStream(createOutputStream(file)); - CacheHeader e = new CacheHeader(key, entry); - boolean success = e.writeHeader(fos); - if (!success) { - fos.close(); - VolleyLog.d("Failed to write header for %s", file.getAbsolutePath()); - throw new IOException(); - } - fos.write(entry.data); - fos.close(); - e.size = file.length(); - putEntry(key, e); - pruneIfNeeded(); - } catch (IOException e) { - boolean deleted = file.delete(); - if (!deleted) { - VolleyLog.d("Could not clean up file %s", file.getAbsolutePath()); - } - initializeIfRootDirectoryDeleted(); - } - } - - /** Removes the specified key from the cache if it exists. */ - @Override - public synchronized void remove(String key) { - boolean deleted = getFileForKey(key).delete(); - removeEntry(key); - if (!deleted) { - VolleyLog.d( - "Could not delete cache entry for key=%s, filename=%s", - key, getFilenameForKey(key)); - } - } - - /** - * Creates a pseudo-unique filename for the specified cache key. - * - * @param key The key to generate a file name for. - * @return A pseudo-unique filename. - */ - private String getFilenameForKey(String key) { - int firstHalfLength = key.length() / 2; - String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode()); - localFilename += String.valueOf(key.substring(firstHalfLength).hashCode()); - return localFilename; - } - - /** Returns a file object for the given cache key. */ - public File getFileForKey(String key) { - return new File(mRootDirectorySupplier.get(), getFilenameForKey(key)); - } - - /** Re-initialize the cache if the directory was deleted. */ - private void initializeIfRootDirectoryDeleted() { - if (!mRootDirectorySupplier.get().exists()) { - VolleyLog.d("Re-initializing cache after external clearing."); - mEntries.clear(); - mTotalSize = 0; - initialize(); - } - } - - /** Represents a supplier for {@link File}s. */ - public interface FileSupplier { - File get(); - } - - /** Prunes the cache to fit the maximum size. */ - private void pruneIfNeeded() { - if (mTotalSize < mMaxCacheSizeInBytes) { - return; - } - if (VolleyLog.DEBUG) { - VolleyLog.v("Pruning old cache entries."); - } - - long before = mTotalSize; - int prunedFiles = 0; - long startTime = SystemClock.elapsedRealtime(); - - Iterator> iterator = mEntries.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - CacheHeader e = entry.getValue(); - boolean deleted = getFileForKey(e.key).delete(); - if (deleted) { - mTotalSize -= e.size; - } else { - VolleyLog.d( - "Could not delete cache entry for key=%s, filename=%s", - e.key, getFilenameForKey(e.key)); - } - iterator.remove(); - prunedFiles++; - - if (mTotalSize < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) { - break; - } - } - - if (VolleyLog.DEBUG) { - VolleyLog.v( - "pruned %d files, %d bytes, %d ms", - prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime); - } - } - - /** - * Puts the entry with the specified key into the cache. - * - * @param key The key to identify the entry by. - * @param entry The entry to cache. - */ - private void putEntry(String key, CacheHeader entry) { - if (!mEntries.containsKey(key)) { - mTotalSize += entry.size; - } else { - CacheHeader oldEntry = mEntries.get(key); - mTotalSize += (entry.size - oldEntry.size); - } - mEntries.put(key, entry); - } - - /** Removes the entry identified by 'key' from the cache. */ - private void removeEntry(String key) { - CacheHeader removed = mEntries.remove(key); - if (removed != null) { - mTotalSize -= removed.size; - } - } - - /** - * Reads length bytes from CountingInputStream into byte array. - * - * @param cis input stream - * @param length number of bytes to read - * @throws IOException if fails to read all bytes - */ - @VisibleForTesting - static byte[] streamToBytes(CountingInputStream cis, long length) throws IOException { - long maxLength = cis.bytesRemaining(); - // Length cannot be negative or greater than bytes remaining, and must not overflow int. - if (length < 0 || length > maxLength || (int) length != length) { - throw new IOException("streamToBytes length=" + length + ", maxLength=" + maxLength); - } - byte[] bytes = new byte[(int) length]; - new DataInputStream(cis).readFully(bytes); - return bytes; - } - - @VisibleForTesting - InputStream createInputStream(File file) throws FileNotFoundException { - return new FileInputStream(file); - } - - @VisibleForTesting - OutputStream createOutputStream(File file) throws FileNotFoundException { - return new FileOutputStream(file); - } - - /** Handles holding onto the cache headers for an entry. */ - @VisibleForTesting - static class CacheHeader { - /** - * The size of the data identified by this CacheHeader on disk (both header and data). - * - *

Must be set by the caller after it has been calculated. - * - *

This is not serialized to disk. - */ - long size; - - /** The key that identifies the cache entry. */ - final String key; - - /** ETag for cache coherence. */ - final String etag; - - /** Date of this response as reported by the server. */ - final long serverDate; - - /** The last modified date for the requested object. */ - final long lastModified; - - /** TTL for this record. */ - final long ttl; - - /** Soft TTL for this record. */ - final long softTtl; - - /** Headers from the response resulting in this cache entry. */ - final List

allResponseHeaders; - - private CacheHeader( - String key, - String etag, - long serverDate, - long lastModified, - long ttl, - long softTtl, - List
allResponseHeaders) { - this.key = key; - this.etag = "".equals(etag) ? null : etag; - this.serverDate = serverDate; - this.lastModified = lastModified; - this.ttl = ttl; - this.softTtl = softTtl; - this.allResponseHeaders = allResponseHeaders; - } - - /** - * Instantiates a new CacheHeader object. - * - * @param key The key that identifies the cache entry - * @param entry The cache entry. - */ - CacheHeader(String key, Entry entry) { - this( - key, - entry.etag, - entry.serverDate, - entry.lastModified, - entry.ttl, - entry.softTtl, - getAllResponseHeaders(entry)); - } - - private static List
getAllResponseHeaders(Entry entry) { - // If the entry contains all the response headers, use that field directly. - if (entry.allResponseHeaders != null) { - return entry.allResponseHeaders; - } - - // Legacy fallback - copy headers from the map. - return HttpHeaderParser.toAllHeaderList(entry.responseHeaders); - } - - /** - * Reads the header from a CountingInputStream and returns a CacheHeader object. - * - * @param is The InputStream to read from. - * @throws IOException if fails to read header - */ - static CacheHeader readHeader(CountingInputStream is) throws IOException { - int magic = readInt(is); - if (magic != CACHE_MAGIC) { - // don't bother deleting, it'll get pruned eventually - throw new IOException(); - } - String key = readString(is); - String etag = readString(is); - long serverDate = readLong(is); - long lastModified = readLong(is); - long ttl = readLong(is); - long softTtl = readLong(is); - List
allResponseHeaders = readHeaderList(is); - return new CacheHeader( - key, etag, serverDate, lastModified, ttl, softTtl, allResponseHeaders); - } - - /** Creates a cache entry for the specified data. */ - Entry toCacheEntry(byte[] data) { - Entry e = new Entry(); - e.data = data; - e.etag = etag; - e.serverDate = serverDate; - e.lastModified = lastModified; - e.ttl = ttl; - e.softTtl = softTtl; - e.responseHeaders = HttpHeaderParser.toHeaderMap(allResponseHeaders); - e.allResponseHeaders = Collections.unmodifiableList(allResponseHeaders); - return e; - } - - /** Writes the contents of this CacheHeader to the specified OutputStream. */ - boolean writeHeader(OutputStream os) { - try { - writeInt(os, CACHE_MAGIC); - writeString(os, key); - writeString(os, etag == null ? "" : etag); - writeLong(os, serverDate); - writeLong(os, lastModified); - writeLong(os, ttl); - writeLong(os, softTtl); - writeHeaderList(allResponseHeaders, os); - os.flush(); - return true; - } catch (IOException e) { - VolleyLog.d("%s", e.toString()); - return false; - } - } - } - - @VisibleForTesting - static class CountingInputStream extends FilterInputStream { - private final long length; - private long bytesRead; - - CountingInputStream(InputStream in, long length) { - super(in); - this.length = length; - } - - @Override - public int read() throws IOException { - int result = super.read(); - if (result != -1) { - bytesRead++; - } - return result; - } - - @Override - public int read(byte[] buffer, int offset, int count) throws IOException { - int result = super.read(buffer, offset, count); - if (result != -1) { - bytesRead += result; - } - return result; - } - - @VisibleForTesting - long bytesRead() { - return bytesRead; - } - - long bytesRemaining() { - return length - bytesRead; - } - } - - /* - * Homebrewed simple serialization system used for reading and writing cache - * headers on disk. Once upon a time, this used the standard Java - * Object{Input,Output}Stream, but the default implementation relies heavily - * on reflection (even for standard types) and generates a ton of garbage. - * - * TODO: Replace by standard DataInput and DataOutput in next cache version. - */ - - /** - * Simple wrapper around {@link InputStream#read()} that throws EOFException instead of - * returning -1. - */ - private static int read(InputStream is) throws IOException { - int b = is.read(); - if (b == -1) { - throw new EOFException(); - } - return b; - } - - static void writeInt(OutputStream os, int n) throws IOException { - os.write((n >> 0) & 0xff); - os.write((n >> 8) & 0xff); - os.write((n >> 16) & 0xff); - os.write((n >> 24) & 0xff); - } - - static int readInt(InputStream is) throws IOException { - int n = 0; - n |= (read(is) << 0); - n |= (read(is) << 8); - n |= (read(is) << 16); - n |= (read(is) << 24); - return n; - } - - static void writeLong(OutputStream os, long n) throws IOException { - os.write((byte) (n >>> 0)); - os.write((byte) (n >>> 8)); - os.write((byte) (n >>> 16)); - os.write((byte) (n >>> 24)); - os.write((byte) (n >>> 32)); - os.write((byte) (n >>> 40)); - os.write((byte) (n >>> 48)); - os.write((byte) (n >>> 56)); - } - - static long readLong(InputStream is) throws IOException { - long n = 0; - n |= ((read(is) & 0xFFL) << 0); - n |= ((read(is) & 0xFFL) << 8); - n |= ((read(is) & 0xFFL) << 16); - n |= ((read(is) & 0xFFL) << 24); - n |= ((read(is) & 0xFFL) << 32); - n |= ((read(is) & 0xFFL) << 40); - n |= ((read(is) & 0xFFL) << 48); - n |= ((read(is) & 0xFFL) << 56); - return n; - } - - static void writeString(OutputStream os, String s) throws IOException { - byte[] b = s.getBytes("UTF-8"); - writeLong(os, b.length); - os.write(b, 0, b.length); - } - - static String readString(CountingInputStream cis) throws IOException { - long n = readLong(cis); - byte[] b = streamToBytes(cis, n); - return new String(b, "UTF-8"); - } - - static void writeHeaderList(List
headers, OutputStream os) throws IOException { - if (headers != null) { - writeInt(os, headers.size()); - for (Header header : headers) { - writeString(os, header.getName()); - writeString(os, header.getValue()); - } - } else { - writeInt(os, 0); - } - } - - static List
readHeaderList(CountingInputStream cis) throws IOException { - int size = readInt(cis); - if (size < 0) { - throw new IOException("readHeaderList size=" + size); - } - List
result = - (size == 0) ? Collections.
emptyList() : new ArrayList
(); - for (int i = 0; i < size; i++) { - String name = readString(cis).intern(); - String value = readString(cis).intern(); - result.add(new Header(name, value)); - } - return result; - } -} diff --git a/src/main/java/com/android/volley/toolbox/FileSupplier.java b/src/main/java/com/android/volley/toolbox/FileSupplier.java deleted file mode 100644 index 70898a6..0000000 --- a/src/main/java/com/android/volley/toolbox/FileSupplier.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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/HttpClientStack.java b/src/main/java/com/android/volley/toolbox/HttpClientStack.java deleted file mode 100644 index 1e9e4b0..0000000 --- a/src/main/java/com/android/volley/toolbox/HttpClientStack.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import com.android.volley.AuthFailureError; -import com.android.volley.Request; -import com.android.volley.Request.Method; -import java.io.IOException; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpHead; -import org.apache.http.client.methods.HttpOptions; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpTrace; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; - -/** - * An HttpStack that performs request over an {@link HttpClient}. - * - * @deprecated The Apache HTTP library on Android is deprecated. Use {@link HurlStack} or another - * {@link BaseHttpStack} implementation. - */ -@Deprecated -public class HttpClientStack implements HttpStack { - protected final HttpClient mClient; - - private static final String HEADER_CONTENT_TYPE = "Content-Type"; - - public HttpClientStack(HttpClient client) { - mClient = client; - } - - private static void setHeaders(HttpUriRequest httpRequest, Map headers) { - for (String key : headers.keySet()) { - httpRequest.setHeader(key, headers.get(key)); - } - } - - @SuppressWarnings("unused") - private static List getPostParameterPairs(Map postParams) { - List result = new ArrayList<>(postParams.size()); - for (String key : postParams.keySet()) { - result.add(new BasicNameValuePair(key, postParams.get(key))); - } - return result; - } - - @Override - public HttpResponse performRequest(Request request, Map additionalHeaders) - throws IOException, AuthFailureError { - HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders); - setHeaders(httpRequest, additionalHeaders); - // Request.getHeaders() takes precedence over the given additional (cache) headers) and any - // headers set by createHttpRequest (like the Content-Type header). - setHeaders(httpRequest, request.getHeaders()); - onPrepareRequest(httpRequest); - HttpParams httpParams = httpRequest.getParams(); - int timeoutMs = request.getTimeoutMs(); - // TODO: Reevaluate this connection timeout based on more wide-scale - // data collection and possibly different for wifi vs. 3G. - HttpConnectionParams.setConnectionTimeout(httpParams, 5000); - HttpConnectionParams.setSoTimeout(httpParams, timeoutMs); - return mClient.execute(httpRequest); - } - - /** Creates the appropriate subclass of HttpUriRequest for passed in request. */ - @SuppressWarnings("deprecation") - /* protected */ static HttpUriRequest createHttpRequest( - Request request, Map additionalHeaders) throws AuthFailureError { - switch (request.getMethod()) { - case Method.DEPRECATED_GET_OR_POST: - { - // This is the deprecated way that needs to be handled for backwards - // compatibility. - // If the request's post body is null, then the assumption is that the request - // is - // GET. Otherwise, it is assumed that the request is a POST. - byte[] postBody = request.getPostBody(); - if (postBody != null) { - HttpPost postRequest = new HttpPost(request.getUrl()); - postRequest.addHeader( - HEADER_CONTENT_TYPE, request.getPostBodyContentType()); - HttpEntity entity; - entity = new ByteArrayEntity(postBody); - postRequest.setEntity(entity); - return postRequest; - } else { - return new HttpGet(request.getUrl()); - } - } - case Method.GET: - return new HttpGet(request.getUrl()); - case Method.DELETE: - return new HttpDelete(request.getUrl()); - case Method.POST: - { - HttpPost postRequest = new HttpPost(request.getUrl()); - postRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); - setEntityIfNonEmptyBody(postRequest, request); - return postRequest; - } - case Method.PUT: - { - HttpPut putRequest = new HttpPut(request.getUrl()); - putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); - setEntityIfNonEmptyBody(putRequest, request); - return putRequest; - } - case Method.HEAD: - return new HttpHead(request.getUrl()); - case Method.OPTIONS: - return new HttpOptions(request.getUrl()); - case Method.TRACE: - return new HttpTrace(request.getUrl()); - case Method.PATCH: - { - HttpPatch patchRequest = new HttpPatch(request.getUrl()); - patchRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); - setEntityIfNonEmptyBody(patchRequest, request); - return patchRequest; - } - default: - throw new IllegalStateException("Unknown request method."); - } - } - - private static void setEntityIfNonEmptyBody( - HttpEntityEnclosingRequestBase httpRequest, Request request) - throws AuthFailureError { - byte[] body = request.getBody(); - if (body != null) { - HttpEntity entity = new ByteArrayEntity(body); - httpRequest.setEntity(entity); - } - } - - /** - * Called before the request is executed using the underlying HttpClient. - * - *

Overwrite in subclasses to augment the request. - */ - protected void onPrepareRequest(HttpUriRequest request) throws IOException { - // Nothing. - } - - /** - * The HttpPatch class does not exist in the Android framework, so this has been defined here. - */ - public static final class HttpPatch extends HttpEntityEnclosingRequestBase { - - public static final String METHOD_NAME = "PATCH"; - - public HttpPatch() { - super(); - } - - public HttpPatch(final URI uri) { - super(); - setURI(uri); - } - - /** @throws IllegalArgumentException if the uri is invalid. */ - public HttpPatch(final String uri) { - super(); - setURI(URI.create(uri)); - } - - @Override - public String getMethod() { - return METHOD_NAME; - } - } -} diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java deleted file mode 100644 index 0b29e80..0000000 --- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import androidx.annotation.RestrictTo.Scope; -import com.android.volley.Cache; -import com.android.volley.Header; -import com.android.volley.NetworkResponse; -import com.android.volley.VolleyLog; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.TimeZone; -import java.util.TreeMap; -import java.util.TreeSet; - -/** Utility methods for parsing HTTP headers. */ -public class HttpHeaderParser { - - @RestrictTo({Scope.LIBRARY_GROUP}) - public static final String HEADER_CONTENT_TYPE = "Content-Type"; - - private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1"; - - private static final String RFC1123_PARSE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; - - // Hardcode 'GMT' rather than using 'zzz' since some platforms append an extraneous +00:00. - // See #287. - private static final String RFC1123_OUTPUT_FORMAT = "EEE, dd MMM yyyy HH:mm:ss 'GMT'"; - - /** - * Extracts a {@link com.android.volley.Cache.Entry} from a {@link NetworkResponse}. - * - * @param response The network response to parse headers from - * @return a cache entry for the given response, or null if the response is not cacheable. - */ - @Nullable - public static Cache.Entry parseCacheHeaders(NetworkResponse response) { - long now = System.currentTimeMillis(); - - Map headers = response.headers; - if (headers == null) { - return null; - } - - long serverDate = 0; - long lastModified = 0; - long serverExpires = 0; - long softExpire = 0; - long finalExpire = 0; - long maxAge = 0; - long staleWhileRevalidate = 0; - boolean hasCacheControl = false; - boolean mustRevalidate = false; - - String serverEtag = null; - String headerValue; - - headerValue = headers.get("Date"); - if (headerValue != null) { - serverDate = parseDateAsEpoch(headerValue); - } - - headerValue = headers.get("Cache-Control"); - if (headerValue != null) { - hasCacheControl = true; - String[] tokens = headerValue.split(",", 0); - for (int i = 0; i < tokens.length; i++) { - String token = tokens[i].trim(); - if (token.equals("no-cache") || token.equals("no-store")) { - return null; - } else if (token.startsWith("max-age=")) { - try { - maxAge = Long.parseLong(token.substring(8)); - } catch (Exception e) { - } - } else if (token.startsWith("stale-while-revalidate=")) { - try { - staleWhileRevalidate = Long.parseLong(token.substring(23)); - } catch (Exception e) { - } - } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) { - mustRevalidate = true; - } - } - } - - headerValue = headers.get("Expires"); - if (headerValue != null) { - serverExpires = parseDateAsEpoch(headerValue); - } - - headerValue = headers.get("Last-Modified"); - if (headerValue != null) { - lastModified = parseDateAsEpoch(headerValue); - } - - serverEtag = headers.get("ETag"); - - // Cache-Control takes precedence over an Expires header, even if both exist and Expires - // is more restrictive. - if (hasCacheControl) { - softExpire = now + maxAge * 1000; - finalExpire = mustRevalidate ? softExpire : softExpire + staleWhileRevalidate * 1000; - } else if (serverDate > 0 && serverExpires >= serverDate) { - // Default semantic for Expire header in HTTP specification is softExpire. - softExpire = now + (serverExpires - serverDate); - finalExpire = softExpire; - } - - Cache.Entry entry = new Cache.Entry(); - entry.data = response.data; - entry.etag = serverEtag; - entry.softTtl = softExpire; - entry.ttl = finalExpire; - entry.serverDate = serverDate; - entry.lastModified = lastModified; - entry.responseHeaders = headers; - entry.allResponseHeaders = response.allHeaders; - - return entry; - } - - /** Parse date in RFC1123 format, and return its value as epoch */ - public static long parseDateAsEpoch(String dateStr) { - try { - // Parse date in RFC1123 format if this header contains one - return newUsGmtFormatter(RFC1123_PARSE_FORMAT).parse(dateStr).getTime(); - } catch (ParseException e) { - // Date in invalid format, fallback to 0 - // If the value is either "0" or "-1" we only log to verbose, - // these values are pretty common and cause log spam. - String message = "Unable to parse dateStr: %s, falling back to 0"; - if ("0".equals(dateStr) || "-1".equals(dateStr)) { - VolleyLog.v(message, dateStr); - } else { - VolleyLog.e(e, message, dateStr); - } - - return 0; - } - } - - /** Format an epoch date in RFC1123 format. */ - static String formatEpochAsRfc1123(long epoch) { - return newUsGmtFormatter(RFC1123_OUTPUT_FORMAT).format(new Date(epoch)); - } - - private static SimpleDateFormat newUsGmtFormatter(String format) { - SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.US); - formatter.setTimeZone(TimeZone.getTimeZone("GMT")); - return formatter; - } - - /** - * Retrieve a charset from headers - * - * @param headers An {@link java.util.Map} of headers - * @param defaultCharset Charset to return if none can be found - * @return Returns the charset specified in the Content-Type of this header, or the - * defaultCharset if none can be found. - */ - public static String parseCharset( - @Nullable Map headers, String defaultCharset) { - if (headers == null) { - return defaultCharset; - } - String contentType = headers.get(HEADER_CONTENT_TYPE); - if (contentType != null) { - String[] params = contentType.split(";", 0); - for (int i = 1; i < params.length; i++) { - String[] pair = params[i].trim().split("=", 0); - if (pair.length == 2) { - if (pair[0].equals("charset")) { - return pair[1]; - } - } - } - } - - return defaultCharset; - } - - /** - * Returns the charset specified in the Content-Type of this header, or the HTTP default - * (ISO-8859-1) if none can be found. - */ - public static String parseCharset(@Nullable Map headers) { - return parseCharset(headers, DEFAULT_CONTENT_CHARSET); - } - - // Note - these are copied from NetworkResponse to avoid making them public (as needed to access - // them from the .toolbox package), which would mean they'd become part of the Volley API. - // TODO: Consider obfuscating official releases so we can share utility methods between Volley - // and Toolbox without making them public APIs. - - static Map toHeaderMap(List

allHeaders) { - Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - // Later elements in the list take precedence. - for (Header header : allHeaders) { - headers.put(header.getName(), header.getValue()); - } - return headers; - } - - static List
toAllHeaderList(Map headers) { - List
allHeaders = new ArrayList<>(headers.size()); - for (Map.Entry header : headers.entrySet()) { - allHeaders.add(new Header(header.getKey(), header.getValue())); - } - return allHeaders; - } - - /** - * Combine cache headers with network response headers for an HTTP 304 response. - * - *

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

combineHeaders(List
responseHeaders, Cache.Entry entry) { - // First, create a case-insensitive set of header names from the network - // response. - Set 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
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 header : entry.responseHeaders.entrySet()) { - if (!headerNamesFromNetworkResponse.contains(header.getKey())) { - combinedHeaders.add(new Header(header.getKey(), header.getValue())); - } - } - } - } - return combinedHeaders; - } - - static Map getCacheHeaders(Cache.Entry entry) { - // If there's no cache entry, we're done. - if (entry == null) { - return Collections.emptyMap(); - } - - Map 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 deleted file mode 100644 index 595f926..0000000 --- a/src/main/java/com/android/volley/toolbox/HttpResponse.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.volley.toolbox; - -import androidx.annotation.Nullable; -import com.android.volley.Header; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.Collections; -import java.util.List; - -/** A response from an HTTP server. */ -public final class HttpResponse { - - private final int mStatusCode; - private final List
mHeaders; - private final int mContentLength; - @Nullable private final InputStream mContent; - @Nullable private final byte[] mContentBytes; - - /** - * Construct a new HttpResponse for an empty response body. - * - * @param statusCode the HTTP status code of the response - * @param headers the response headers - */ - public HttpResponse(int statusCode, List
headers) { - this(statusCode, headers, /* contentLength= */ -1, /* content= */ null); - } - - /** - * Construct a new HttpResponse. - * - * @param statusCode the HTTP status code of the response - * @param headers the response headers - * @param contentLength the length of the response content. Ignored if there is no content. - * @param content an {@link InputStream} of the response content. May be null to indicate that - * the response has no content. - */ - public HttpResponse( - int statusCode, List
headers, int contentLength, InputStream content) { - mStatusCode = statusCode; - mHeaders = headers; - mContentLength = contentLength; - mContent = content; - mContentBytes = null; - } - - /** - * Construct a new HttpResponse. - * - * @param statusCode the HTTP status code of the response - * @param headers the response headers - * @param contentBytes a byte[] of the response content. This is an optimization for HTTP stacks - * that natively support returning a byte[]. - */ - public HttpResponse(int statusCode, List
headers, byte[] contentBytes) { - mStatusCode = statusCode; - mHeaders = headers; - mContentLength = contentBytes.length; - mContentBytes = contentBytes; - mContent = null; - } - - /** Returns the HTTP status code of the response. */ - public final int getStatusCode() { - return mStatusCode; - } - - /** Returns the response headers. Must not be mutated directly. */ - public final List
getHeaders() { - return Collections.unmodifiableList(mHeaders); - } - - /** Returns the length of the content. Only valid if {@link #getContent} is non-null. */ - public final int getContentLength() { - return mContentLength; - } - - /** - * If a byte[] was already provided by an HTTP stack that natively supports returning one, this - * method will return that byte[] as an optimization over copying the bytes from an input - * stream. It may return null, even if the response has content, as long as mContent is - * provided. - */ - @Nullable - public final byte[] getContentBytes() { - return mContentBytes; - } - - /** - * Returns an {@link InputStream} of the response content. May be null to indicate that the - * response has no content. - */ - @Nullable - public final InputStream getContent() { - if (mContent != null) { - return mContent; - } else if (mContentBytes != null) { - return new ByteArrayInputStream(mContentBytes); - } else { - return null; - } - } -} diff --git a/src/main/java/com/android/volley/toolbox/HttpStack.java b/src/main/java/com/android/volley/toolbox/HttpStack.java deleted file mode 100644 index 85179a7..0000000 --- a/src/main/java/com/android/volley/toolbox/HttpStack.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import com.android.volley.AuthFailureError; -import com.android.volley.Request; -import java.io.IOException; -import java.util.Map; -import org.apache.http.HttpResponse; - -/** - * An HTTP stack abstraction. - * - * @deprecated This interface should be avoided as it depends on the deprecated Apache HTTP library. - * Use {@link BaseHttpStack} to avoid this dependency. This class may be removed in a future - * release of Volley. - */ -@Deprecated -public interface HttpStack { - /** - * Performs an HTTP request with the given parameters. - * - *

A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise, - * and the Content-Type header is set to request.getPostBodyContentType(). - * - * @param request the request to perform - * @param additionalHeaders additional headers to be sent together with {@link - * Request#getHeaders()} - * @return the HTTP response - */ - HttpResponse performRequest(Request request, Map additionalHeaders) - throws IOException, AuthFailureError; -} diff --git a/src/main/java/com/android/volley/toolbox/HurlStack.java b/src/main/java/com/android/volley/toolbox/HurlStack.java deleted file mode 100644 index 35c6a72..0000000 --- a/src/main/java/com/android/volley/toolbox/HurlStack.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import androidx.annotation.VisibleForTesting; -import com.android.volley.AuthFailureError; -import com.android.volley.Header; -import com.android.volley.Request; -import com.android.volley.Request.Method; -import java.io.DataOutputStream; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLSocketFactory; - -/** A {@link BaseHttpStack} based on {@link HttpURLConnection}. */ -public class HurlStack extends BaseHttpStack { - - private static final int HTTP_CONTINUE = 100; - - /** An interface for transforming URLs before use. */ - public interface UrlRewriter extends com.android.volley.toolbox.UrlRewriter {} - - private final UrlRewriter mUrlRewriter; - private final SSLSocketFactory mSslSocketFactory; - - public HurlStack() { - this(/* urlRewriter = */ null); - } - - /** @param urlRewriter Rewriter to use for request URLs */ - public HurlStack(UrlRewriter urlRewriter) { - this(urlRewriter, /* sslSocketFactory = */ null); - } - - /** - * @param urlRewriter Rewriter to use for request URLs - * @param sslSocketFactory SSL factory to use for HTTPS connections - */ - public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) { - mUrlRewriter = urlRewriter; - mSslSocketFactory = sslSocketFactory; - } - - @Override - public HttpResponse executeRequest(Request request, Map additionalHeaders) - throws IOException, AuthFailureError { - String url = request.getUrl(); - HashMap map = new HashMap<>(); - map.putAll(additionalHeaders); - // Request.getHeaders() takes precedence over the given additional (cache) headers). - map.putAll(request.getHeaders()); - if (mUrlRewriter != null) { - String rewritten = mUrlRewriter.rewriteUrl(url); - if (rewritten == null) { - throw new IOException("URL blocked by rewriter: " + url); - } - url = rewritten; - } - URL parsedUrl = new URL(url); - HttpURLConnection connection = openConnection(parsedUrl, request); - boolean keepConnectionOpen = false; - try { - for (String headerName : map.keySet()) { - connection.setRequestProperty(headerName, map.get(headerName)); - } - setConnectionParametersForRequest(connection, request); - // Initialize HttpResponse with data from the HttpURLConnection. - int responseCode = connection.getResponseCode(); - if (responseCode == -1) { - // -1 is returned by getResponseCode() if the response code could not be retrieved. - // Signal to the caller that something was wrong with the connection. - throw new IOException("Could not retrieve response code from HttpUrlConnection."); - } - - if (!hasResponseBody(request.getMethod(), responseCode)) { - return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields())); - } - - // Need to keep the connection open until the stream is consumed by the caller. Wrap the - // stream such that close() will disconnect the connection. - keepConnectionOpen = true; - return new HttpResponse( - responseCode, - convertHeaders(connection.getHeaderFields()), - connection.getContentLength(), - createInputStream(request, connection)); - } finally { - if (!keepConnectionOpen) { - connection.disconnect(); - } - } - } - - @VisibleForTesting - static List

convertHeaders(Map> responseHeaders) { - List
headerList = new ArrayList<>(responseHeaders.size()); - for (Map.Entry> entry : responseHeaders.entrySet()) { - // HttpUrlConnection includes the status line as a header with a null key; omit it here - // since it's not really a header and the rest of Volley assumes non-null keys. - if (entry.getKey() != null) { - for (String value : entry.getValue()) { - headerList.add(new Header(entry.getKey(), value)); - } - } - } - return headerList; - } - - /** - * Checks if a response message contains a body. - * - * @see RFC 7230 section 3.3 - * @param requestMethod request method - * @param responseCode response status code - * @return whether the response has a body - */ - private static boolean hasResponseBody(int requestMethod, int responseCode) { - return requestMethod != Request.Method.HEAD - && !(HTTP_CONTINUE <= responseCode && responseCode < HttpURLConnection.HTTP_OK) - && responseCode != HttpURLConnection.HTTP_NO_CONTENT - && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED; - } - - /** - * Wrapper for a {@link HttpURLConnection}'s InputStream which disconnects the connection on - * stream close. - */ - static class UrlConnectionInputStream extends FilterInputStream { - private final HttpURLConnection mConnection; - - UrlConnectionInputStream(HttpURLConnection connection) { - super(inputStreamFromConnection(connection)); - mConnection = connection; - } - - @Override - public void close() throws IOException { - super.close(); - mConnection.disconnect(); - } - } - - /** - * Create and return an InputStream from which the response will be read. - * - *

May be overridden by subclasses to manipulate or monitor this input stream. - * - * @param request current request. - * @param connection current connection of request. - * @return an InputStream from which the response will be read. - */ - protected InputStream createInputStream(Request request, HttpURLConnection connection) { - return new UrlConnectionInputStream(connection); - } - - /** - * Initializes an {@link InputStream} from the given {@link HttpURLConnection}. - * - * @param connection - * @return an HttpEntity populated with data from connection. - */ - private static InputStream inputStreamFromConnection(HttpURLConnection connection) { - InputStream inputStream; - try { - inputStream = connection.getInputStream(); - } catch (IOException ioe) { - inputStream = connection.getErrorStream(); - } - return inputStream; - } - - /** Create an {@link HttpURLConnection} for the specified {@code url}. */ - protected HttpURLConnection createConnection(URL url) throws IOException { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - - // Workaround for the M release HttpURLConnection not observing the - // HttpURLConnection.setFollowRedirects() property. - // https://code.google.com/p/android/issues/detail?id=194495 - connection.setInstanceFollowRedirects(HttpURLConnection.getFollowRedirects()); - - return connection; - } - - /** - * Opens an {@link HttpURLConnection} with parameters. - * - * @param url - * @return an open connection - * @throws IOException - */ - private HttpURLConnection openConnection(URL url, Request request) throws IOException { - HttpURLConnection connection = createConnection(url); - - int timeoutMs = request.getTimeoutMs(); - connection.setConnectTimeout(timeoutMs); - connection.setReadTimeout(timeoutMs); - connection.setUseCaches(false); - connection.setDoInput(true); - - // use caller-provided custom SslSocketFactory, if any, for HTTPS - if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) { - ((HttpsURLConnection) connection).setSSLSocketFactory(mSslSocketFactory); - } - - return connection; - } - - // NOTE: Any request headers added here (via setRequestProperty or addRequestProperty) should be - // checked against the existing properties in the connection and not overridden if already set. - @SuppressWarnings("deprecation") - /* package */ void setConnectionParametersForRequest( - HttpURLConnection connection, Request request) throws IOException, AuthFailureError { - switch (request.getMethod()) { - case Method.DEPRECATED_GET_OR_POST: - // This is the deprecated way that needs to be handled for backwards compatibility. - // If the request's post body is null, then the assumption is that the request is - // GET. Otherwise, it is assumed that the request is a POST. - byte[] postBody = request.getPostBody(); - if (postBody != null) { - connection.setRequestMethod("POST"); - addBody(connection, request, postBody); - } - break; - case Method.GET: - // Not necessary to set the request method because connection defaults to GET but - // being explicit here. - connection.setRequestMethod("GET"); - break; - case Method.DELETE: - connection.setRequestMethod("DELETE"); - break; - case Method.POST: - connection.setRequestMethod("POST"); - addBodyIfExists(connection, request); - break; - case Method.PUT: - connection.setRequestMethod("PUT"); - addBodyIfExists(connection, request); - break; - case Method.HEAD: - connection.setRequestMethod("HEAD"); - break; - case Method.OPTIONS: - connection.setRequestMethod("OPTIONS"); - break; - case Method.TRACE: - connection.setRequestMethod("TRACE"); - break; - case Method.PATCH: - connection.setRequestMethod("PATCH"); - addBodyIfExists(connection, request); - break; - default: - throw new IllegalStateException("Unknown method type."); - } - } - - private void addBodyIfExists(HttpURLConnection connection, Request request) - throws IOException, AuthFailureError { - byte[] body = request.getBody(); - if (body != null) { - addBody(connection, request, body); - } - } - - private void addBody(HttpURLConnection connection, Request request, byte[] body) - throws IOException { - // Prepare output. There is no need to set Content-Length explicitly, - // since this is handled by HttpURLConnection using the size of the prepared - // output stream. - connection.setDoOutput(true); - // Set the content-type unless it was already set (by Request#getHeaders). - if (!connection.getRequestProperties().containsKey(HttpHeaderParser.HEADER_CONTENT_TYPE)) { - connection.setRequestProperty( - HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType()); - } - DataOutputStream out = - new DataOutputStream(createOutputStream(request, connection, body.length)); - out.write(body); - out.close(); - } - - /** - * Create and return an OutputStream to which the request body will be written. - * - *

May be overridden by subclasses to manipulate or monitor this output stream. - * - * @param request current request. - * @param connection current connection of request. - * @param length size of stream to write. - * @return an OutputStream to which the request body will be written. - * @throws IOException if an I/O error occurs while creating the stream. - */ - protected OutputStream createOutputStream( - Request request, HttpURLConnection connection, int length) throws IOException { - return connection.getOutputStream(); - } -} diff --git a/src/main/java/com/android/volley/toolbox/ImageLoader.java b/src/main/java/com/android/volley/toolbox/ImageLoader.java deleted file mode 100644 index eece2cf..0000000 --- a/src/main/java/com/android/volley/toolbox/ImageLoader.java +++ /dev/null @@ -1,541 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.volley.toolbox; - -import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; -import android.os.Handler; -import android.os.Looper; -import android.widget.ImageView; -import android.widget.ImageView.ScaleType; -import androidx.annotation.MainThread; -import androidx.annotation.Nullable; -import com.android.volley.Request; -import com.android.volley.RequestQueue; -import com.android.volley.Response.ErrorListener; -import com.android.volley.Response.Listener; -import com.android.volley.ResponseDelivery; -import com.android.volley.VolleyError; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -/** - * Helper that handles loading and caching images from remote URLs. - * - *

The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)} and - * to pass in the default image listener provided by {@link ImageLoader#getImageListener(ImageView, - * int, int)}. Note that all function calls to this class must be made from the main thread, and all - * responses will be delivered to the main thread as well. Custom {@link ResponseDelivery}s which - * don't use the main thread are not supported. - */ -public class ImageLoader { - /** RequestQueue for dispatching ImageRequests onto. */ - private final RequestQueue mRequestQueue; - - /** Amount of time to wait after first response arrives before delivering all responses. */ - private int mBatchResponseDelayMs = 100; - - /** The cache implementation to be used as an L1 cache before calling into volley. */ - private final ImageCache mCache; - - /** - * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so that we can - * coalesce multiple requests to the same URL into a single network request. - */ - private final HashMap mInFlightRequests = new HashMap<>(); - - /** HashMap of the currently pending responses (waiting to be delivered). */ - private final HashMap mBatchedResponses = new HashMap<>(); - - /** Handler to the main thread. */ - private final Handler mHandler = new Handler(Looper.getMainLooper()); - - /** Runnable for in-flight response delivery. */ - private Runnable mRunnable; - - /** - * Simple cache adapter interface. If provided to the ImageLoader, it will be used as an L1 - * cache before dispatch to Volley. Implementations must not block. Implementation with an - * LruCache is recommended. - */ - public interface ImageCache { - @Nullable - Bitmap getBitmap(String url); - - void putBitmap(String url, Bitmap bitmap); - } - - /** - * Constructs a new ImageLoader. - * - * @param queue The RequestQueue to use for making image requests. - * @param imageCache The cache to use as an L1 cache. - */ - public ImageLoader(RequestQueue queue, ImageCache imageCache) { - mRequestQueue = queue; - mCache = imageCache; - } - - /** - * The default implementation of ImageListener which handles basic functionality of showing a - * default image until the network response is received, at which point it will switch to either - * the actual image or the error image. - * - * @param view The imageView that the listener is associated with. - * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist. - * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist. - */ - public static ImageListener getImageListener( - final ImageView view, final int defaultImageResId, final int errorImageResId) { - return new ImageListener() { - @Override - public void onErrorResponse(VolleyError error) { - if (errorImageResId != 0) { - view.setImageResource(errorImageResId); - } - } - - @Override - public void onResponse(ImageContainer response, boolean isImmediate) { - if (response.getBitmap() != null) { - view.setImageBitmap(response.getBitmap()); - } else if (defaultImageResId != 0) { - view.setImageResource(defaultImageResId); - } - } - }; - } - - /** - * Interface for the response handlers on image requests. - * - *

The call flow is this: 1. Upon being attached to a request, onResponse(response, true) - * will be invoked to reflect any cached data that was already available. If the data was - * available, response.getBitmap() will be non-null. - * - *

2. After a network response returns, only one of the following cases will happen: - - * onResponse(response, false) will be called if the image was loaded. or - onErrorResponse will - * be called if there was an error loading the image. - */ - public interface ImageListener extends ErrorListener { - /** - * Listens for non-error changes to the loading of the image request. - * - * @param response Holds all information pertaining to the request, as well as the bitmap - * (if it is loaded). - * @param isImmediate True if this was called during ImageLoader.get() variants. This can be - * used to differentiate between a cached image loading and a network image loading in - * order to, for example, run an animation to fade in network loaded images. - */ - void onResponse(ImageContainer response, boolean isImmediate); - } - - /** - * Checks if the item is available in the cache. - * - * @param requestUrl The url of the remote image - * @param maxWidth The maximum width of the returned image. - * @param maxHeight The maximum height of the returned image. - * @return True if the item exists in cache, false otherwise. - */ - public boolean isCached(String requestUrl, int maxWidth, int maxHeight) { - return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); - } - - /** - * Checks if the item is available in the cache. - * - *

Must be called from the main thread. - * - * @param requestUrl The url of the remote image - * @param maxWidth The maximum width of the returned image. - * @param maxHeight The maximum height of the returned image. - * @param scaleType The scaleType of the imageView. - * @return True if the item exists in cache, false otherwise. - */ - @MainThread - public boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) { - Threads.throwIfNotOnMainThread(); - - String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); - return mCache.getBitmap(cacheKey) != null; - } - - /** - * Returns an ImageContainer for the requested URL. - * - *

The ImageContainer will contain either the specified default bitmap or the loaded bitmap. - * If the default was returned, the {@link ImageLoader} will be invoked when the request is - * fulfilled. - * - * @param requestUrl The URL of the image to be loaded. - */ - public ImageContainer get(String requestUrl, final ImageListener listener) { - return get(requestUrl, listener, /* maxWidth= */ 0, /* maxHeight= */ 0); - } - - /** - * Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with {@code - * Scaletype == ScaleType.CENTER_INSIDE}. - */ - public ImageContainer get( - String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight) { - return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); - } - - /** - * Issues a bitmap request with the given URL if that image is not available in the cache, and - * returns a bitmap container that contains all of the data relating to the request (as well as - * the default image if the requested image is not available). - * - *

Must be called from the main thread. - * - * @param requestUrl The url of the remote image - * @param imageListener The listener to call when the remote image is loaded - * @param maxWidth The maximum width of the returned image. - * @param maxHeight The maximum height of the returned image. - * @param scaleType The ImageViews ScaleType used to calculate the needed image size. - * @return A container object that contains all of the properties of the request, as well as the - * currently available image (default if remote is not loaded). - */ - @MainThread - public ImageContainer get( - String requestUrl, - ImageListener imageListener, - int maxWidth, - int maxHeight, - ScaleType scaleType) { - - // only fulfill requests that were initiated from the main thread. - Threads.throwIfNotOnMainThread(); - - final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); - - // Try to look up the request in the cache of remote images. - Bitmap cachedBitmap = mCache.getBitmap(cacheKey); - if (cachedBitmap != null) { - // Return the cached bitmap. - ImageContainer container = - new ImageContainer( - cachedBitmap, requestUrl, /* cacheKey= */ null, /* listener= */ null); - imageListener.onResponse(container, true); - return container; - } - - // The bitmap did not exist in the cache, fetch it! - ImageContainer imageContainer = - new ImageContainer(null, requestUrl, cacheKey, imageListener); - - // Update the caller to let them know that they should use the default bitmap. - imageListener.onResponse(imageContainer, true); - - // Check to see if a request is already in-flight or completed but pending batch delivery. - BatchedImageRequest request = mInFlightRequests.get(cacheKey); - if (request == null) { - request = mBatchedResponses.get(cacheKey); - } - if (request != null) { - // If it is, add this request to the list of listeners. - request.addContainer(imageContainer); - return imageContainer; - } - - // The request is not already in flight. Send the new request to the network and - // track it. - Request newRequest = - makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey); - - mRequestQueue.add(newRequest); - mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); - return imageContainer; - } - - protected Request makeImageRequest( - String requestUrl, - int maxWidth, - int maxHeight, - ScaleType scaleType, - final String cacheKey) { - return new ImageRequest( - requestUrl, - new Listener() { - @Override - public void onResponse(Bitmap response) { - onGetImageSuccess(cacheKey, response); - } - }, - maxWidth, - maxHeight, - scaleType, - Config.RGB_565, - new ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onGetImageError(cacheKey, error); - } - }); - } - - /** - * Sets the amount of time to wait after the first response arrives before delivering all - * responses. Batching can be disabled entirely by passing in 0. - * - * @param newBatchedResponseDelayMs The time in milliseconds to wait. - */ - public void setBatchedResponseDelay(int newBatchedResponseDelayMs) { - mBatchResponseDelayMs = newBatchedResponseDelayMs; - } - - /** - * Handler for when an image was successfully loaded. - * - * @param cacheKey The cache key that is associated with the image request. - * @param response The bitmap that was returned from the network. - */ - protected void onGetImageSuccess(String cacheKey, Bitmap response) { - // cache the image that was fetched. - mCache.putBitmap(cacheKey, response); - - // remove the request from the list of in-flight requests. - BatchedImageRequest request = mInFlightRequests.remove(cacheKey); - - if (request != null) { - // Update the response bitmap. - request.mResponseBitmap = response; - - // Send the batched response - batchResponse(cacheKey, request); - } - } - - /** - * Handler for when an image failed to load. - * - * @param cacheKey The cache key that is associated with the image request. - */ - protected void onGetImageError(String cacheKey, VolleyError error) { - // Notify the requesters that something failed via a null result. - // Remove this request from the list of in-flight requests. - BatchedImageRequest request = mInFlightRequests.remove(cacheKey); - - if (request != null) { - // Set the error for this request - request.setError(error); - - // Send the batched response - batchResponse(cacheKey, request); - } - } - - /** Container object for all of the data surrounding an image request. */ - public class ImageContainer { - /** - * The most relevant bitmap for the container. If the image was in cache, the Holder to use - * for the final bitmap (the one that pairs to the requested URL). - */ - private Bitmap mBitmap; - - private final ImageListener mListener; - - /** The cache key that was associated with the request */ - private final String mCacheKey; - - /** The request URL that was specified */ - private final String mRequestUrl; - - /** - * Constructs a BitmapContainer object. - * - * @param bitmap The final bitmap (if it exists). - * @param requestUrl The requested URL for this container. - * @param cacheKey The cache key that identifies the requested URL for this container. - */ - public ImageContainer( - Bitmap bitmap, String requestUrl, String cacheKey, ImageListener listener) { - mBitmap = bitmap; - mRequestUrl = requestUrl; - mCacheKey = cacheKey; - mListener = listener; - } - - /** - * Releases interest in the in-flight request (and cancels it if no one else is listening). - * - *

Must be called from the main thread. - */ - @MainThread - public void cancelRequest() { - Threads.throwIfNotOnMainThread(); - - if (mListener == null) { - return; - } - - BatchedImageRequest request = mInFlightRequests.get(mCacheKey); - if (request != null) { - boolean canceled = request.removeContainerAndCancelIfNecessary(this); - if (canceled) { - mInFlightRequests.remove(mCacheKey); - } - } else { - // check to see if it is already batched for delivery. - request = mBatchedResponses.get(mCacheKey); - if (request != null) { - request.removeContainerAndCancelIfNecessary(this); - if (request.mContainers.size() == 0) { - mBatchedResponses.remove(mCacheKey); - } - } - } - } - - /** - * Returns the bitmap associated with the request URL if it has been loaded, null otherwise. - */ - public Bitmap getBitmap() { - return mBitmap; - } - - /** Returns the requested URL for this container. */ - public String getRequestUrl() { - return mRequestUrl; - } - } - - /** - * Wrapper class used to map a Request to the set of active ImageContainer objects that are - * interested in its results. - */ - private static class BatchedImageRequest { - /** The request being tracked */ - private final Request mRequest; - - /** The result of the request being tracked by this item */ - private Bitmap mResponseBitmap; - - /** Error if one occurred for this response */ - private VolleyError mError; - - /** List of all of the active ImageContainers that are interested in the request */ - private final List mContainers = new ArrayList<>(); - - /** - * Constructs a new BatchedImageRequest object - * - * @param request The request being tracked - * @param container The ImageContainer of the person who initiated the request. - */ - public BatchedImageRequest(Request request, ImageContainer container) { - mRequest = request; - mContainers.add(container); - } - - /** Set the error for this response */ - public void setError(VolleyError error) { - mError = error; - } - - /** Get the error for this response */ - public VolleyError getError() { - return mError; - } - - /** - * Adds another ImageContainer to the list of those interested in the results of the - * request. - */ - public void addContainer(ImageContainer container) { - mContainers.add(container); - } - - /** - * Detaches the bitmap container from the request and cancels the request if no one is left - * listening. - * - * @param container The container to remove from the list - * @return True if the request was canceled, false otherwise. - */ - public boolean removeContainerAndCancelIfNecessary(ImageContainer container) { - mContainers.remove(container); - if (mContainers.size() == 0) { - mRequest.cancel(); - return true; - } - return false; - } - } - - /** - * Starts the runnable for batched delivery of responses if it is not already started. - * - * @param cacheKey The cacheKey of the response being delivered. - * @param request The BatchedImageRequest to be delivered. - */ - private void batchResponse(String cacheKey, BatchedImageRequest request) { - mBatchedResponses.put(cacheKey, request); - // If we don't already have a batch delivery runnable in flight, make a new one. - // Note that this will be used to deliver responses to all callers in mBatchedResponses. - if (mRunnable == null) { - mRunnable = - new Runnable() { - @Override - public void run() { - for (BatchedImageRequest bir : mBatchedResponses.values()) { - for (ImageContainer container : bir.mContainers) { - // If one of the callers in the batched request canceled the - // request - // after the response was received but before it was delivered, - // skip them. - if (container.mListener == null) { - continue; - } - if (bir.getError() == null) { - container.mBitmap = bir.mResponseBitmap; - container.mListener.onResponse(container, false); - } else { - container.mListener.onErrorResponse(bir.getError()); - } - } - } - mBatchedResponses.clear(); - mRunnable = null; - } - }; - // Post the runnable. - mHandler.postDelayed(mRunnable, mBatchResponseDelayMs); - } - } - - /** - * Creates a cache key for use with the L1 cache. - * - * @param url The URL of the request. - * @param maxWidth The max-width of the output. - * @param maxHeight The max-height of the output. - * @param scaleType The scaleType of the imageView. - */ - private static String getCacheKey( - String url, int maxWidth, int maxHeight, ScaleType scaleType) { - return new StringBuilder(url.length() + 12) - .append("#W") - .append(maxWidth) - .append("#H") - .append(maxHeight) - .append("#S") - .append(scaleType.ordinal()) - .append(url) - .toString(); - } -} diff --git a/src/main/java/com/android/volley/toolbox/ImageRequest.java b/src/main/java/com/android/volley/toolbox/ImageRequest.java deleted file mode 100644 index 32b5aa3..0000000 --- a/src/main/java/com/android/volley/toolbox/ImageRequest.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; -import android.graphics.BitmapFactory; -import android.widget.ImageView.ScaleType; -import androidx.annotation.GuardedBy; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.android.volley.DefaultRetryPolicy; -import com.android.volley.NetworkResponse; -import com.android.volley.ParseError; -import com.android.volley.Request; -import com.android.volley.Response; -import com.android.volley.VolleyLog; - -/** A canned request for getting an image at a given URL and calling back with a decoded Bitmap. */ -public class ImageRequest extends Request { - /** Socket timeout in milliseconds for image requests */ - public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000; - - /** Default number of retries for image requests */ - public static final int DEFAULT_IMAGE_MAX_RETRIES = 2; - - /** Default backoff multiplier for image requests */ - public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f; - - /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */ - private final Object mLock = new Object(); - - @GuardedBy("mLock") - @Nullable - private Response.Listener mListener; - - private final Config mDecodeConfig; - private final int mMaxWidth; - private final int mMaxHeight; - private final ScaleType mScaleType; - - /** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */ - private static final Object sDecodeLock = new Object(); - - /** - * Creates a new image request, decoding to a maximum specified width and height. If both width - * and height are zero, the image will be decoded to its natural size. If one of the two is - * nonzero, that dimension will be clamped and the other one will be set to preserve the image's - * aspect ratio. If both width and height are nonzero, the image will be decoded to be fit in - * the rectangle of dimensions width x height while keeping its aspect ratio. - * - * @param url URL of the image - * @param listener Listener to receive the decoded bitmap - * @param maxWidth Maximum width to decode this bitmap to, or zero for none - * @param maxHeight Maximum height to decode this bitmap to, or zero for none - * @param scaleType The ImageViews ScaleType used to calculate the needed image size. - * @param decodeConfig Format to decode the bitmap to - * @param errorListener Error listener, or null to ignore errors - */ - public ImageRequest( - String url, - Response.Listener listener, - int maxWidth, - int maxHeight, - ScaleType scaleType, - Config decodeConfig, - @Nullable Response.ErrorListener errorListener) { - super(Method.GET, url, errorListener); - setRetryPolicy( - new DefaultRetryPolicy( - DEFAULT_IMAGE_TIMEOUT_MS, - DEFAULT_IMAGE_MAX_RETRIES, - DEFAULT_IMAGE_BACKOFF_MULT)); - mListener = listener; - mDecodeConfig = decodeConfig; - mMaxWidth = maxWidth; - mMaxHeight = maxHeight; - mScaleType = scaleType; - } - - /** - * For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to the - * normal constructor with {@code ScaleType.CENTER_INSIDE}. - */ - @Deprecated - public ImageRequest( - String url, - Response.Listener listener, - int maxWidth, - int maxHeight, - Config decodeConfig, - Response.ErrorListener errorListener) { - this( - url, - listener, - maxWidth, - maxHeight, - ScaleType.CENTER_INSIDE, - decodeConfig, - errorListener); - } - - @Override - public Priority getPriority() { - return Priority.LOW; - } - - /** - * Scales one side of a rectangle to fit aspect ratio. - * - * @param maxPrimary Maximum size of the primary dimension (i.e. width for max width), or zero - * to maintain aspect ratio with secondary dimension - * @param maxSecondary Maximum size of the secondary dimension, or zero to maintain aspect ratio - * with primary dimension - * @param actualPrimary Actual size of the primary dimension - * @param actualSecondary Actual size of the secondary dimension - * @param scaleType The ScaleType used to calculate the needed image size. - */ - private static int getResizedDimension( - int maxPrimary, - int maxSecondary, - int actualPrimary, - int actualSecondary, - ScaleType scaleType) { - - // If no dominant value at all, just return the actual. - if ((maxPrimary == 0) && (maxSecondary == 0)) { - return actualPrimary; - } - - // If ScaleType.FIT_XY fill the whole rectangle, ignore ratio. - if (scaleType == ScaleType.FIT_XY) { - if (maxPrimary == 0) { - return actualPrimary; - } - return maxPrimary; - } - - // If primary is unspecified, scale primary to match secondary's scaling ratio. - if (maxPrimary == 0) { - double ratio = (double) maxSecondary / (double) actualSecondary; - return (int) (actualPrimary * ratio); - } - - if (maxSecondary == 0) { - return maxPrimary; - } - - double ratio = (double) actualSecondary / (double) actualPrimary; - int resized = maxPrimary; - - // If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio. - if (scaleType == ScaleType.CENTER_CROP) { - if ((resized * ratio) < maxSecondary) { - resized = (int) (maxSecondary / ratio); - } - return resized; - } - - if ((resized * ratio) > maxSecondary) { - resized = (int) (maxSecondary / ratio); - } - return resized; - } - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - // Serialize all decode on a global lock to reduce concurrent heap usage. - synchronized (sDecodeLock) { - try { - return doParse(response); - } catch (OutOfMemoryError e) { - VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl()); - return Response.error(new ParseError(e)); - } - } - } - - /** The real guts of parseNetworkResponse. Broken out for readability. */ - private Response doParse(NetworkResponse response) { - byte[] data = response.data; - BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); - Bitmap bitmap = null; - if (mMaxWidth == 0 && mMaxHeight == 0) { - decodeOptions.inPreferredConfig = mDecodeConfig; - bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); - } else { - // If we have to resize this image, first get the natural bounds. - decodeOptions.inJustDecodeBounds = true; - BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); - int actualWidth = decodeOptions.outWidth; - int actualHeight = decodeOptions.outHeight; - - // Then compute the dimensions we would ideally like to decode to. - int desiredWidth = - getResizedDimension( - mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType); - int desiredHeight = - getResizedDimension( - mMaxHeight, mMaxWidth, actualHeight, actualWidth, mScaleType); - - // Decode to the nearest power of two scaling factor. - decodeOptions.inJustDecodeBounds = false; - // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it? - // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED; - decodeOptions.inSampleSize = - findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); - Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); - - // If necessary, scale down to the maximal acceptable size. - if (tempBitmap != null - && (tempBitmap.getWidth() > desiredWidth - || tempBitmap.getHeight() > desiredHeight)) { - bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true); - tempBitmap.recycle(); - } else { - bitmap = tempBitmap; - } - } - - if (bitmap == null) { - return Response.error(new ParseError(response)); - } else { - return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); - } - } - - @Override - public void cancel() { - super.cancel(); - synchronized (mLock) { - mListener = null; - } - } - - @Override - protected void deliverResponse(Bitmap response) { - Response.Listener listener; - synchronized (mLock) { - listener = mListener; - } - if (listener != null) { - listener.onResponse(response); - } - } - - /** - * Returns the largest power-of-two divisor for use in downscaling a bitmap that will not result - * in the scaling past the desired dimensions. - * - * @param actualWidth Actual width of the bitmap - * @param actualHeight Actual height of the bitmap - * @param desiredWidth Desired width of the bitmap - * @param desiredHeight Desired height of the bitmap - */ - @VisibleForTesting - static int findBestSampleSize( - int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { - double wr = (double) actualWidth / desiredWidth; - double hr = (double) actualHeight / desiredHeight; - double ratio = Math.min(wr, hr); - float n = 1.0f; - while ((n * 2) <= ratio) { - n *= 2; - } - - return (int) n; - } -} diff --git a/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java b/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java deleted file mode 100644 index 86ed9e9..0000000 --- a/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import androidx.annotation.Nullable; -import com.android.volley.NetworkResponse; -import com.android.volley.ParseError; -import com.android.volley.Response; -import com.android.volley.Response.ErrorListener; -import com.android.volley.Response.Listener; -import java.io.UnsupportedEncodingException; -import org.json.JSONArray; -import org.json.JSONException; - -/** A request for retrieving a {@link JSONArray} response body at a given URL. */ -public class JsonArrayRequest extends JsonRequest { - - /** - * Creates a new request. - * - * @param url URL to fetch the JSON from - * @param listener Listener to receive the JSON response - * @param errorListener Error listener, or null to ignore errors. - */ - public JsonArrayRequest( - String url, Listener listener, @Nullable ErrorListener errorListener) { - super(Method.GET, url, null, listener, errorListener); - } - - /** - * Creates a new request. - * - * @param method the HTTP method to use - * @param url URL to fetch the JSON from - * @param jsonRequest A {@link JSONArray} to post with the request. Null indicates no parameters - * will be posted along with request. - * @param listener Listener to receive the JSON response - * @param errorListener Error listener, or null to ignore errors. - */ - public JsonArrayRequest( - int method, - String url, - @Nullable JSONArray jsonRequest, - Listener listener, - @Nullable ErrorListener errorListener) { - super( - method, - url, - (jsonRequest == null) ? null : jsonRequest.toString(), - listener, - errorListener); - } - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - try { - String jsonString = - new String( - response.data, - HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET)); - return Response.success( - new JSONArray(jsonString), HttpHeaderParser.parseCacheHeaders(response)); - } catch (UnsupportedEncodingException e) { - return Response.error(new ParseError(e)); - } catch (JSONException je) { - return Response.error(new ParseError(je)); - } - } -} diff --git a/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java b/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java deleted file mode 100644 index 8dca0ec..0000000 --- a/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import androidx.annotation.Nullable; -import com.android.volley.NetworkResponse; -import com.android.volley.ParseError; -import com.android.volley.Response; -import com.android.volley.Response.ErrorListener; -import com.android.volley.Response.Listener; -import java.io.UnsupportedEncodingException; -import org.json.JSONException; -import org.json.JSONObject; - -/** - * A request for retrieving a {@link JSONObject} response body at a given URL, allowing for an - * optional {@link JSONObject} to be passed in as part of the request body. - */ -public class JsonObjectRequest extends JsonRequest { - - /** - * Creates a new request. - * - * @param method the HTTP method to use - * @param url URL to fetch the JSON from - * @param jsonRequest A {@link JSONObject} to post with the request. Null indicates no - * parameters will be posted along with request. - * @param listener Listener to receive the JSON response - * @param errorListener Error listener, or null to ignore errors. - */ - public JsonObjectRequest( - int method, - String url, - @Nullable JSONObject jsonRequest, - Listener listener, - @Nullable ErrorListener errorListener) { - super( - method, - url, - (jsonRequest == null) ? null : jsonRequest.toString(), - listener, - errorListener); - } - - /** - * Constructor which defaults to GET if jsonRequest is null - * , POST otherwise. - * - * @see #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener) - */ - public JsonObjectRequest( - String url, - @Nullable JSONObject jsonRequest, - Listener listener, - @Nullable ErrorListener errorListener) { - this( - jsonRequest == null ? Method.GET : Method.POST, - url, - jsonRequest, - listener, - errorListener); - } - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - try { - String jsonString = - new String( - response.data, - HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET)); - return Response.success( - new JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response)); - } catch (UnsupportedEncodingException e) { - return Response.error(new ParseError(e)); - } catch (JSONException je) { - return Response.error(new ParseError(je)); - } - } -} diff --git a/src/main/java/com/android/volley/toolbox/JsonRequest.java b/src/main/java/com/android/volley/toolbox/JsonRequest.java deleted file mode 100644 index bc035ae..0000000 --- a/src/main/java/com/android/volley/toolbox/JsonRequest.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import androidx.annotation.GuardedBy; -import androidx.annotation.Nullable; -import com.android.volley.NetworkResponse; -import com.android.volley.Request; -import com.android.volley.Response; -import com.android.volley.Response.ErrorListener; -import com.android.volley.Response.Listener; -import com.android.volley.VolleyLog; -import java.io.UnsupportedEncodingException; - -/** - * A request for retrieving a T type response body at a given URL that also optionally sends along a - * JSON body in the request specified. - * - * @param JSON type of response expected - */ -public abstract class JsonRequest extends Request { - /** Default charset for JSON request. */ - protected static final String PROTOCOL_CHARSET = "utf-8"; - - /** Content type for request. */ - private static final String PROTOCOL_CONTENT_TYPE = - String.format("application/json; charset=%s", PROTOCOL_CHARSET); - - /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */ - private final Object mLock = new Object(); - - @Nullable - @GuardedBy("mLock") - private Listener mListener; - - @Nullable private final String mRequestBody; - - /** - * Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()} - * or {@link #getPostParams()} is overridden (which defaults to POST). - * - * @deprecated Use {@link #JsonRequest(int, String, String, Listener, ErrorListener)}. - */ - @Deprecated - public JsonRequest( - String url, String requestBody, Listener listener, ErrorListener errorListener) { - this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener); - } - - public JsonRequest( - int method, - String url, - @Nullable String requestBody, - Listener listener, - @Nullable ErrorListener errorListener) { - super(method, url, errorListener); - mListener = listener; - mRequestBody = requestBody; - } - - @Override - public void cancel() { - super.cancel(); - synchronized (mLock) { - mListener = null; - } - } - - @Override - protected void deliverResponse(T response) { - Response.Listener listener; - synchronized (mLock) { - listener = mListener; - } - if (listener != null) { - listener.onResponse(response); - } - } - - @Override - protected abstract Response parseNetworkResponse(NetworkResponse response); - - /** @deprecated Use {@link #getBodyContentType()}. */ - @Deprecated - @Override - public String getPostBodyContentType() { - return getBodyContentType(); - } - - /** @deprecated Use {@link #getBody()}. */ - @Deprecated - @Override - public byte[] getPostBody() { - return getBody(); - } - - @Override - public String getBodyContentType() { - return PROTOCOL_CONTENT_TYPE; - } - - @Override - public byte[] getBody() { - try { - return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET); - } catch (UnsupportedEncodingException uee) { - VolleyLog.wtf( - "Unsupported Encoding while trying to get the bytes of %s using %s", - mRequestBody, PROTOCOL_CHARSET); - return null; - } - } -} diff --git a/src/main/java/com/android/volley/toolbox/NetworkImageView.java b/src/main/java/com/android/volley/toolbox/NetworkImageView.java deleted file mode 100644 index a24b3e2..0000000 --- a/src/main/java/com/android/volley/toolbox/NetworkImageView.java +++ /dev/null @@ -1,332 +0,0 @@ -/** - * Copyright (C) 2013 The Android Open Source Project - * - *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - *

http://www.apache.org/licenses/LICENSE-2.0 - * - *

Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.volley.toolbox; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.ViewGroup.LayoutParams; -import android.widget.ImageView; -import androidx.annotation.MainThread; -import androidx.annotation.Nullable; -import com.android.volley.VolleyError; -import com.android.volley.toolbox.ImageLoader.ImageContainer; -import com.android.volley.toolbox.ImageLoader.ImageListener; - -/** Handles fetching an image from a URL as well as the life-cycle of the associated request. */ -public class NetworkImageView extends ImageView { - /** The URL of the network image to load */ - private String mUrl; - - /** - * Resource ID of the image to be used as a placeholder until the network image is loaded. Won't - * be set at the same time as mDefaultImageDrawable or mDefaultImageBitmap. - */ - private int mDefaultImageId; - - /** - * Drawable of the image to be used as a placeholder until the network image is loaded. Won't be - * set at the same time as mDefaultImageId or mDefaultImageBitmap. - */ - @Nullable private Drawable mDefaultImageDrawable; - - /** - * Bitmap of the image to be used as a placeholder until the network image is loaded. Won't be - * set at the same time as mDefaultImageId or mDefaultImageDrawable. - */ - @Nullable private Bitmap mDefaultImageBitmap; - - /** - * Resource ID of the image to be used if the network response fails. Won't be set at the same - * time as mErrorImageDrawable or mErrorImageBitmap. - */ - private int mErrorImageId; - - /** - * Bitmap of the image to be used if the network response fails. Won't be set at the same time - * as mErrorImageId or mErrorImageBitmap. - */ - @Nullable private Drawable mErrorImageDrawable; - - /** - * Bitmap of the image to be used if the network response fails. Won't be set at the same time - * as mErrorImageId or mErrorImageDrawable. - */ - @Nullable private Bitmap mErrorImageBitmap; - - /** Local copy of the ImageLoader. */ - private ImageLoader mImageLoader; - - /** Current ImageContainer. (either in-flight or finished) */ - private ImageContainer mImageContainer; - - public NetworkImageView(Context context) { - this(context, null); - } - - public NetworkImageView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public NetworkImageView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - /** - * Sets URL of the image that should be loaded into this view. Note that calling this will - * immediately either set the cached image (if available) or the default image specified by - * {@link NetworkImageView#setDefaultImageResId(int)} on the view. - * - *

NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} or {@link - * NetworkImageView#setDefaultImageBitmap} and {@link NetworkImageView#setErrorImageResId(int)} - * or {@link NetworkImageView#setErrorImageBitmap(Bitmap)} should be called prior to calling - * this function. - * - *

Must be called from the main thread. - * - * @param url The URL that should be loaded into this ImageView. - * @param imageLoader ImageLoader that will be used to make the request. - */ - @MainThread - public void setImageUrl(String url, ImageLoader imageLoader) { - Threads.throwIfNotOnMainThread(); - mUrl = url; - mImageLoader = imageLoader; - // The URL has potentially changed. See if we need to load it. - loadImageIfNecessary(/* isInLayoutPass= */ false); - } - - /** - * Sets the default image resource ID to be used for this view until the attempt to load it - * completes. - * - *

This will clear anything set by {@link NetworkImageView#setDefaultImageBitmap} or {@link - * NetworkImageView#setDefaultImageDrawable}. - */ - public void setDefaultImageResId(int defaultImage) { - mDefaultImageBitmap = null; - mDefaultImageDrawable = null; - mDefaultImageId = defaultImage; - } - - /** - * Sets the default image drawable to be used for this view until the attempt to load it - * completes. - * - *

This will clear anything set by {@link NetworkImageView#setDefaultImageResId} or {@link - * NetworkImageView#setDefaultImageBitmap}. - */ - public void setDefaultImageDrawable(@Nullable Drawable defaultImageDrawable) { - mDefaultImageId = 0; - mDefaultImageBitmap = null; - mDefaultImageDrawable = defaultImageDrawable; - } - - /** - * Sets the default image bitmap to be used for this view until the attempt to load it - * completes. - * - *

This will clear anything set by {@link NetworkImageView#setDefaultImageResId} or {@link - * NetworkImageView#setDefaultImageDrawable}. - */ - public void setDefaultImageBitmap(Bitmap defaultImage) { - mDefaultImageId = 0; - mDefaultImageDrawable = null; - mDefaultImageBitmap = defaultImage; - } - - /** - * Sets the error image resource ID to be used for this view in the event that the image - * requested fails to load. - * - *

This will clear anything set by {@link NetworkImageView#setErrorImageBitmap} or {@link - * NetworkImageView#setErrorImageDrawable}. - */ - public void setErrorImageResId(int errorImage) { - mErrorImageBitmap = null; - mErrorImageDrawable = null; - mErrorImageId = errorImage; - } - - /** - * Sets the error image drawable to be used for this view in the event that the image requested - * fails to load. - * - *

This will clear anything set by {@link NetworkImageView#setErrorImageResId} or {@link - * NetworkImageView#setDefaultImageBitmap}. - */ - public void setErrorImageDrawable(@Nullable Drawable errorImageDrawable) { - mErrorImageId = 0; - mErrorImageBitmap = null; - mErrorImageDrawable = errorImageDrawable; - } - - /** - * Sets the error image bitmap to be used for this view in the event that the image requested - * fails to load. - * - *

This will clear anything set by {@link NetworkImageView#setErrorImageResId} or {@link - * NetworkImageView#setDefaultImageDrawable}. - */ - public void setErrorImageBitmap(Bitmap errorImage) { - mErrorImageId = 0; - mErrorImageDrawable = null; - mErrorImageBitmap = errorImage; - } - - /** - * Loads the image for the view if it isn't already loaded. - * - * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise. - */ - void loadImageIfNecessary(final boolean isInLayoutPass) { - int width = getWidth(); - int height = getHeight(); - ScaleType scaleType = getScaleType(); - - boolean wrapWidth = false, wrapHeight = false; - if (getLayoutParams() != null) { - wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT; - wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT; - } - - // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content - // view, hold off on loading the image. - boolean isFullyWrapContent = wrapWidth && wrapHeight; - if (width == 0 && height == 0 && !isFullyWrapContent) { - return; - } - - // if the URL to be loaded in this view is empty, cancel any old requests and clear the - // currently loaded image. - if (TextUtils.isEmpty(mUrl)) { - if (mImageContainer != null) { - mImageContainer.cancelRequest(); - mImageContainer = null; - } - setDefaultImageOrNull(); - return; - } - - // if there was an old request in this view, check if it needs to be canceled. - if (mImageContainer != null && mImageContainer.getRequestUrl() != null) { - if (mImageContainer.getRequestUrl().equals(mUrl)) { - // if the request is from the same URL, return. - return; - } else { - // if there is a pre-existing request, cancel it if it's fetching a different URL. - mImageContainer.cancelRequest(); - setDefaultImageOrNull(); - } - } - - // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens. - int maxWidth = wrapWidth ? 0 : width; - int maxHeight = wrapHeight ? 0 : height; - - // The pre-existing content of this view didn't match the current URL. Load the new image - // from the network. - - // update the ImageContainer to be the new bitmap container. - mImageContainer = - mImageLoader.get( - mUrl, - new ImageListener() { - @Override - public void onErrorResponse(VolleyError error) { - if (mErrorImageId != 0) { - setImageResource(mErrorImageId); - } else if (mErrorImageDrawable != null) { - setImageDrawable(mErrorImageDrawable); - } else if (mErrorImageBitmap != null) { - setImageBitmap(mErrorImageBitmap); - } - } - - @Override - public void onResponse( - final ImageContainer response, boolean isImmediate) { - // If this was an immediate response that was delivered inside of a - // layout - // pass do not set the image immediately as it will trigger a - // requestLayout - // inside of a layout. Instead, defer setting the image by posting - // back to - // the main thread. - if (isImmediate && isInLayoutPass) { - post( - new Runnable() { - @Override - public void run() { - onResponse(response, /* isImmediate= */ false); - } - }); - return; - } - - if (response.getBitmap() != null) { - setImageBitmap(response.getBitmap()); - } else if (mDefaultImageId != 0) { - setImageResource(mDefaultImageId); - } else if (mDefaultImageDrawable != null) { - setImageDrawable(mDefaultImageDrawable); - } else if (mDefaultImageBitmap != null) { - setImageBitmap(mDefaultImageBitmap); - } - } - }, - maxWidth, - maxHeight, - scaleType); - } - - private void setDefaultImageOrNull() { - if (mDefaultImageId != 0) { - setImageResource(mDefaultImageId); - } else if (mDefaultImageDrawable != null) { - setImageDrawable(mDefaultImageDrawable); - } else if (mDefaultImageBitmap != null) { - setImageBitmap(mDefaultImageBitmap); - } else { - setImageBitmap(null); - } - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - loadImageIfNecessary(/* isInLayoutPass= */ true); - } - - @Override - protected void onDetachedFromWindow() { - if (mImageContainer != null) { - // If the view was bound to an image request, cancel it and clear - // out the image from the view. - mImageContainer.cancelRequest(); - setImageBitmap(null); - // also clear out the container so we can reload the image if necessary. - mImageContainer = null; - } - super.onDetachedFromWindow(); - } - - @Override - protected void drawableStateChanged() { - super.drawableStateChanged(); - invalidate(); - } -} diff --git a/src/main/java/com/android/volley/toolbox/NetworkUtility.java b/src/main/java/com/android/volley/toolbox/NetworkUtility.java deleted file mode 100644 index 44d5904..0000000 --- a/src/main/java/com/android/volley/toolbox/NetworkUtility.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * 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

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
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
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 deleted file mode 100644 index aa4aeea..0000000 --- a/src/main/java/com/android/volley/toolbox/NoAsyncCache.java +++ /dev/null @@ -1,37 +0,0 @@ -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/NoCache.java b/src/main/java/com/android/volley/toolbox/NoCache.java deleted file mode 100644 index 51f9945..0000000 --- a/src/main/java/com/android/volley/toolbox/NoCache.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import com.android.volley.Cache; - -/** A cache that doesn't. */ -public class NoCache implements Cache { - @Override - public void clear() {} - - @Override - public Entry get(String key) { - return null; - } - - @Override - public void put(String key, Entry entry) {} - - @Override - public void invalidate(String key, boolean fullExpire) {} - - @Override - public void remove(String key) {} - - @Override - public void initialize() {} -} diff --git a/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java b/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java deleted file mode 100644 index bdcc45e..0000000 --- a/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -/** - * A variation of {@link java.io.ByteArrayOutputStream} that uses a pool of byte[] buffers instead - * of always allocating them fresh, saving on heap churn. - */ -public class PoolingByteArrayOutputStream extends ByteArrayOutputStream { - /** - * If the {@link #PoolingByteArrayOutputStream(ByteArrayPool)} constructor is called, this is - * the default size to which the underlying byte array is initialized. - */ - private static final int DEFAULT_SIZE = 256; - - private final ByteArrayPool mPool; - - /** - * Constructs a new PoolingByteArrayOutputStream with a default size. If more bytes are written - * to this instance, the underlying byte array will expand. - */ - public PoolingByteArrayOutputStream(ByteArrayPool pool) { - this(pool, DEFAULT_SIZE); - } - - /** - * Constructs a new {@code ByteArrayOutputStream} with a default size of {@code size} bytes. If - * more than {@code size} bytes are written to this instance, the underlying byte array will - * expand. - * - * @param size initial size for the underlying byte array. The value will be pinned to a default - * minimum size. - */ - public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) { - mPool = pool; - buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE)); - } - - @Override - public void close() throws IOException { - mPool.returnBuf(buf); - buf = null; - super.close(); - } - - @Override - public void finalize() { - mPool.returnBuf(buf); - } - - /** Ensures there is enough space in the buffer for the given number of additional bytes. */ - @SuppressWarnings("UnsafeFinalization") - private void expand(int i) { - /* Can the buffer handle @i more bytes, if not expand it */ - if (count + i <= buf.length) { - return; - } - byte[] newbuf = mPool.getBuf((count + i) * 2); - System.arraycopy(buf, 0, newbuf, 0, count); - mPool.returnBuf(buf); - buf = newbuf; - } - - @Override - public synchronized void write(byte[] buffer, int offset, int len) { - expand(len); - super.write(buffer, offset, len); - } - - @Override - public synchronized void write(int oneByte) { - expand(1); - super.write(oneByte); - } -} diff --git a/src/main/java/com/android/volley/toolbox/RequestFuture.java b/src/main/java/com/android/volley/toolbox/RequestFuture.java deleted file mode 100644 index f9cbce2..0000000 --- a/src/main/java/com/android/volley/toolbox/RequestFuture.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import android.os.SystemClock; -import com.android.volley.Request; -import com.android.volley.Response; -import com.android.volley.VolleyError; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -/** - * A Future that represents a Volley request. - * - *

Used by providing as your response and error listeners. For example: - * - *

- * RequestFuture<JSONObject> future = RequestFuture.newFuture();
- * MyRequest request = new MyRequest(URL, future, future);
- *
- * // If you want to be able to cancel the request:
- * future.setRequest(requestQueue.add(request));
- *
- * // Otherwise:
- * requestQueue.add(request);
- *
- * try {
- *   JSONObject response = future.get();
- *   // do something with response
- * } catch (InterruptedException e) {
- *   // handle the error
- * } catch (ExecutionException e) {
- *   // handle the error
- * }
- * 
- * - * @param The type of parsed response this future expects. - */ -public class RequestFuture implements Future, Response.Listener, Response.ErrorListener { - private Request mRequest; - private boolean mResultReceived = false; - private T mResult; - private VolleyError mException; - - public static RequestFuture newFuture() { - return new RequestFuture<>(); - } - - private RequestFuture() {} - - public void setRequest(Request request) { - mRequest = request; - } - - @Override - public synchronized boolean cancel(boolean mayInterruptIfRunning) { - if (mRequest == null) { - return false; - } - - if (!isDone()) { - mRequest.cancel(); - return true; - } else { - return false; - } - } - - @Override - public T get() throws InterruptedException, ExecutionException { - try { - return doGet(/* timeoutMs= */ null); - } catch (TimeoutException e) { - throw new AssertionError(e); - } - } - - @Override - public T get(long timeout, TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException { - return doGet(TimeUnit.MILLISECONDS.convert(timeout, unit)); - } - - private synchronized T doGet(Long timeoutMs) - throws InterruptedException, ExecutionException, TimeoutException { - if (mException != null) { - throw new ExecutionException(mException); - } - - if (mResultReceived) { - return mResult; - } - - if (timeoutMs == null) { - while (!isDone()) { - wait(0); - } - } else if (timeoutMs > 0) { - long nowMs = SystemClock.uptimeMillis(); - long deadlineMs = nowMs + timeoutMs; - while (!isDone() && nowMs < deadlineMs) { - wait(deadlineMs - nowMs); - nowMs = SystemClock.uptimeMillis(); - } - } - - if (mException != null) { - throw new ExecutionException(mException); - } - - if (!mResultReceived) { - throw new TimeoutException(); - } - - return mResult; - } - - @Override - public boolean isCancelled() { - if (mRequest == null) { - return false; - } - return mRequest.isCanceled(); - } - - @Override - public synchronized boolean isDone() { - return mResultReceived || mException != null || isCancelled(); - } - - @Override - public synchronized void onResponse(T response) { - mResultReceived = true; - mResult = response; - notifyAll(); - } - - @Override - public synchronized void onErrorResponse(VolleyError error) { - mException = error; - notifyAll(); - } -} diff --git a/src/main/java/com/android/volley/toolbox/StringRequest.java b/src/main/java/com/android/volley/toolbox/StringRequest.java deleted file mode 100644 index df7b386..0000000 --- a/src/main/java/com/android/volley/toolbox/StringRequest.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import androidx.annotation.GuardedBy; -import androidx.annotation.Nullable; -import com.android.volley.NetworkResponse; -import com.android.volley.Request; -import com.android.volley.Response; -import com.android.volley.Response.ErrorListener; -import com.android.volley.Response.Listener; -import java.io.UnsupportedEncodingException; - -/** A canned request for retrieving the response body at a given URL as a String. */ -public class StringRequest extends Request { - - /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */ - private final Object mLock = new Object(); - - @Nullable - @GuardedBy("mLock") - private Listener mListener; - - /** - * Creates a new request with the given method. - * - * @param method the request {@link Method} to use - * @param url URL to fetch the string at - * @param listener Listener to receive the String response - * @param errorListener Error listener, or null to ignore errors - */ - public StringRequest( - int method, - String url, - Listener listener, - @Nullable ErrorListener errorListener) { - super(method, url, errorListener); - mListener = listener; - } - - /** - * Creates a new GET request. - * - * @param url URL to fetch the string at - * @param listener Listener to receive the String response - * @param errorListener Error listener, or null to ignore errors - */ - public StringRequest( - String url, Listener listener, @Nullable ErrorListener errorListener) { - this(Method.GET, url, listener, errorListener); - } - - @Override - public void cancel() { - super.cancel(); - synchronized (mLock) { - mListener = null; - } - } - - @Override - protected void deliverResponse(String response) { - Response.Listener listener; - synchronized (mLock) { - listener = mListener; - } - if (listener != null) { - listener.onResponse(response); - } - } - - @Override - @SuppressWarnings("DefaultCharset") - protected Response parseNetworkResponse(NetworkResponse response) { - String parsed; - try { - parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); - } catch (UnsupportedEncodingException e) { - // Since minSdkVersion = 8, we can't call - // new String(response.data, Charset.defaultCharset()) - // So suppress the warning instead. - parsed = new String(response.data); - } - return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response)); - } -} diff --git a/src/main/java/com/android/volley/toolbox/Threads.java b/src/main/java/com/android/volley/toolbox/Threads.java deleted file mode 100644 index 66c3e41..0000000 --- a/src/main/java/com/android/volley/toolbox/Threads.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.android.volley.toolbox; - -import android.os.Looper; - -final class Threads { - private Threads() {} - - static void throwIfNotOnMainThread() { - if (Looper.myLooper() != Looper.getMainLooper()) { - throw new IllegalStateException("Must be invoked from the main thread."); - } - } -} diff --git a/src/main/java/com/android/volley/toolbox/UrlRewriter.java b/src/main/java/com/android/volley/toolbox/UrlRewriter.java deleted file mode 100644 index 8bbb770..0000000 --- a/src/main/java/com/android/volley/toolbox/UrlRewriter.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import androidx.annotation.Nullable; - -/** An interface for transforming URLs before use. */ -public interface UrlRewriter { - /** - * Returns a URL to use instead of the provided one, or null to indicate this URL should not be - * used at all. - */ - @Nullable - String rewriteUrl(String originalUrl); -} diff --git a/src/main/java/com/android/volley/toolbox/Volley.java b/src/main/java/com/android/volley/toolbox/Volley.java deleted file mode 100644 index bc65c9c..0000000 --- a/src/main/java/com/android/volley/toolbox/Volley.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.net.http.AndroidHttpClient; -import android.os.Build; -import com.android.volley.Network; -import com.android.volley.RequestQueue; -import java.io.File; - -public class Volley { - - /** Default on-disk cache directory. */ - private static final String DEFAULT_CACHE_DIR = "volley"; - - /** - * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it. - * - * @param context A {@link Context} to use for creating the cache dir. - * @param stack A {@link BaseHttpStack} to use for the network, or null for default. - * @return A started {@link RequestQueue} instance. - */ - public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) { - BasicNetwork network; - if (stack == null) { - if (Build.VERSION.SDK_INT >= 9) { - network = new BasicNetwork(new HurlStack()); - } else { - // Prior to Gingerbread, HttpUrlConnection was unreliable. - // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html - // At some point in the future we'll move our minSdkVersion past Froyo and can - // delete this fallback (along with all Apache HTTP code). - String userAgent = "volley/0"; - try { - String packageName = context.getPackageName(); - PackageInfo info = - context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0); - userAgent = packageName + "/" + info.versionCode; - } catch (NameNotFoundException e) { - } - - network = - new BasicNetwork( - new HttpClientStack(AndroidHttpClient.newInstance(userAgent))); - } - } else { - network = new BasicNetwork(stack); - } - - return newRequestQueue(context, network); - } - - /** - * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it. - * - * @param context A {@link Context} to use for creating the cache dir. - * @param stack An {@link HttpStack} to use for the network, or null for default. - * @return A started {@link RequestQueue} instance. - * @deprecated Use {@link #newRequestQueue(Context, BaseHttpStack)} instead to avoid depending - * on Apache HTTP. This method may be removed in a future release of Volley. - */ - @Deprecated - @SuppressWarnings("deprecation") - public static RequestQueue newRequestQueue(Context context, HttpStack stack) { - if (stack == null) { - return newRequestQueue(context, (BaseHttpStack) null); - } - return newRequestQueue(context, new BasicNetwork(stack)); - } - - private static RequestQueue newRequestQueue(Context context, Network network) { - final Context appContext = context.getApplicationContext(); - // Use a lazy supplier for the cache directory so that newRequestQueue() can be called on - // main thread without causing strict mode violation. - DiskBasedCache.FileSupplier cacheSupplier = - new DiskBasedCache.FileSupplier() { - private File cacheDir = null; - - @Override - public File get() { - if (cacheDir == null) { - cacheDir = new File(appContext.getCacheDir(), DEFAULT_CACHE_DIR); - } - return cacheDir; - } - }; - RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheSupplier), network); - queue.start(); - return queue; - } - - /** - * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it. - * - * @param context A {@link Context} to use for creating the cache dir. - * @return A started {@link RequestQueue} instance. - */ - public static RequestQueue newRequestQueue(Context context) { - return newRequestQueue(context, (BaseHttpStack) null); - } -} diff --git a/src/test/java/com/android/volley/AsyncRequestQueueTest.java b/src/test/java/com/android/volley/AsyncRequestQueueTest.java deleted file mode 100644 index 54ff0a1..0000000 --- a/src/test/java/com/android/volley/AsyncRequestQueueTest.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import com.android.volley.mock.ShadowSystemClock; -import com.android.volley.toolbox.NoAsyncCache; -import com.android.volley.toolbox.StringRequest; -import com.android.volley.utils.ImmediateResponseDelivery; -import com.google.common.util.concurrent.MoreExecutors; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ScheduledExecutorService; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -/** Unit tests for AsyncRequestQueue, with all dependencies mocked out */ -@RunWith(RobolectricTestRunner.class) -@Config(shadows = {ShadowSystemClock.class}) -public class AsyncRequestQueueTest { - - @Mock private AsyncNetwork mMockNetwork; - @Mock private ScheduledExecutorService mMockScheduledExecutor; - private AsyncRequestQueue queue; - - @Before - public void setUp() throws Exception { - ResponseDelivery mDelivery = new ImmediateResponseDelivery(); - initMocks(this); - queue = - new AsyncRequestQueue.Builder(mMockNetwork) - .setAsyncCache(new NoAsyncCache()) - .setResponseDelivery(mDelivery) - .setExecutorFactory( - new AsyncRequestQueue.ExecutorFactory() { - @Override - public ExecutorService createNonBlockingExecutor( - BlockingQueue taskQueue) { - return MoreExecutors.newDirectExecutorService(); - } - - @Override - public ExecutorService createBlockingExecutor( - BlockingQueue taskQueue) { - return MoreExecutors.newDirectExecutorService(); - } - - @Override - public ScheduledExecutorService - createNonBlockingScheduledExecutor() { - return mMockScheduledExecutor; - } - }) - .build(); - } - - @Test - public void cancelAll_onlyCorrectTag() throws Exception { - queue.start(); - Object tagA = new Object(); - Object tagB = new Object(); - StringRequest req1 = mock(StringRequest.class); - when(req1.getTag()).thenReturn(tagA); - StringRequest req2 = mock(StringRequest.class); - when(req2.getTag()).thenReturn(tagB); - StringRequest req3 = mock(StringRequest.class); - when(req3.getTag()).thenReturn(tagA); - StringRequest req4 = mock(StringRequest.class); - when(req4.getTag()).thenReturn(tagA); - - queue.add(req1); // A - queue.add(req2); // B - queue.add(req3); // A - queue.cancelAll(tagA); - queue.add(req4); // A - - verify(req1).cancel(); // A cancelled - verify(req3).cancel(); // A cancelled - verify(req2, never()).cancel(); // B not cancelled - verify(req4, never()).cancel(); // A added after cancel not cancelled - queue.stop(); - } - - @Test - public void add_notifiesListener() throws Exception { - RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); - queue.start(); - queue.addRequestEventListener(listener); - StringRequest req = mock(StringRequest.class); - - queue.add(req); - - verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_QUEUED); - verifyNoMoreInteractions(listener); - queue.stop(); - } - - @Test - public void finish_notifiesListener() throws Exception { - RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); - queue.start(); - queue.addRequestEventListener(listener); - StringRequest req = mock(StringRequest.class); - - queue.finish(req); - - verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_FINISHED); - verifyNoMoreInteractions(listener); - queue.stop(); - } - - @Test - public void sendRequestEvent_notifiesListener() throws Exception { - StringRequest req = mock(StringRequest.class); - RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); - queue.start(); - queue.addRequestEventListener(listener); - - queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); - - verify(listener) - .onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); - verifyNoMoreInteractions(listener); - queue.stop(); - } - - @Test - public void removeRequestEventListener_removesListener() throws Exception { - StringRequest req = mock(StringRequest.class); - RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); - queue.start(); - queue.addRequestEventListener(listener); - queue.removeRequestEventListener(listener); - - queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); - - verifyNoMoreInteractions(listener); - queue.stop(); - } -} diff --git a/src/test/java/com/android/volley/CacheDispatcherTest.java b/src/test/java/com/android/volley/CacheDispatcherTest.java deleted file mode 100644 index aef6785..0000000 --- a/src/test/java/com/android/volley/CacheDispatcherTest.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import com.android.volley.toolbox.StringRequest; -import com.android.volley.utils.CacheTestUtils; -import java.util.concurrent.BlockingQueue; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -@SuppressWarnings("rawtypes") -public class CacheDispatcherTest { - private CacheDispatcher mDispatcher; - private @Mock BlockingQueue> mCacheQueue; - private @Mock BlockingQueue> mNetworkQueue; - private @Mock Cache mCache; - private @Mock ResponseDelivery mDelivery; - private @Mock Network mNetwork; - private StringRequest mRequest; - - @Before - public void setUp() throws Exception { - initMocks(this); - - mRequest = new StringRequest(Request.Method.GET, "http://foo", null, null); - mDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery); - } - - private static class WaitForever implements Answer { - @Override - public Object answer(InvocationOnMock invocationOnMock) throws Throwable { - Thread.sleep(Long.MAX_VALUE); - return null; - } - } - - @Test - public void runStopsOnQuit() throws Exception { - when(mCacheQueue.take()).then(new WaitForever()); - mDispatcher.start(); - mDispatcher.quit(); - mDispatcher.join(1000); - } - - private static void verifyNoResponse(ResponseDelivery delivery) { - verify(delivery, never()).postResponse(any(Request.class), any(Response.class)); - verify(delivery, never()) - .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); - verify(delivery, never()).postError(any(Request.class), any(VolleyError.class)); - } - - // A cancelled request should not be processed at all. - @Test - public void cancelledRequest() throws Exception { - mRequest.cancel(); - mDispatcher.processRequest(mRequest); - verify(mCache, never()).get(anyString()); - verifyNoResponse(mDelivery); - } - - // A cache miss does not post a response and puts the request on the network queue. - @Test - public void cacheMiss() throws Exception { - mDispatcher.processRequest(mRequest); - verifyNoResponse(mDelivery); - verify(mNetworkQueue).put(mRequest); - assertNull(mRequest.getCacheEntry()); - } - - // A non-expired cache hit posts a response and does not queue to the network. - @Test - public void nonExpiredCacheHit() throws Exception { - Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); - when(mCache.get(anyString())).thenReturn(entry); - mDispatcher.processRequest(mRequest); - verify(mDelivery).postResponse(any(Request.class), any(Response.class)); - verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class)); - } - - // A soft-expired cache hit posts a response and queues to the network. - @Test - public void softExpiredCacheHit() throws Exception { - Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); - when(mCache.get(anyString())).thenReturn(entry); - mDispatcher.processRequest(mRequest); - - // Soft expiration needs to use the deferred Runnable variant of postResponse, - // so make sure it gets to run. - ArgumentCaptor runnable = ArgumentCaptor.forClass(Runnable.class); - verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture()); - runnable.getValue().run(); - // This way we can verify the behavior of the Runnable as well. - verify(mNetworkQueue).put(mRequest); - assertSame(entry, mRequest.getCacheEntry()); - - verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class)); - } - - // An expired cache hit does not post a response and queues to the network. - @Test - public void expiredCacheHit() throws Exception { - Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, true, true); - when(mCache.get(anyString())).thenReturn(entry); - mDispatcher.processRequest(mRequest); - verifyNoResponse(mDelivery); - verify(mNetworkQueue).put(mRequest); - assertSame(entry, mRequest.getCacheEntry()); - } - - // An fresh cache hit with parse error, does not post a response and queues to the network. - @Test - public void freshCacheHit_parseError() throws Exception { - Request request = mock(Request.class); - when(request.parseNetworkResponse(any(NetworkResponse.class))) - .thenReturn(Response.error(new ParseError())); - when(request.getCacheKey()).thenReturn("cache/key"); - Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); - when(mCache.get(anyString())).thenReturn(entry); - - mDispatcher.processRequest(request); - - verifyNoResponse(mDelivery); - verify(mNetworkQueue).put(request); - assertNull(request.getCacheEntry()); - verify(mCache).invalidate("cache/key", true); - verify(request).addMarker("cache-parsing-failed"); - } - - @Test - public void duplicateCacheMiss() throws Exception { - StringRequest secondRequest = - new StringRequest(Request.Method.GET, "http://foo", null, null); - mRequest.setSequence(1); - secondRequest.setSequence(2); - mDispatcher.processRequest(mRequest); - mDispatcher.processRequest(secondRequest); - verify(mNetworkQueue).put(mRequest); - verifyNoResponse(mDelivery); - } - - @Test - public void tripleCacheMiss_networkErrorOnFirst() throws Exception { - StringRequest secondRequest = - new StringRequest(Request.Method.GET, "http://foo", null, null); - StringRequest thirdRequest = - new StringRequest(Request.Method.GET, "http://foo", null, null); - mRequest.setSequence(1); - secondRequest.setSequence(2); - thirdRequest.setSequence(3); - mDispatcher.processRequest(mRequest); - mDispatcher.processRequest(secondRequest); - mDispatcher.processRequest(thirdRequest); - - verify(mNetworkQueue).put(mRequest); - verifyNoResponse(mDelivery); - - ((Request) mRequest).notifyListenerResponseNotUsable(); - // Second request should now be in network queue. - verify(mNetworkQueue).put(secondRequest); - // Another unusable response, third request should now be added. - ((Request) secondRequest).notifyListenerResponseNotUsable(); - verify(mNetworkQueue).put(thirdRequest); - } - - @Test - public void duplicateSoftExpiredCacheHit_failedRequest() throws Exception { - Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); - when(mCache.get(anyString())).thenReturn(entry); - - StringRequest secondRequest = - new StringRequest(Request.Method.GET, "http://foo", null, null); - mRequest.setSequence(1); - secondRequest.setSequence(2); - - mDispatcher.processRequest(mRequest); - mDispatcher.processRequest(secondRequest); - - // Soft expiration needs to use the deferred Runnable variant of postResponse, - // so make sure it gets to run. - ArgumentCaptor runnable = ArgumentCaptor.forClass(Runnable.class); - verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture()); - runnable.getValue().run(); - // This way we can verify the behavior of the Runnable as well. - - verify(mNetworkQueue).put(mRequest); - verify(mDelivery) - .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); - - ((Request) mRequest).notifyListenerResponseNotUsable(); - // Second request should now be in network queue. - verify(mNetworkQueue).put(secondRequest); - } - - @Test - public void duplicateSoftExpiredCacheHit_successfulRequest() throws Exception { - Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); - when(mCache.get(anyString())).thenReturn(entry); - - StringRequest secondRequest = - new StringRequest(Request.Method.GET, "http://foo", null, null); - mRequest.setSequence(1); - secondRequest.setSequence(2); - - mDispatcher.processRequest(mRequest); - mDispatcher.processRequest(secondRequest); - - // Soft expiration needs to use the deferred Runnable variant of postResponse, - // so make sure it gets to run. - ArgumentCaptor runnable = ArgumentCaptor.forClass(Runnable.class); - verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture()); - runnable.getValue().run(); - // This way we can verify the behavior of the Runnable as well. - - verify(mNetworkQueue).put(mRequest); - verify(mDelivery) - .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); - - ((Request) mRequest).notifyListenerResponseReceived(Response.success(null, entry)); - // Second request should have delivered response. - verify(mNetworkQueue, never()).put(secondRequest); - verify(mDelivery) - .postResponse(any(Request.class), any(Response.class), any(Runnable.class)); - } - - @Test - public void processRequestNotifiesListener() throws Exception { - RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); - RequestQueue queue = new RequestQueue(mCache, mNetwork, 0, mDelivery); - queue.addRequestEventListener(listener); - mRequest.setRequestQueue(queue); - - Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); - when(mCache.get(anyString())).thenReturn(entry); - mDispatcher.processRequest(mRequest); - - InOrder inOrder = inOrder(listener); - inOrder.verify(listener) - .onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED); - inOrder.verify(listener) - .onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED); - inOrder.verifyNoMoreInteractions(); - } -} diff --git a/src/test/java/com/android/volley/NetworkDispatcherTest.java b/src/test/java/com/android/volley/NetworkDispatcherTest.java deleted file mode 100644 index 74dfe8a..0000000 --- a/src/test/java/com/android/volley/NetworkDispatcherTest.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import com.android.volley.toolbox.NoCache; -import com.android.volley.toolbox.StringRequest; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.concurrent.BlockingQueue; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class NetworkDispatcherTest { - private NetworkDispatcher mDispatcher; - private @Mock ResponseDelivery mDelivery; - private @Mock BlockingQueue> mNetworkQueue; - private @Mock Network mNetwork; - private @Mock Cache mCache; - private StringRequest mRequest; - - private static final byte[] CANNED_DATA = - "Ceci n'est pas une vraie reponse".getBytes(StandardCharsets.UTF_8); - - @Before - public void setUp() throws Exception { - initMocks(this); - mRequest = new StringRequest(Request.Method.GET, "http://foo", null, null); - mDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery); - } - - @Test - public void successPostsResponse() throws Exception { - when(mNetwork.performRequest(any(Request.class))) - .thenReturn(new NetworkResponse(CANNED_DATA)); - mDispatcher.processRequest(mRequest); - - ArgumentCaptor response = ArgumentCaptor.forClass(Response.class); - verify(mDelivery).postResponse(any(Request.class), response.capture()); - assertTrue(response.getValue().isSuccess()); - assertEquals(response.getValue().result, new String(CANNED_DATA, StandardCharsets.UTF_8)); - - verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class)); - } - - @Test - public void successNotifiesListener() throws Exception { - RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); - RequestQueue queue = new RequestQueue(new NoCache(), mNetwork, 0, mDelivery); - queue.addRequestEventListener(listener); - mRequest.setRequestQueue(queue); - - when(mNetwork.performRequest(any(Request.class))) - .thenReturn(new NetworkResponse(CANNED_DATA)); - - mDispatcher.processRequest(mRequest); - - InOrder inOrder = inOrder(listener); - inOrder.verify(listener) - .onRequestEvent( - mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); - inOrder.verify(listener) - .onRequestEvent( - mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void exceptionPostsError() throws Exception { - when(mNetwork.performRequest(any(Request.class))).thenThrow(new ServerError()); - mDispatcher.processRequest(mRequest); - - verify(mDelivery).postError(any(Request.class), any(VolleyError.class)); - verify(mDelivery, never()).postResponse(any(Request.class), any(Response.class)); - } - - @Test - public void exceptionNotifiesListener() throws Exception { - RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); - RequestQueue queue = new RequestQueue(new NoCache(), mNetwork, 0, mDelivery); - queue.addRequestEventListener(listener); - mRequest.setRequestQueue(queue); - - when(mNetwork.performRequest(any(Request.class))).thenThrow(new ServerError()); - - mDispatcher.processRequest(mRequest); - - InOrder inOrder = inOrder(listener); - inOrder.verify(listener) - .onRequestEvent( - mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); - inOrder.verify(listener) - .onRequestEvent( - mRequest, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void shouldCacheFalse() throws Exception { - mRequest.setShouldCache(false); - mDispatcher.processRequest(mRequest); - verify(mCache, never()).put(anyString(), any(Cache.Entry.class)); - } - - @Test - public void shouldCacheTrue() throws Exception { - when(mNetwork.performRequest(any(Request.class))) - .thenReturn(new NetworkResponse(CANNED_DATA)); - mRequest.setShouldCache(true); - mDispatcher.processRequest(mRequest); - ArgumentCaptor entry = ArgumentCaptor.forClass(Cache.Entry.class); - verify(mCache).put(eq(mRequest.getCacheKey()), entry.capture()); - assertTrue(Arrays.equals(entry.getValue().data, CANNED_DATA)); - } -} diff --git a/src/test/java/com/android/volley/NetworkResponseTest.java b/src/test/java/com/android/volley/NetworkResponseTest.java deleted file mode 100644 index 70210da..0000000 --- a/src/test/java/com/android/volley/NetworkResponseTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.android.volley; - -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class NetworkResponseTest { - - @SuppressWarnings("deprecation") - @Test - public void mapToList() { - Map headers = new HashMap<>(); - headers.put("key1", "value1"); - headers.put("key2", "value2"); - - NetworkResponse resp = new NetworkResponse(200, null, headers, false); - - List
expectedHeaders = new ArrayList<>(); - expectedHeaders.add(new Header("key1", "value1")); - expectedHeaders.add(new Header("key2", "value2")); - - assertThat(expectedHeaders, containsInAnyOrder(resp.allHeaders.toArray(new Header[0]))); - } - - @Test - public void listToMap() { - List
headers = new ArrayList<>(); - headers.add(new Header("key1", "value1")); - // Later values should be preferred. - headers.add(new Header("key2", "ignoredvalue")); - headers.add(new Header("key2", "value2")); - - NetworkResponse resp = new NetworkResponse(200, null, false, 0L, headers); - - Map expectedHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - expectedHeaders.put("key1", "value1"); - expectedHeaders.put("key2", "value2"); - - assertEquals(expectedHeaders, resp.headers); - } - - @SuppressWarnings("deprecation") - @Test - public void nullValuesDontCrash() { - new NetworkResponse(null); - new NetworkResponse(null, null); - new NetworkResponse(200, null, null, false); - new NetworkResponse(200, null, null, false, 0L); - new NetworkResponse(200, null, false, 0L, null); - } -} diff --git a/src/test/java/com/android/volley/RequestQueueIntegrationTest.java b/src/test/java/com/android/volley/RequestQueueIntegrationTest.java deleted file mode 100644 index a2bfbc6..0000000 --- a/src/test/java/com/android/volley/RequestQueueIntegrationTest.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import com.android.volley.Request.Priority; -import com.android.volley.RequestQueue.RequestFinishedListener; -import com.android.volley.mock.MockRequest; -import com.android.volley.mock.ShadowSystemClock; -import com.android.volley.toolbox.NoCache; -import com.android.volley.utils.ImmediateResponseDelivery; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -/** - * Integration tests for {@link RequestQueue} that verify its behavior in conjunction with real - * dispatcher, queues and Requests. - * - *

The Network is mocked out. - */ -@RunWith(RobolectricTestRunner.class) -@Config(shadows = {ShadowSystemClock.class}) -public class RequestQueueIntegrationTest { - - private ResponseDelivery mDelivery; - @Mock private Network mMockNetwork; - @Mock private RequestFinishedListener mMockListener; - @Mock private RequestFinishedListener mMockListener2; - - @Before - public void setUp() throws Exception { - mDelivery = new ImmediateResponseDelivery(); - initMocks(this); - } - - @Test - public void add_requestProcessedInCorrectOrder() throws Exception { - // Enqueue 2 requests with different cache keys, and different priorities. The second, - // higher priority request takes 20ms. - // Assert that the first request is only handled after the first one has been parsed and - // delivered. - MockRequest lowerPriorityReq = new MockRequest(); - MockRequest higherPriorityReq = new MockRequest(); - lowerPriorityReq.setCacheKey("1"); - higherPriorityReq.setCacheKey("2"); - lowerPriorityReq.setPriority(Priority.LOW); - higherPriorityReq.setPriority(Priority.HIGH); - - Answer delayAnswer = - new Answer() { - @Override - public NetworkResponse answer(InvocationOnMock invocationOnMock) - throws Throwable { - Thread.sleep(20); - return mock(NetworkResponse.class); - } - }; - // delay only for higher request - when(mMockNetwork.performRequest(higherPriorityReq)).thenAnswer(delayAnswer); - when(mMockNetwork.performRequest(lowerPriorityReq)).thenReturn(mock(NetworkResponse.class)); - - RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); - queue.addRequestFinishedListener(mMockListener); - queue.add(lowerPriorityReq); - queue.add(higherPriorityReq); - queue.start(); - - InOrder inOrder = inOrder(mMockListener); - // verify higherPriorityReq goes through first - inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(higherPriorityReq); - // verify lowerPriorityReq goes last - inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(lowerPriorityReq); - - queue.stop(); - } - - /** Asserts that requests with same cache key are processed in order. */ - @Test - public void add_dedupeByCacheKey() throws Exception { - // Enqueue 2 requests with the same cache key. The first request takes 20ms. Assert that the - // second request is only handled after the first one has been parsed and delivered. - MockRequest req1 = new MockRequest(); - MockRequest req2 = new MockRequest(); - Answer delayAnswer = - new Answer() { - @Override - public NetworkResponse answer(InvocationOnMock invocationOnMock) - throws Throwable { - Thread.sleep(20); - return mock(NetworkResponse.class); - } - }; - // delay only for first - when(mMockNetwork.performRequest(req1)).thenAnswer(delayAnswer); - when(mMockNetwork.performRequest(req2)).thenReturn(mock(NetworkResponse.class)); - - RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 3, mDelivery); - queue.addRequestFinishedListener(mMockListener); - queue.add(req1); - queue.add(req2); - queue.start(); - - InOrder inOrder = inOrder(mMockListener); - // verify req1 goes through first - inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(req1); - // verify req2 goes last - inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(req2); - - queue.stop(); - } - - /** Verify RequestFinishedListeners are informed when requests are canceled. */ - @Test - public void add_requestFinishedListenerCanceled() throws Exception { - MockRequest request = new MockRequest(); - Answer delayAnswer = - new Answer() { - @Override - public NetworkResponse answer(InvocationOnMock invocationOnMock) - throws Throwable { - Thread.sleep(200); - return mock(NetworkResponse.class); - } - }; - RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); - - when(mMockNetwork.performRequest(request)).thenAnswer(delayAnswer); - - queue.addRequestFinishedListener(mMockListener); - queue.start(); - queue.add(request); - - request.cancel(); - verify(mMockListener, timeout(10000)).onRequestFinished(request); - queue.stop(); - } - - /** Verify RequestFinishedListeners are informed when requests are successfully delivered. */ - @Test - public void add_requestFinishedListenerSuccess() throws Exception { - MockRequest request = new MockRequest(); - RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); - - queue.addRequestFinishedListener(mMockListener); - queue.addRequestFinishedListener(mMockListener2); - queue.start(); - queue.add(request); - - verify(mMockListener, timeout(10000)).onRequestFinished(request); - verify(mMockListener2, timeout(10000)).onRequestFinished(request); - - queue.stop(); - } - - /** Verify RequestFinishedListeners are informed when request errors. */ - @Test - public void add_requestFinishedListenerError() throws Exception { - MockRequest request = new MockRequest(); - RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); - - when(mMockNetwork.performRequest(request)).thenThrow(new VolleyError()); - - queue.addRequestFinishedListener(mMockListener); - queue.start(); - queue.add(request); - - verify(mMockListener, timeout(10000)).onRequestFinished(request); - queue.stop(); - } -} diff --git a/src/test/java/com/android/volley/RequestQueueTest.java b/src/test/java/com/android/volley/RequestQueueTest.java deleted file mode 100644 index ba9b0f8..0000000 --- a/src/test/java/com/android/volley/RequestQueueTest.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import com.android.volley.mock.ShadowSystemClock; -import com.android.volley.toolbox.NoCache; -import com.android.volley.toolbox.StringRequest; -import com.android.volley.utils.ImmediateResponseDelivery; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -/** Unit tests for RequestQueue, with all dependencies mocked out */ -@RunWith(RobolectricTestRunner.class) -@Config(shadows = {ShadowSystemClock.class}) -public class RequestQueueTest { - - private ResponseDelivery mDelivery; - @Mock private Network mMockNetwork; - - @Before - public void setUp() throws Exception { - mDelivery = new ImmediateResponseDelivery(); - initMocks(this); - } - - @Test - public void cancelAll_onlyCorrectTag() throws Exception { - RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); - Object tagA = new Object(); - Object tagB = new Object(); - StringRequest req1 = mock(StringRequest.class); - when(req1.getTag()).thenReturn(tagA); - StringRequest req2 = mock(StringRequest.class); - when(req2.getTag()).thenReturn(tagB); - StringRequest req3 = mock(StringRequest.class); - when(req3.getTag()).thenReturn(tagA); - StringRequest req4 = mock(StringRequest.class); - when(req4.getTag()).thenReturn(tagA); - - queue.add(req1); // A - queue.add(req2); // B - queue.add(req3); // A - queue.cancelAll(tagA); - queue.add(req4); // A - - verify(req1).cancel(); // A cancelled - verify(req3).cancel(); // A cancelled - verify(req2, never()).cancel(); // B not cancelled - verify(req4, never()).cancel(); // A added after cancel not cancelled - } - - @Test - public void add_notifiesListener() throws Exception { - RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); - RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); - queue.addRequestEventListener(listener); - StringRequest req = mock(StringRequest.class); - - queue.add(req); - - verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_QUEUED); - verifyNoMoreInteractions(listener); - } - - @Test - public void finish_notifiesListener() throws Exception { - RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); - RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); - queue.addRequestEventListener(listener); - StringRequest req = mock(StringRequest.class); - - queue.finish(req); - - verify(listener).onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_FINISHED); - verifyNoMoreInteractions(listener); - } - - @Test - public void sendRequestEvent_notifiesListener() throws Exception { - StringRequest req = mock(StringRequest.class); - RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); - RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); - queue.addRequestEventListener(listener); - - queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); - - verify(listener) - .onRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); - verifyNoMoreInteractions(listener); - } - - @Test - public void removeRequestEventListener_removesListener() throws Exception { - StringRequest req = mock(StringRequest.class); - RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); - RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); - queue.addRequestEventListener(listener); - queue.removeRequestEventListener(listener); - - queue.sendRequestEvent(req, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); - - verifyNoMoreInteractions(listener); - } -} diff --git a/src/test/java/com/android/volley/RequestTest.java b/src/test/java/com/android/volley/RequestTest.java deleted file mode 100644 index cced39f..0000000 --- a/src/test/java/com/android/volley/RequestTest.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.MockitoAnnotations.initMocks; - -import com.android.volley.Request.Method; -import com.android.volley.Request.Priority; -import com.android.volley.toolbox.NoCache; -import java.util.Collections; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class RequestTest { - private @Mock ResponseDelivery mDelivery; - private @Mock Network mNetwork; - - @Before - public void setUp() throws Exception { - initMocks(this); - } - - @Test - public void compareTo() { - int sequence = 0; - TestRequest low = new TestRequest(Priority.LOW); - low.setSequence(sequence++); - TestRequest low2 = new TestRequest(Priority.LOW); - low2.setSequence(sequence++); - TestRequest high = new TestRequest(Priority.HIGH); - high.setSequence(sequence++); - TestRequest immediate = new TestRequest(Priority.IMMEDIATE); - immediate.setSequence(sequence++); - - // "Low" should sort higher because it's really processing order. - assertTrue(low.compareTo(high) > 0); - assertTrue(high.compareTo(low) < 0); - assertTrue(low.compareTo(low2) < 0); - assertTrue(low.compareTo(immediate) > 0); - assertTrue(immediate.compareTo(high) < 0); - } - - private static class TestRequest extends Request { - private Priority mPriority = Priority.NORMAL; - - public TestRequest(Priority priority) { - super(Request.Method.GET, "", null); - mPriority = priority; - } - - @Override - public Priority getPriority() { - return mPriority; - } - - @Override - protected void deliverResponse(Object response) {} - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - return null; - } - } - - @Test - public void urlParsing() { - UrlParseRequest nullUrl = new UrlParseRequest(null); - assertEquals(0, nullUrl.getTrafficStatsTag()); - UrlParseRequest emptyUrl = new UrlParseRequest(""); - assertEquals(0, emptyUrl.getTrafficStatsTag()); - UrlParseRequest noHost = new UrlParseRequest("http:///"); - assertEquals(0, noHost.getTrafficStatsTag()); - UrlParseRequest badProtocol = new UrlParseRequest("bad:http://foo"); - assertEquals(0, badProtocol.getTrafficStatsTag()); - UrlParseRequest goodProtocol = new UrlParseRequest("http://foo"); - assertFalse(0 == goodProtocol.getTrafficStatsTag()); - } - - @Test - public void getCacheKey() { - assertEquals( - "http://example.com", - new UrlParseRequest(Method.GET, "http://example.com").getCacheKey()); - assertEquals( - "http://example.com", - new UrlParseRequest(Method.DEPRECATED_GET_OR_POST, "http://example.com") - .getCacheKey()); - assertEquals( - "1-http://example.com", - new UrlParseRequest(Method.POST, "http://example.com").getCacheKey()); - assertEquals( - "2-http://example.com", - new UrlParseRequest(Method.PUT, "http://example.com").getCacheKey()); - } - - private static class UrlParseRequest extends Request { - UrlParseRequest(String url) { - this(Method.GET, url); - } - - UrlParseRequest(int method, String url) { - super(method, url, null); - } - - @Override - protected void deliverResponse(Object response) {} - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - return null; - } - } - - @Test - public void nullKeyInPostParams() throws Exception { - Request request = - new Request(Method.POST, "url", null) { - @Override - protected void deliverResponse(Object response) {} - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - return null; - } - - @Override - protected Map getParams() { - return Collections.singletonMap(null, "value"); - } - - @Override - protected Map getPostParams() { - return Collections.singletonMap(null, "value"); - } - }; - try { - request.getBody(); - } catch (IllegalArgumentException e) { - // expected - } - try { - request.getPostBody(); - } catch (IllegalArgumentException e) { - // expected - } - } - - @Test - public void nullValueInPostParams() throws Exception { - Request request = - new Request(Method.POST, "url", null) { - @Override - protected void deliverResponse(Object response) {} - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - return null; - } - - @Override - protected Map getParams() { - return Collections.singletonMap("key", null); - } - - @Override - protected Map getPostParams() { - return Collections.singletonMap("key", null); - } - }; - try { - request.getBody(); - } catch (IllegalArgumentException e) { - // expected - } - try { - request.getPostBody(); - } catch (IllegalArgumentException e) { - // expected - } - } - - @Test - public void sendEvent_notifiesListeners() throws Exception { - RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class); - RequestQueue queue = new RequestQueue(new NoCache(), mNetwork, 0, mDelivery); - queue.addRequestEventListener(listener); - - Request request = - new Request(Method.POST, "url", null) { - @Override - protected void deliverResponse(Object response) {} - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - return null; - } - }; - request.setRequestQueue(queue); - - request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); - - verify(listener) - .onRequestEvent( - request, RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); - verifyNoMoreInteractions(listener); - } -} diff --git a/src/test/java/com/android/volley/ResponseDeliveryTest.java b/src/test/java/com/android/volley/ResponseDeliveryTest.java deleted file mode 100644 index 6e71c3b..0000000 --- a/src/test/java/com/android/volley/ResponseDeliveryTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import com.android.volley.mock.MockRequest; -import com.android.volley.utils.CacheTestUtils; -import com.android.volley.utils.ImmediateResponseDelivery; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class ResponseDeliveryTest { - - private ExecutorDelivery mDelivery; - private MockRequest mRequest; - private Response mSuccessResponse; - - @Before - public void setUp() throws Exception { - // Make the delivery just run its posted responses immediately. - mDelivery = new ImmediateResponseDelivery(); - mRequest = new MockRequest(); - mRequest.setSequence(1); - byte[] data = new byte[16]; - Cache.Entry cacheEntry = CacheTestUtils.makeRandomCacheEntry(data); - mSuccessResponse = Response.success(data, cacheEntry); - } - - @Test - public void postResponseCallsDeliverResponse() { - mDelivery.postResponse(mRequest, mSuccessResponse); - assertTrue(mRequest.deliverResponse_called); - assertFalse(mRequest.deliverError_called); - } - - @Test - public void postResponseSuppressesCanceled() { - mRequest.cancel(); - mDelivery.postResponse(mRequest, mSuccessResponse); - assertFalse(mRequest.deliverResponse_called); - assertFalse(mRequest.deliverError_called); - } - - @Test - public void postErrorCallsDeliverError() { - Response errorResponse = Response.error(new ServerError()); - - mDelivery.postResponse(mRequest, errorResponse); - assertTrue(mRequest.deliverError_called); - assertFalse(mRequest.deliverResponse_called); - } -} diff --git a/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java b/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java deleted file mode 100644 index cedb6ff..0000000 --- a/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java +++ /dev/null @@ -1,381 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.cronet; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.android.volley.Header; -import com.android.volley.cronet.CronetHttpStack.CurlCommandLogger; -import com.android.volley.mock.TestRequest; -import com.android.volley.toolbox.AsyncHttpStack.OnRequestComplete; -import com.android.volley.toolbox.UrlRewriter; -import com.google.common.collect.ImmutableMap; -import com.google.common.util.concurrent.MoreExecutors; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import org.chromium.net.CronetEngine; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; - -@RunWith(RobolectricTestRunner.class) -public class CronetHttpStackTest { - @Mock private CurlCommandLogger mMockCurlCommandLogger; - @Mock private OnRequestComplete mMockOnRequestComplete; - @Mock private UrlRewriter mMockUrlRewriter; - - // A fake would be ideal here, but Cronet doesn't (yet) provide one, and at the moment we aren't - // exercising the full response flow. - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private CronetEngine mMockCronetEngine; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void curlLogging_disabled() { - CronetHttpStack stack = - createStack( - new Consumer() { - @Override - public void accept(CronetHttpStack.Builder builder) { - // Default parameters should not enable cURL logging. - } - }); - - stack.executeRequest( - new TestRequest.Get(), ImmutableMap.of(), mMockOnRequestComplete); - - verify(mMockCurlCommandLogger, never()).logCurlCommand(anyString()); - } - - @Test - public void curlLogging_simpleTextRequest() { - CronetHttpStack stack = - createStack( - new Consumer() { - @Override - public void accept(CronetHttpStack.Builder builder) { - builder.setCurlLoggingEnabled(true); - } - }); - - stack.executeRequest( - new TestRequest.Get(), ImmutableMap.of(), mMockOnRequestComplete); - - ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); - verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); - assertEquals("curl -X GET \"http://foo.com\"", curlCommandCaptor.getValue()); - } - - @Test - public void curlLogging_rewrittenUrl() { - CronetHttpStack stack = - createStack( - new Consumer() { - @Override - public void accept(CronetHttpStack.Builder builder) { - builder.setCurlLoggingEnabled(true) - .setUrlRewriter(mMockUrlRewriter); - } - }); - when(mMockUrlRewriter.rewriteUrl("http://foo.com")).thenReturn("http://bar.com"); - - stack.executeRequest( - new TestRequest.Get(), ImmutableMap.of(), mMockOnRequestComplete); - - ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); - verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); - assertEquals("curl -X GET \"http://bar.com\"", curlCommandCaptor.getValue()); - } - - @Test - public void curlLogging_headers_withoutTokens() { - CronetHttpStack stack = - createStack( - new Consumer() { - @Override - public void accept(CronetHttpStack.Builder builder) { - builder.setCurlLoggingEnabled(true); - } - }); - - stack.executeRequest( - new TestRequest.Delete() { - @Override - public Map getHeaders() { - return ImmutableMap.of( - "SomeHeader", "SomeValue", - "Authorization", "SecretToken"); - } - }, - ImmutableMap.of("SomeOtherHeader", "SomeValue"), - mMockOnRequestComplete); - - ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); - verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); - // NOTE: Header order is stable because the implementation uses a TreeMap. - assertEquals( - "curl -X DELETE --header \"Authorization: [REDACTED]\" " - + "--header \"SomeHeader: SomeValue\" " - + "--header \"SomeOtherHeader: SomeValue\" \"http://foo.com\"", - curlCommandCaptor.getValue()); - } - - @Test - public void curlLogging_headers_withTokens() { - CronetHttpStack stack = - createStack( - new Consumer() { - @Override - public void accept(CronetHttpStack.Builder builder) { - builder.setCurlLoggingEnabled(true) - .setLogAuthTokensInCurlCommands(true); - } - }); - - stack.executeRequest( - new TestRequest.Delete() { - @Override - public Map getHeaders() { - return ImmutableMap.of( - "SomeHeader", "SomeValue", - "Authorization", "SecretToken"); - } - }, - ImmutableMap.of("SomeOtherHeader", "SomeValue"), - mMockOnRequestComplete); - - ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); - verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); - // NOTE: Header order is stable because the implementation uses a TreeMap. - assertEquals( - "curl -X DELETE --header \"Authorization: SecretToken\" " - + "--header \"SomeHeader: SomeValue\" " - + "--header \"SomeOtherHeader: SomeValue\" \"http://foo.com\"", - curlCommandCaptor.getValue()); - } - - @Test - public void curlLogging_textRequest() { - CronetHttpStack stack = - createStack( - new Consumer() { - @Override - public void accept(CronetHttpStack.Builder builder) { - builder.setCurlLoggingEnabled(true); - } - }); - - stack.executeRequest( - new TestRequest.PostWithBody() { - @Override - public byte[] getBody() { - try { - return "hello".getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - - @Override - public String getBodyContentType() { - return "text/plain; charset=UTF-8"; - } - }, - ImmutableMap.of(), - mMockOnRequestComplete); - - ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); - verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); - assertEquals( - "curl -X POST " - + "--header \"Content-Type: text/plain; charset=UTF-8\" \"http://foo.com\" " - + "--data-ascii \"hello\"", - curlCommandCaptor.getValue()); - } - - @Test - public void curlLogging_gzipTextRequest() { - CronetHttpStack stack = - createStack( - new Consumer() { - @Override - public void accept(CronetHttpStack.Builder builder) { - builder.setCurlLoggingEnabled(true); - } - }); - - stack.executeRequest( - new TestRequest.PostWithBody() { - @Override - public byte[] getBody() { - return new byte[] {1, 2, 3, 4, 5}; - } - - @Override - public String getBodyContentType() { - return "text/plain"; - } - - @Override - public Map getHeaders() { - return ImmutableMap.of("Content-Encoding", "gzip, identity"); - } - }, - ImmutableMap.of(), - mMockOnRequestComplete); - - ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); - verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); - assertEquals( - "echo 'AQIDBAU=' | base64 -d > /tmp/$$.bin; curl -X POST " - + "--header \"Content-Encoding: gzip, identity\" " - + "--header \"Content-Type: text/plain\" \"http://foo.com\" " - + "--data-binary @/tmp/$$.bin", - curlCommandCaptor.getValue()); - } - - @Test - public void curlLogging_binaryRequest() { - CronetHttpStack stack = - createStack( - new Consumer() { - @Override - public void accept(CronetHttpStack.Builder builder) { - builder.setCurlLoggingEnabled(true); - } - }); - - stack.executeRequest( - new TestRequest.PostWithBody() { - @Override - public byte[] getBody() { - return new byte[] {1, 2, 3, 4, 5}; - } - - @Override - public String getBodyContentType() { - return "application/octet-stream"; - } - }, - ImmutableMap.of(), - mMockOnRequestComplete); - - ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); - verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); - assertEquals( - "echo 'AQIDBAU=' | base64 -d > /tmp/$$.bin; curl -X POST " - + "--header \"Content-Type: application/octet-stream\" \"http://foo.com\" " - + "--data-binary @/tmp/$$.bin", - curlCommandCaptor.getValue()); - } - - @Test - public void curlLogging_largeRequest() { - CronetHttpStack stack = - createStack( - new Consumer() { - @Override - public void accept(CronetHttpStack.Builder builder) { - builder.setCurlLoggingEnabled(true); - } - }); - - stack.executeRequest( - new TestRequest.PostWithBody() { - @Override - public byte[] getBody() { - return new byte[2048]; - } - - @Override - public String getBodyContentType() { - return "application/octet-stream"; - } - }, - ImmutableMap.of(), - mMockOnRequestComplete); - - ArgumentCaptor curlCommandCaptor = ArgumentCaptor.forClass(String.class); - verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture()); - assertEquals( - "curl -X POST " - + "--header \"Content-Type: application/octet-stream\" \"http://foo.com\" " - + "[REQUEST BODY TOO LARGE TO INCLUDE]", - curlCommandCaptor.getValue()); - } - - @Test - public void getHeadersEmptyTest() { - List> list = new ArrayList<>(); - List
actual = CronetHttpStack.getHeaders(list); - List
expected = new ArrayList<>(); - assertEquals(expected, actual); - } - - @Test - public void getHeadersNonEmptyTest() { - Map headers = new HashMap<>(); - for (int i = 1; i < 5; i++) { - headers.put("key" + i, "value" + i); - } - List> list = new ArrayList<>(headers.entrySet()); - List
actual = CronetHttpStack.getHeaders(list); - List
expected = new ArrayList<>(); - for (int i = 1; i < 5; i++) { - expected.add(new Header("key" + i, "value" + i)); - } - assertHeaderListsEqual(expected, actual); - } - - private void assertHeaderListsEqual(List
expected, List
actual) { - assertEquals(expected.size(), actual.size()); - for (int i = 0; i < expected.size(); i++) { - assertEquals(expected.get(i).getName(), actual.get(i).getName()); - assertEquals(expected.get(i).getValue(), actual.get(i).getValue()); - } - } - - private CronetHttpStack createStack(Consumer stackEditor) { - CronetHttpStack.Builder builder = - new CronetHttpStack.Builder(RuntimeEnvironment.application) - .setCronetEngine(mMockCronetEngine) - .setCurlCommandLogger(mMockCurlCommandLogger); - stackEditor.accept(builder); - CronetHttpStack stack = builder.build(); - stack.setBlockingExecutor(MoreExecutors.newDirectExecutorService()); - stack.setNonBlockingExecutor(MoreExecutors.newDirectExecutorService()); - return stack; - } -} diff --git a/src/test/java/com/android/volley/mock/MockAsyncStack.java b/src/test/java/com/android/volley/mock/MockAsyncStack.java deleted file mode 100644 index 5ea8343..0000000 --- a/src/test/java/com/android/volley/mock/MockAsyncStack.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.mock; - -import com.android.volley.AuthFailureError; -import com.android.volley.Request; -import com.android.volley.toolbox.AsyncHttpStack; -import com.android.volley.toolbox.HttpResponse; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -public class MockAsyncStack extends AsyncHttpStack { - - private HttpResponse mResponseToReturn; - - private IOException mExceptionToThrow; - - private String mLastUrl; - - private Map mLastHeaders; - - private byte[] mLastPostBody; - - public String getLastUrl() { - return mLastUrl; - } - - public Map getLastHeaders() { - return mLastHeaders; - } - - public byte[] getLastPostBody() { - return mLastPostBody; - } - - public void setResponseToReturn(HttpResponse response) { - mResponseToReturn = response; - } - - public void setExceptionToThrow(IOException exception) { - mExceptionToThrow = exception; - } - - @Override - public void executeRequest( - Request request, Map additionalHeaders, OnRequestComplete callback) { - if (mExceptionToThrow != null) { - callback.onError(mExceptionToThrow); - return; - } - mLastUrl = request.getUrl(); - mLastHeaders = new HashMap<>(); - try { - if (request.getHeaders() != null) { - mLastHeaders.putAll(request.getHeaders()); - } - } catch (AuthFailureError authFailureError) { - callback.onAuthError(authFailureError); - return; - } - if (additionalHeaders != null) { - mLastHeaders.putAll(additionalHeaders); - } - try { - mLastPostBody = request.getBody(); - } catch (AuthFailureError e) { - mLastPostBody = null; - } - callback.onSuccess(mResponseToReturn); - } -} diff --git a/src/test/java/com/android/volley/mock/MockHttpStack.java b/src/test/java/com/android/volley/mock/MockHttpStack.java deleted file mode 100644 index b86e7a0..0000000 --- a/src/test/java/com/android/volley/mock/MockHttpStack.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.mock; - -import com.android.volley.AuthFailureError; -import com.android.volley.Request; -import com.android.volley.toolbox.BaseHttpStack; -import com.android.volley.toolbox.HttpResponse; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -public class MockHttpStack extends BaseHttpStack { - - private HttpResponse mResponseToReturn; - - private IOException mExceptionToThrow; - - private String mLastUrl; - - private Map mLastHeaders; - - private byte[] mLastPostBody; - - public String getLastUrl() { - return mLastUrl; - } - - public Map getLastHeaders() { - return mLastHeaders; - } - - public byte[] getLastPostBody() { - return mLastPostBody; - } - - public void setResponseToReturn(HttpResponse response) { - mResponseToReturn = response; - } - - public void setExceptionToThrow(IOException exception) { - mExceptionToThrow = exception; - } - - @Override - public HttpResponse executeRequest(Request request, Map additionalHeaders) - throws IOException, AuthFailureError { - if (mExceptionToThrow != null) { - throw mExceptionToThrow; - } - mLastUrl = request.getUrl(); - mLastHeaders = new HashMap<>(); - if (request.getHeaders() != null) { - mLastHeaders.putAll(request.getHeaders()); - } - if (additionalHeaders != null) { - mLastHeaders.putAll(additionalHeaders); - } - try { - mLastPostBody = request.getBody(); - } catch (AuthFailureError e) { - mLastPostBody = null; - } - return mResponseToReturn; - } -} diff --git a/src/test/java/com/android/volley/mock/MockRequest.java b/src/test/java/com/android/volley/mock/MockRequest.java deleted file mode 100644 index 6fc26b4..0000000 --- a/src/test/java/com/android/volley/mock/MockRequest.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.mock; - -import com.android.volley.NetworkResponse; -import com.android.volley.Request; -import com.android.volley.Response; -import com.android.volley.Response.ErrorListener; -import com.android.volley.VolleyError; -import com.android.volley.utils.CacheTestUtils; -import java.util.HashMap; -import java.util.Map; - -public class MockRequest extends Request { - public MockRequest() { - super(Request.Method.GET, "http://foo.com", null); - } - - public MockRequest(String url, ErrorListener listener) { - super(Request.Method.GET, url, listener); - } - - private Map mPostParams = new HashMap(); - - public void setPostParams(Map postParams) { - mPostParams = postParams; - } - - @Override - public Map getPostParams() { - return mPostParams; - } - - private String mCacheKey = super.getCacheKey(); - - public void setCacheKey(String cacheKey) { - mCacheKey = cacheKey; - } - - @Override - public String getCacheKey() { - return mCacheKey; - } - - public boolean deliverResponse_called = false; - public boolean parseResponse_called = false; - - @Override - protected void deliverResponse(byte[] response) { - deliverResponse_called = true; - } - - public boolean deliverError_called = false; - - @Override - public void deliverError(VolleyError error) { - super.deliverError(error); - deliverError_called = true; - } - - public boolean cancel_called = false; - - @Override - public void cancel() { - cancel_called = true; - super.cancel(); - } - - private Priority mPriority = super.getPriority(); - - public void setPriority(Priority priority) { - mPriority = priority; - } - - @Override - public Priority getPriority() { - return mPriority; - } - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - parseResponse_called = true; - return Response.success(response.data, CacheTestUtils.makeRandomCacheEntry(response.data)); - } -} diff --git a/src/test/java/com/android/volley/mock/ShadowSystemClock.java b/src/test/java/com/android/volley/mock/ShadowSystemClock.java deleted file mode 100644 index 6d75d4b..0000000 --- a/src/test/java/com/android/volley/mock/ShadowSystemClock.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.mock; - -import android.os.SystemClock; -import org.robolectric.annotation.Implements; - -@Implements(value = SystemClock.class, callThroughByDefault = true) -public class ShadowSystemClock { - public static long elapsedRealtime() { - return 0; - } -} diff --git a/src/test/java/com/android/volley/mock/TestRequest.java b/src/test/java/com/android/volley/mock/TestRequest.java deleted file mode 100644 index f397f01..0000000 --- a/src/test/java/com/android/volley/mock/TestRequest.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.mock; - -import com.android.volley.NetworkResponse; -import com.android.volley.Request; -import com.android.volley.Response; -import java.util.HashMap; -import java.util.Map; - -public class TestRequest { - private static final String TEST_URL = "http://foo.com"; - - /** Base Request class for testing allowing both the deprecated and new constructor. */ - private static class Base extends Request { - @SuppressWarnings("deprecation") - public Base(String url, Response.ErrorListener listener) { - super(url, listener); - } - - public Base(int method, String url, Response.ErrorListener listener) { - super(method, url, listener); - } - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - return null; - } - - @Override - protected void deliverResponse(byte[] response) {} - } - - /** Test example of a GET request in the deprecated style. */ - public static class DeprecatedGet extends Base { - public DeprecatedGet() { - super(TEST_URL, null); - } - } - - /** Test example of a POST request in the deprecated style. */ - public static class DeprecatedPost extends Base { - private final Map mPostParams; - - public DeprecatedPost() { - super(TEST_URL, null); - mPostParams = new HashMap(); - mPostParams.put("requestpost", "foo"); - } - - @Override - protected Map getPostParams() { - return mPostParams; - } - } - - /** Test example of a GET request in the new style. */ - public static class Get extends Base { - public Get() { - super(Method.GET, TEST_URL, null); - } - } - - /** - * Test example of a POST request in the new style. In the new style, it is possible to have a - * POST with no body. - */ - public static class Post extends Base { - public Post() { - super(Method.POST, TEST_URL, null); - } - } - - /** Test example of a POST request in the new style with a body. */ - public static class PostWithBody extends Post { - private final Map mParams; - - public PostWithBody() { - mParams = new HashMap(); - mParams.put("testKey", "testValue"); - } - - @Override - public Map getParams() { - return mParams; - } - } - - /** - * Test example of a PUT request in the new style. In the new style, it is possible to have a - * PUT with no body. - */ - public static class Put extends Base { - public Put() { - super(Method.PUT, TEST_URL, null); - } - } - - /** Test example of a PUT request in the new style with a body. */ - public static class PutWithBody extends Put { - private Map mParams = new HashMap(); - - public PutWithBody() { - mParams = new HashMap(); - mParams.put("testKey", "testValue"); - } - - @Override - public Map getParams() { - return mParams; - } - } - - /** Test example of a DELETE request in the new style. */ - public static class Delete extends Base { - public Delete() { - super(Method.DELETE, TEST_URL, null); - } - } - - /** Test example of a HEAD request in the new style. */ - public static class Head extends Base { - public Head() { - super(Method.HEAD, TEST_URL, null); - } - } - - /** Test example of a OPTIONS request in the new style. */ - public static class Options extends Base { - public Options() { - super(Method.OPTIONS, TEST_URL, null); - } - } - - /** Test example of a TRACE request in the new style. */ - public static class Trace extends Base { - public Trace() { - super(Method.TRACE, TEST_URL, null); - } - } - - /** Test example of a PATCH request in the new style. */ - public static class Patch extends Base { - public Patch() { - super(Method.PATCH, TEST_URL, null); - } - } - - /** Test example of a PATCH request in the new style with a body. */ - public static class PatchWithBody extends Patch { - private Map mParams = new HashMap(); - - public PatchWithBody() { - mParams = new HashMap(); - mParams.put("testKey", "testValue"); - } - - @Override - public Map getParams() { - return mParams; - } - } -} diff --git a/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java b/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java deleted file mode 100644 index dbd6535..0000000 --- a/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.android.volley.toolbox; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.when; - -import com.android.volley.Header; -import com.android.volley.Request; -import com.android.volley.mock.TestRequest; -import java.io.IOException; -import java.io.InputStream; -import java.net.SocketTimeoutException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; -import org.apache.http.conn.ConnectTimeoutException; -import org.apache.http.message.BasicHeader; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class AdaptedHttpStackTest { - private static final Request REQUEST = new TestRequest.Get(); - private static final Map ADDITIONAL_HEADERS = Collections.emptyMap(); - - @Mock private HttpStack mHttpStack; - @Mock private HttpResponse mHttpResponse; - @Mock private StatusLine mStatusLine; - @Mock private HttpEntity mHttpEntity; - @Mock private InputStream mContent; - - private AdaptedHttpStack mAdaptedHttpStack; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mAdaptedHttpStack = new AdaptedHttpStack(mHttpStack); - when(mHttpResponse.getStatusLine()).thenReturn(mStatusLine); - } - - @Test(expected = SocketTimeoutException.class) - public void requestTimeout() throws Exception { - when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)) - .thenThrow(new ConnectTimeoutException()); - - mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); - } - - @Test - public void emptyResponse() throws Exception { - when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); - when(mStatusLine.getStatusCode()).thenReturn(12345); - when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]); - - com.android.volley.toolbox.HttpResponse response = - mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); - - assertEquals(12345, response.getStatusCode()); - assertEquals(Collections.emptyList(), response.getHeaders()); - assertNull(response.getContent()); - } - - @Test - public void nonEmptyResponse() throws Exception { - when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); - when(mStatusLine.getStatusCode()).thenReturn(12345); - when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]); - when(mHttpResponse.getEntity()).thenReturn(mHttpEntity); - when(mHttpEntity.getContentLength()).thenReturn((long) Integer.MAX_VALUE); - when(mHttpEntity.getContent()).thenReturn(mContent); - - com.android.volley.toolbox.HttpResponse response = - mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); - - assertEquals(12345, response.getStatusCode()); - assertEquals(Collections.emptyList(), response.getHeaders()); - assertEquals(Integer.MAX_VALUE, response.getContentLength()); - assertSame(mContent, response.getContent()); - } - - @Test(expected = IOException.class) - public void responseTooBig() throws Exception { - when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); - when(mStatusLine.getStatusCode()).thenReturn(12345); - when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]); - when(mHttpResponse.getEntity()).thenReturn(mHttpEntity); - when(mHttpEntity.getContentLength()).thenReturn(Integer.MAX_VALUE + 1L); - when(mHttpEntity.getContent()).thenReturn(mContent); - - mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); - } - - @Test - public void responseWithHeaders() throws Exception { - when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); - when(mStatusLine.getStatusCode()).thenReturn(12345); - when(mHttpResponse.getAllHeaders()) - .thenReturn( - new org.apache.http.Header[] { - new BasicHeader("header1", "value1_B"), - new BasicHeader("header3", "value3"), - new BasicHeader("HEADER2", "value2"), - new BasicHeader("header1", "value1_A") - }); - - com.android.volley.toolbox.HttpResponse response = - mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); - - assertEquals(12345, response.getStatusCode()); - assertNull(response.getContent()); - - List
expectedHeaders = new ArrayList<>(); - expectedHeaders.add(new Header("header1", "value1_B")); - expectedHeaders.add(new Header("header3", "value3")); - expectedHeaders.add(new Header("HEADER2", "value2")); - expectedHeaders.add(new Header("header1", "value1_A")); - assertEquals(expectedHeaders, response.getHeaders()); - } -} diff --git a/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java b/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java deleted file mode 100644 index 982eda2..0000000 --- a/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import com.android.volley.AuthFailureError; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class AndroidAuthenticatorTest { - @Mock private AccountManager mAccountManager; - @Mock private AccountManagerFuture mFuture; - private Account mAccount; - private AndroidAuthenticator mAuthenticator; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mAccount = new Account("coolperson", "cooltype"); - mAuthenticator = new AndroidAuthenticator(mAccountManager, mAccount, "cooltype", false); - } - - @Test(expected = AuthFailureError.class) - public void failedGetAuthToken() throws Exception { - when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)) - .thenReturn(mFuture); - when(mFuture.getResult()).thenThrow(new AuthenticatorException("sadness!")); - mAuthenticator.getAuthToken(); - } - - @Test(expected = AuthFailureError.class) - public void resultContainsIntent() throws Exception { - Intent intent = new Intent(); - Bundle bundle = new Bundle(); - bundle.putParcelable(AccountManager.KEY_INTENT, intent); - when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)) - .thenReturn(mFuture); - when(mFuture.getResult()).thenReturn(bundle); - when(mFuture.isDone()).thenReturn(true); - when(mFuture.isCancelled()).thenReturn(false); - mAuthenticator.getAuthToken(); - } - - @Test(expected = AuthFailureError.class) - public void missingAuthToken() throws Exception { - Bundle bundle = new Bundle(); - when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)) - .thenReturn(mFuture); - when(mFuture.getResult()).thenReturn(bundle); - when(mFuture.isDone()).thenReturn(true); - when(mFuture.isCancelled()).thenReturn(false); - mAuthenticator.getAuthToken(); - } - - @Test - public void invalidateAuthToken() throws Exception { - mAuthenticator.invalidateAuthToken("monkey"); - verify(mAccountManager).invalidateAuthToken("cooltype", "monkey"); - } - - @Test - public void goodToken() throws Exception { - Bundle bundle = new Bundle(); - bundle.putString(AccountManager.KEY_AUTHTOKEN, "monkey"); - when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)) - .thenReturn(mFuture); - when(mFuture.getResult()).thenReturn(bundle); - when(mFuture.isDone()).thenReturn(true); - when(mFuture.isCancelled()).thenReturn(false); - Assert.assertEquals("monkey", mAuthenticator.getAuthToken()); - } - - @Test - public void publicMethods() throws Exception { - // Catch-all test to find API-breaking changes. - Context context = mock(Context.class); - new AndroidAuthenticator(context, mAccount, "cooltype"); - new AndroidAuthenticator(context, mAccount, "cooltype", true); - Assert.assertSame(mAccount, mAuthenticator.getAccount()); - } -} diff --git a/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java b/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java deleted file mode 100644 index 1049ad0..0000000 --- a/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.android.volley.toolbox; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; - -import com.android.volley.AuthFailureError; -import com.android.volley.Header; -import com.android.volley.Request; -import com.android.volley.mock.TestRequest; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class BaseHttpStackTest { - private static final Request REQUEST = new TestRequest.Get(); - private static final Map ADDITIONAL_HEADERS = Collections.emptyMap(); - - @Mock private InputStream mContent; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void legacyRequestWithoutBody() throws Exception { - BaseHttpStack stack = - new BaseHttpStack() { - @Override - public HttpResponse executeRequest( - Request request, Map additionalHeaders) - throws IOException, AuthFailureError { - assertSame(REQUEST, request); - assertSame(ADDITIONAL_HEADERS, additionalHeaders); - return new HttpResponse(12345, Collections.
emptyList()); - } - }; - org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS); - assertEquals(12345, resp.getStatusLine().getStatusCode()); - assertEquals(0, resp.getAllHeaders().length); - assertNull(resp.getEntity()); - } - - @Test - public void legacyResponseWithBody() throws Exception { - BaseHttpStack stack = - new BaseHttpStack() { - @Override - public HttpResponse executeRequest( - Request request, Map additionalHeaders) - throws IOException, AuthFailureError { - assertSame(REQUEST, request); - assertSame(ADDITIONAL_HEADERS, additionalHeaders); - return new HttpResponse( - 12345, Collections.
emptyList(), 555, mContent); - } - }; - org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS); - assertEquals(12345, resp.getStatusLine().getStatusCode()); - assertEquals(0, resp.getAllHeaders().length); - assertEquals(555L, resp.getEntity().getContentLength()); - assertSame(mContent, resp.getEntity().getContent()); - } - - @Test - public void legacyResponseHeaders() throws Exception { - BaseHttpStack stack = - new BaseHttpStack() { - @Override - public HttpResponse executeRequest( - Request request, Map additionalHeaders) - throws IOException, AuthFailureError { - assertSame(REQUEST, request); - assertSame(ADDITIONAL_HEADERS, additionalHeaders); - List
headers = new ArrayList<>(); - headers.add(new Header("HeaderA", "ValueA")); - headers.add(new Header("HeaderB", "ValueB_1")); - headers.add(new Header("HeaderB", "ValueB_2")); - return new HttpResponse(12345, headers); - } - }; - org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS); - assertEquals(12345, resp.getStatusLine().getStatusCode()); - assertEquals(3, resp.getAllHeaders().length); - assertEquals("HeaderA", resp.getAllHeaders()[0].getName()); - assertEquals("ValueA", resp.getAllHeaders()[0].getValue()); - assertEquals("HeaderB", resp.getAllHeaders()[1].getName()); - assertEquals("ValueB_1", resp.getAllHeaders()[1].getValue()); - assertEquals("HeaderB", resp.getAllHeaders()[2].getName()); - assertEquals("ValueB_2", resp.getAllHeaders()[2].getValue()); - assertNull(resp.getEntity()); - } -} diff --git a/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java b/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java deleted file mode 100644 index 91d4062..0000000 --- a/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java +++ /dev/null @@ -1,508 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.*; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.mockito.MockitoAnnotations.initMocks; - -import com.android.volley.AsyncNetwork; -import com.android.volley.AuthFailureError; -import com.android.volley.Cache.Entry; -import com.android.volley.Header; -import com.android.volley.NetworkResponse; -import com.android.volley.NoConnectionError; -import com.android.volley.Request; -import com.android.volley.Response; -import com.android.volley.RetryPolicy; -import com.android.volley.ServerError; -import com.android.volley.TimeoutError; -import com.android.volley.VolleyError; -import com.android.volley.mock.MockAsyncStack; -import com.google.common.util.concurrent.MoreExecutors; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.SocketTimeoutException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class BasicAsyncNetworkTest { - - @Mock private RetryPolicy mMockRetryPolicy; - @Mock private AsyncNetwork.OnRequestComplete mockCallback; - private ExecutorService executor = MoreExecutors.newDirectExecutorService(); - - @Before - public void setUp() throws Exception { - initMocks(this); - } - - @Test - public void headersAndPostParams() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - HttpResponse fakeResponse = - new HttpResponse( - 200, - Collections.
emptyList(), - "foobar".getBytes(StandardCharsets.UTF_8)); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - Entry entry = new Entry(); - entry.etag = "foobar"; - entry.lastModified = 1503102002000L; - request.setCacheEntry(entry); - perform(request, httpNetwork).get(); - assertEquals("foo", mockAsyncStack.getLastHeaders().get("requestheader")); - assertEquals("foobar", mockAsyncStack.getLastHeaders().get("If-None-Match")); - assertEquals( - "Sat, 19 Aug 2017 00:20:02 GMT", - mockAsyncStack.getLastHeaders().get("If-Modified-Since")); - assertEquals( - "requestpost=foo&", - new String(mockAsyncStack.getLastPostBody(), StandardCharsets.UTF_8)); - } - - @Test - public void headersAndPostParamsStream() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - ByteArrayInputStream stream = new ByteArrayInputStream("foobar".getBytes("UTF-8")); - HttpResponse fakeResponse = - new HttpResponse(200, Collections.
emptyList(), 6, stream); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - Entry entry = new Entry(); - entry.etag = "foobar"; - entry.lastModified = 1503102002000L; - request.setCacheEntry(entry); - perform(request, httpNetwork).get(); - assertEquals("foo", mockAsyncStack.getLastHeaders().get("requestheader")); - assertEquals("foobar", mockAsyncStack.getLastHeaders().get("If-None-Match")); - assertEquals( - "Sat, 19 Aug 2017 00:20:02 GMT", - mockAsyncStack.getLastHeaders().get("If-Modified-Since")); - assertEquals( - "requestpost=foo&", - new String(mockAsyncStack.getLastPostBody(), StandardCharsets.UTF_8)); - } - - @Test - public void notModified() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - List
headers = new ArrayList<>(); - headers.add(new Header("ServerKeyA", "ServerValueA")); - headers.add(new Header("ServerKeyB", "ServerValueB")); - headers.add(new Header("SharedKey", "ServerValueShared")); - headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); - headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); - HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - Entry entry = new Entry(); - entry.allResponseHeaders = new ArrayList<>(); - entry.allResponseHeaders.add(new Header("CachedKeyA", "CachedValueA")); - entry.allResponseHeaders.add(new Header("CachedKeyB", "CachedValueB")); - entry.allResponseHeaders.add(new Header("SharedKey", "CachedValueShared")); - entry.allResponseHeaders.add(new Header("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1")); - entry.allResponseHeaders.add(new Header("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2")); - request.setCacheEntry(entry); - httpNetwork.performRequest(request, mockCallback); - NetworkResponse response = perform(request, httpNetwork).get(); - List
expectedHeaders = new ArrayList<>(); - // Should have all server headers + cache headers that didn't show up in server response. - expectedHeaders.add(new Header("ServerKeyA", "ServerValueA")); - expectedHeaders.add(new Header("ServerKeyB", "ServerValueB")); - expectedHeaders.add(new Header("SharedKey", "ServerValueShared")); - expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); - expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); - expectedHeaders.add(new Header("CachedKeyA", "CachedValueA")); - expectedHeaders.add(new Header("CachedKeyB", "CachedValueB")); - assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0]))); - } - - @Test - public void notModified_legacyCache() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - List
headers = new ArrayList<>(); - headers.add(new Header("ServerKeyA", "ServerValueA")); - headers.add(new Header("ServerKeyB", "ServerValueB")); - headers.add(new Header("SharedKey", "ServerValueShared")); - headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); - headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); - HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - Entry entry = new Entry(); - entry.responseHeaders = new HashMap<>(); - entry.responseHeaders.put("CachedKeyA", "CachedValueA"); - entry.responseHeaders.put("CachedKeyB", "CachedValueB"); - entry.responseHeaders.put("SharedKey", "CachedValueShared"); - entry.responseHeaders.put("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1"); - entry.responseHeaders.put("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2"); - request.setCacheEntry(entry); - NetworkResponse response = perform(request, httpNetwork).get(); - List
expectedHeaders = new ArrayList<>(); - // Should have all server headers + cache headers that didn't show up in server response. - expectedHeaders.add(new Header("ServerKeyA", "ServerValueA")); - expectedHeaders.add(new Header("ServerKeyB", "ServerValueB")); - expectedHeaders.add(new Header("SharedKey", "ServerValueShared")); - expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); - expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); - expectedHeaders.add(new Header("CachedKeyA", "CachedValueA")); - expectedHeaders.add(new Header("CachedKeyB", "CachedValueB")); - assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0]))); - } - - @Test - public void socketTimeout() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - mockAsyncStack.setExceptionToThrow(new SocketTimeoutException()); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - httpNetwork.performRequest(request, mockCallback); - verify(mockCallback, times(1)).onError(any(VolleyError.class)); - verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); - // should retry socket timeouts - verify(mMockRetryPolicy).retry(any(TimeoutError.class)); - reset(mMockRetryPolicy, mockCallback); - } - - @Test - public void noConnectionDefault() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - mockAsyncStack.setExceptionToThrow(new IOException()); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - httpNetwork.performRequest(request, mockCallback); - verify(mockCallback, times(1)).onError(any(VolleyError.class)); - verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); - // should not retry when there is no connection - verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); - reset(mMockRetryPolicy, mockCallback); - } - - @Test - public void noConnectionRetry() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - mockAsyncStack.setExceptionToThrow(new IOException()); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - request.setShouldRetryConnectionErrors(true); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - httpNetwork.performRequest(request, mockCallback); - verify(mockCallback, times(1)).onError(any(VolleyError.class)); - verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); - // should retry when there is no connection - verify(mMockRetryPolicy).retry(any(NoConnectionError.class)); - reset(mMockRetryPolicy, mockCallback); - } - - @Test - public void noConnectionNoRetry() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - mockAsyncStack.setExceptionToThrow(new IOException()); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - request.setShouldRetryConnectionErrors(false); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - httpNetwork.performRequest(request, mockCallback); - verify(mockCallback, times(1)).onError(any(VolleyError.class)); - verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); - // should not retry when there is no connection - verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); - reset(mMockRetryPolicy, mockCallback); - } - - @Test - public void unauthorized() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - HttpResponse fakeResponse = new HttpResponse(401, Collections.
emptyList()); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - httpNetwork.performRequest(request, mockCallback); - verify(mockCallback, times(1)).onError(any(VolleyError.class)); - verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); - // should retry in case it's an auth failure. - verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); - reset(mMockRetryPolicy, mockCallback); - } - - @Test(expected = RuntimeException.class) - public void malformedUrlRequest() throws VolleyError, ExecutionException, InterruptedException { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - mockAsyncStack.setExceptionToThrow(new MalformedURLException()); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - perform(request, httpNetwork).get(); - } - - @Test - public void forbidden() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - HttpResponse fakeResponse = new HttpResponse(403, Collections.
emptyList()); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - httpNetwork.performRequest(request, mockCallback); - verify(mockCallback, times(1)).onError(any(VolleyError.class)); - verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); - // should retry in case it's an auth failure. - verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); - reset(mMockRetryPolicy, mockCallback); - } - - @Test - public void redirect() throws Exception { - for (int i = 300; i <= 399; i++) { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - httpNetwork.performRequest(request, mockCallback); - if (i != 304) { - verify(mockCallback, times(1)).onError(any(VolleyError.class)); - verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); - } else { - verify(mockCallback, never()).onError(any(VolleyError.class)); - verify(mockCallback, times(1)).onSuccess(any(NetworkResponse.class)); - } - // should not retry 300 responses. - verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); - reset(mMockRetryPolicy, mockCallback); - } - } - - @Test - public void otherClientError() throws Exception { - for (int i = 400; i <= 499; i++) { - if (i == 401 || i == 403) { - // covered above. - continue; - } - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - httpNetwork.performRequest(request, mockCallback); - verify(mockCallback, times(1)).onError(any(VolleyError.class)); - verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); - // should not retry other 400 errors. - verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); - reset(mMockRetryPolicy, mockCallback); - } - } - - @Test - public void serverError_enableRetries() throws Exception { - for (int i = 500; i <= 599; i++) { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = - new BasicAsyncNetwork.Builder(mockAsyncStack) - .setPool(new ByteArrayPool(4096)) - .build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - request.setShouldRetryServerErrors(true); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - httpNetwork.performRequest(request, mockCallback); - verify(mockCallback, times(1)).onError(any(VolleyError.class)); - verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); - // should retry all 500 errors - verify(mMockRetryPolicy).retry(any(ServerError.class)); - reset(mMockRetryPolicy, mockCallback); - } - } - - @Test - public void serverError_disableRetries() throws Exception { - for (int i = 500; i <= 599; i++) { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - httpNetwork.performRequest(request, mockCallback); - verify(mockCallback, times(1)).onError(any(VolleyError.class)); - verify(mockCallback, never()).onSuccess(any(NetworkResponse.class)); - // should not retry any 500 error w/ HTTP 500 retries turned off (the default). - verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); - reset(mMockRetryPolicy, mockCallback); - } - } - - @Test - public void notModifiedShortCircuit() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - List
headers = new ArrayList<>(); - headers.add(new Header("ServerKeyA", "ServerValueA")); - headers.add(new Header("ServerKeyB", "ServerValueB")); - headers.add(new Header("SharedKey", "ServerValueShared")); - headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); - headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); - HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - httpNetwork.performRequest(request, mockCallback); - verify(mockCallback, times(1)).onSuccess(any(NetworkResponse.class)); - verify(mockCallback, never()).onError(any(VolleyError.class)); - reset(mMockRetryPolicy, mockCallback); - } - - @Test - public void performRequestSuccess() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - HttpResponse fakeResponse = - new HttpResponse( - 200, - Collections.
emptyList(), - "foobar".getBytes(StandardCharsets.UTF_8)); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - httpNetwork.setBlockingExecutor(executor); - Request request = buildRequest(); - Entry entry = new Entry(); - entry.etag = "foobar"; - entry.lastModified = 1503102002000L; - request.setCacheEntry(entry); - httpNetwork.performRequest(request, mockCallback); - verify(mockCallback, times(1)).onSuccess(any(NetworkResponse.class)); - verify(mockCallback, never()).onError(any(VolleyError.class)); - reset(mMockRetryPolicy, mockCallback); - } - - @Test(expected = IllegalStateException.class) - public void performRequestNeverSetExecutorTest() throws Exception { - MockAsyncStack mockAsyncStack = new MockAsyncStack(); - HttpResponse fakeResponse = new HttpResponse(200, Collections.
emptyList()); - mockAsyncStack.setResponseToReturn(fakeResponse); - BasicAsyncNetwork httpNetwork = new BasicAsyncNetwork.Builder(mockAsyncStack).build(); - Request request = buildRequest(); - perform(request, httpNetwork).get(); - } - - /** Helper functions */ - private CompletableFuture perform(Request request, AsyncNetwork network) - throws VolleyError { - final CompletableFuture future = new CompletableFuture<>(); - network.performRequest( - request, - new AsyncNetwork.OnRequestComplete() { - @Override - public void onSuccess(NetworkResponse networkResponse) { - future.complete(networkResponse); - } - - @Override - public void onError(VolleyError volleyError) { - future.complete(null); - } - }); - return future; - } - - private static Request buildRequest() { - return new Request(Request.Method.GET, "http://foo", null) { - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - return null; - } - - @Override - protected void deliverResponse(String response) {} - - @Override - public Map getHeaders() { - Map result = new HashMap(); - result.put("requestheader", "foo"); - return result; - } - - @Override - public Map getParams() { - Map result = new HashMap(); - result.put("requestpost", "foo"); - return result; - } - }; - } -} diff --git a/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java b/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java deleted file mode 100644 index 3630379..0000000 --- a/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java +++ /dev/null @@ -1,384 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.mockito.MockitoAnnotations.initMocks; - -import com.android.volley.AuthFailureError; -import com.android.volley.Cache.Entry; -import com.android.volley.Header; -import com.android.volley.NetworkResponse; -import com.android.volley.NoConnectionError; -import com.android.volley.Request; -import com.android.volley.Response; -import com.android.volley.RetryPolicy; -import com.android.volley.ServerError; -import com.android.volley.TimeoutError; -import com.android.volley.VolleyError; -import com.android.volley.mock.MockHttpStack; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class BasicNetworkTest { - - @Mock private Request mMockRequest; - @Mock private RetryPolicy mMockRetryPolicy; - - @Before - public void setUp() throws Exception { - initMocks(this); - } - - @Test - public void headersAndPostParams() throws Exception { - MockHttpStack mockHttpStack = new MockHttpStack(); - InputStream responseStream = - new ByteArrayInputStream("foobar".getBytes(StandardCharsets.UTF_8)); - HttpResponse fakeResponse = - new HttpResponse(200, Collections.
emptyList(), 6, responseStream); - mockHttpStack.setResponseToReturn(fakeResponse); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - Entry entry = new Entry(); - entry.etag = "foobar"; - entry.lastModified = 1503102002000L; - request.setCacheEntry(entry); - httpNetwork.performRequest(request); - assertEquals("foo", mockHttpStack.getLastHeaders().get("requestheader")); - assertEquals("foobar", mockHttpStack.getLastHeaders().get("If-None-Match")); - assertEquals( - "Sat, 19 Aug 2017 00:20:02 GMT", - mockHttpStack.getLastHeaders().get("If-Modified-Since")); - assertEquals( - "requestpost=foo&", - new String(mockHttpStack.getLastPostBody(), StandardCharsets.UTF_8)); - } - - @Test - public void notModified() throws Exception { - MockHttpStack mockHttpStack = new MockHttpStack(); - List
headers = new ArrayList<>(); - headers.add(new Header("ServerKeyA", "ServerValueA")); - headers.add(new Header("ServerKeyB", "ServerValueB")); - headers.add(new Header("SharedKey", "ServerValueShared")); - headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); - headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); - HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); - mockHttpStack.setResponseToReturn(fakeResponse); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - Entry entry = new Entry(); - entry.allResponseHeaders = new ArrayList<>(); - entry.allResponseHeaders.add(new Header("CachedKeyA", "CachedValueA")); - entry.allResponseHeaders.add(new Header("CachedKeyB", "CachedValueB")); - entry.allResponseHeaders.add(new Header("SharedKey", "CachedValueShared")); - entry.allResponseHeaders.add(new Header("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1")); - entry.allResponseHeaders.add(new Header("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2")); - request.setCacheEntry(entry); - NetworkResponse response = httpNetwork.performRequest(request); - List
expectedHeaders = new ArrayList<>(); - // Should have all server headers + cache headers that didn't show up in server response. - expectedHeaders.add(new Header("ServerKeyA", "ServerValueA")); - expectedHeaders.add(new Header("ServerKeyB", "ServerValueB")); - expectedHeaders.add(new Header("SharedKey", "ServerValueShared")); - expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); - expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); - expectedHeaders.add(new Header("CachedKeyA", "CachedValueA")); - expectedHeaders.add(new Header("CachedKeyB", "CachedValueB")); - assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0]))); - } - - @Test - public void notModified_legacyCache() throws Exception { - MockHttpStack mockHttpStack = new MockHttpStack(); - List
headers = new ArrayList<>(); - headers.add(new Header("ServerKeyA", "ServerValueA")); - headers.add(new Header("ServerKeyB", "ServerValueB")); - headers.add(new Header("SharedKey", "ServerValueShared")); - headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); - headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); - HttpResponse fakeResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers); - mockHttpStack.setResponseToReturn(fakeResponse); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - Entry entry = new Entry(); - entry.responseHeaders = new HashMap<>(); - entry.responseHeaders.put("CachedKeyA", "CachedValueA"); - entry.responseHeaders.put("CachedKeyB", "CachedValueB"); - entry.responseHeaders.put("SharedKey", "CachedValueShared"); - entry.responseHeaders.put("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1"); - entry.responseHeaders.put("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2"); - request.setCacheEntry(entry); - NetworkResponse response = httpNetwork.performRequest(request); - List
expectedHeaders = new ArrayList<>(); - // Should have all server headers + cache headers that didn't show up in server response. - expectedHeaders.add(new Header("ServerKeyA", "ServerValueA")); - expectedHeaders.add(new Header("ServerKeyB", "ServerValueB")); - expectedHeaders.add(new Header("SharedKey", "ServerValueShared")); - expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1")); - expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2")); - expectedHeaders.add(new Header("CachedKeyA", "CachedValueA")); - expectedHeaders.add(new Header("CachedKeyB", "CachedValueB")); - assertThat(expectedHeaders, containsInAnyOrder(response.allHeaders.toArray(new Header[0]))); - } - - @Test - public void socketTimeout() throws Exception { - MockHttpStack mockHttpStack = new MockHttpStack(); - mockHttpStack.setExceptionToThrow(new SocketTimeoutException()); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - try { - httpNetwork.performRequest(request); - } catch (VolleyError e) { - // expected - } - // should retry socket timeouts - verify(mMockRetryPolicy).retry(any(TimeoutError.class)); - } - - @Test - public void noConnectionDefault() throws Exception { - MockHttpStack mockHttpStack = new MockHttpStack(); - mockHttpStack.setExceptionToThrow(new IOException()); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - try { - httpNetwork.performRequest(request); - } catch (VolleyError e) { - // expected - } - // should not retry when there is no connection - verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); - } - - @Test - public void noConnectionRetry() throws Exception { - MockHttpStack mockHttpStack = new MockHttpStack(); - mockHttpStack.setExceptionToThrow(new IOException()); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - request.setShouldRetryConnectionErrors(true); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - try { - httpNetwork.performRequest(request); - } catch (VolleyError e) { - // expected - } - // should retry when there is no connection - verify(mMockRetryPolicy).retry(any(NoConnectionError.class)); - reset(mMockRetryPolicy); - } - - @Test - public void noConnectionNoRetry() throws Exception { - MockHttpStack mockHttpStack = new MockHttpStack(); - mockHttpStack.setExceptionToThrow(new IOException()); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - request.setShouldRetryConnectionErrors(false); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - try { - httpNetwork.performRequest(request); - } catch (VolleyError e) { - // expected - } - // should not retry when there is no connection - verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); - } - - @Test - public void unauthorized() throws Exception { - MockHttpStack mockHttpStack = new MockHttpStack(); - HttpResponse fakeResponse = new HttpResponse(401, Collections.
emptyList()); - mockHttpStack.setResponseToReturn(fakeResponse); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - try { - httpNetwork.performRequest(request); - } catch (VolleyError e) { - // expected - } - // should retry in case it's an auth failure. - verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); - } - - @Test - public void forbidden() throws Exception { - MockHttpStack mockHttpStack = new MockHttpStack(); - HttpResponse fakeResponse = new HttpResponse(403, Collections.
emptyList()); - mockHttpStack.setResponseToReturn(fakeResponse); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - try { - httpNetwork.performRequest(request); - } catch (VolleyError e) { - // expected - } - // should retry in case it's an auth failure. - verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); - } - - @Test - public void redirect() throws Exception { - for (int i = 300; i <= 399; i++) { - MockHttpStack mockHttpStack = new MockHttpStack(); - HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); - mockHttpStack.setResponseToReturn(fakeResponse); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - try { - httpNetwork.performRequest(request); - } catch (VolleyError e) { - // expected - } - // should not retry 300 responses. - verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); - reset(mMockRetryPolicy); - } - } - - @Test - public void otherClientError() throws Exception { - for (int i = 400; i <= 499; i++) { - if (i == 401 || i == 403) { - // covered above. - continue; - } - MockHttpStack mockHttpStack = new MockHttpStack(); - HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); - mockHttpStack.setResponseToReturn(fakeResponse); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - try { - httpNetwork.performRequest(request); - } catch (VolleyError e) { - // expected - } - // should not retry other 400 errors. - verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); - reset(mMockRetryPolicy); - } - } - - @Test - public void serverError_enableRetries() throws Exception { - for (int i = 500; i <= 599; i++) { - MockHttpStack mockHttpStack = new MockHttpStack(); - HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); - mockHttpStack.setResponseToReturn(fakeResponse); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack, new ByteArrayPool(4096)); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - request.setShouldRetryServerErrors(true); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - try { - httpNetwork.performRequest(request); - } catch (VolleyError e) { - // expected - } - // should retry all 500 errors - verify(mMockRetryPolicy).retry(any(ServerError.class)); - reset(mMockRetryPolicy); - } - } - - @Test - public void serverError_disableRetries() throws Exception { - for (int i = 500; i <= 599; i++) { - MockHttpStack mockHttpStack = new MockHttpStack(); - HttpResponse fakeResponse = new HttpResponse(i, Collections.
emptyList()); - mockHttpStack.setResponseToReturn(fakeResponse); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - try { - httpNetwork.performRequest(request); - } catch (VolleyError e) { - // expected - } - // should not retry any 500 error w/ HTTP 500 retries turned off (the default). - verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); - reset(mMockRetryPolicy); - } - } - - private static Request buildRequest() { - return new Request(Request.Method.GET, "http://foo", null) { - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - return null; - } - - @Override - protected void deliverResponse(String response) {} - - @Override - public Map getHeaders() { - Map result = new HashMap(); - result.put("requestheader", "foo"); - return result; - } - - @Override - public Map getParams() { - Map result = new HashMap(); - result.put("requestpost", "foo"); - return result; - } - }; - } -} diff --git a/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java b/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java deleted file mode 100644 index 62da207..0000000 --- a/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; - -public class ByteArrayPoolTest { - @Test - public void reusesBuffer() { - ByteArrayPool pool = new ByteArrayPool(32); - - byte[] buf1 = pool.getBuf(16); - byte[] buf2 = pool.getBuf(16); - - pool.returnBuf(buf1); - pool.returnBuf(buf2); - - byte[] buf3 = pool.getBuf(16); - byte[] buf4 = pool.getBuf(16); - assertTrue(buf3 == buf1 || buf3 == buf2); - assertTrue(buf4 == buf1 || buf4 == buf2); - assertTrue(buf3 != buf4); - } - - @Test - public void obeysSizeLimit() { - ByteArrayPool pool = new ByteArrayPool(32); - - byte[] buf1 = pool.getBuf(16); - byte[] buf2 = pool.getBuf(16); - byte[] buf3 = pool.getBuf(16); - - pool.returnBuf(buf1); - pool.returnBuf(buf2); - pool.returnBuf(buf3); - - byte[] buf4 = pool.getBuf(16); - byte[] buf5 = pool.getBuf(16); - byte[] buf6 = pool.getBuf(16); - - assertTrue(buf4 == buf2 || buf4 == buf3); - assertTrue(buf5 == buf2 || buf5 == buf3); - assertTrue(buf4 != buf5); - assertTrue(buf6 != buf1 && buf6 != buf2 && buf6 != buf3); - } - - @Test - public void returnsBufferWithRightSize() { - ByteArrayPool pool = new ByteArrayPool(32); - - byte[] buf1 = pool.getBuf(16); - pool.returnBuf(buf1); - - byte[] buf2 = pool.getBuf(17); - assertNotSame(buf2, buf1); - - byte[] buf3 = pool.getBuf(15); - assertSame(buf3, buf1); - } -} diff --git a/src/test/java/com/android/volley/toolbox/CacheTest.java b/src/test/java/com/android/volley/toolbox/CacheTest.java deleted file mode 100644 index 22dae22..0000000 --- a/src/test/java/com/android/volley/toolbox/CacheTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertNotNull; - -import com.android.volley.Cache; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class CacheTest { - - @Test - public void publicMethods() throws Exception { - // Catch-all test to find API-breaking changes. - assertNotNull(Cache.class.getMethod("get", String.class)); - assertNotNull(Cache.class.getMethod("put", String.class, Cache.Entry.class)); - assertNotNull(Cache.class.getMethod("initialize")); - assertNotNull(Cache.class.getMethod("invalidate", String.class, boolean.class)); - assertNotNull(Cache.class.getMethod("remove", String.class)); - assertNotNull(Cache.class.getMethod("clear")); - } -} diff --git a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java deleted file mode 100644 index db6e491..0000000 --- a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java +++ /dev/null @@ -1,646 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.hamcrest.Matchers.arrayWithSize; -import static org.hamcrest.Matchers.emptyArray; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -import com.android.volley.Cache; -import com.android.volley.Header; -import com.android.volley.toolbox.DiskBasedCache.CacheHeader; -import com.android.volley.toolbox.DiskBasedCache.CountingInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.EOFException; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Random; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -@RunWith(RobolectricTestRunner.class) -@Config(sdk = 16) -public class DiskBasedCacheTest { - - private static final int MAX_SIZE = 1024 * 1024; - - private Cache cache; - - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Rule public ExpectedException exception = ExpectedException.none(); - - @Before - public void setup() throws IOException { - // Initialize empty cache - cache = new DiskBasedCache(temporaryFolder.getRoot(), MAX_SIZE); - cache.initialize(); - } - - @After - public void teardown() { - cache = null; - } - - @Test - public void testEmptyInitialize() { - assertThat(cache.get("key"), is(nullValue())); - } - - @Test - public void testPutGetZeroBytes() { - Cache.Entry entry = new Cache.Entry(); - entry.data = new byte[0]; - entry.serverDate = 1234567L; - entry.lastModified = 13572468L; - entry.ttl = 9876543L; - entry.softTtl = 8765432L; - entry.etag = "etag"; - entry.responseHeaders = new HashMap<>(); - entry.responseHeaders.put("fruit", "banana"); - entry.responseHeaders.put("color", "yellow"); - cache.put("my-magical-key", entry); - - assertThatEntriesAreEqual(cache.get("my-magical-key"), entry); - assertThat(cache.get("unknown-key"), is(nullValue())); - } - - @Test - public void testPutRemoveGet() { - Cache.Entry entry = randomData(511); - cache.put("key", entry); - - assertThatEntriesAreEqual(cache.get("key"), entry); - - cache.remove("key"); - assertThat(cache.get("key"), is(nullValue())); - assertThat(listCachedFiles(), is(emptyArray())); - } - - @Test - public void testPutClearGet() { - Cache.Entry entry = randomData(511); - cache.put("key", entry); - - assertThatEntriesAreEqual(cache.get("key"), entry); - - cache.clear(); - assertThat(cache.get("key"), is(nullValue())); - assertThat(listCachedFiles(), is(emptyArray())); - } - - @Test - public void testReinitialize() { - Cache.Entry entry = randomData(1023); - cache.put("key", entry); - - Cache copy = new DiskBasedCache(temporaryFolder.getRoot(), MAX_SIZE); - copy.initialize(); - - assertThatEntriesAreEqual(copy.get("key"), entry); - } - - @Test - public void testInvalidate() { - Cache.Entry entry = randomData(32); - entry.softTtl = 8765432L; - entry.ttl = 9876543L; - cache.put("key", entry); - - cache.invalidate("key", false); - entry.softTtl = 0; // expired - assertThatEntriesAreEqual(cache.get("key"), entry); - } - - @Test - public void testInvalidateFullExpire() { - Cache.Entry entry = randomData(32); - entry.softTtl = 8765432L; - entry.ttl = 9876543L; - cache.put("key", entry); - - cache.invalidate("key", true); - entry.softTtl = 0; // expired - entry.ttl = 0; // expired - assertThatEntriesAreEqual(cache.get("key"), entry); - } - - @Test - public void testTooLargeEntry() { - Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("oversize")); - cache.put("oversize", entry); - - assertThat(cache.get("oversize"), is(nullValue())); - } - - @Test - public void testMaxSizeEntry() { - Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("maxsize") - 1); - cache.put("maxsize", entry); - - assertThatEntriesAreEqual(cache.get("maxsize"), entry); - } - - @Test - public void testTrimAtThreshold() { - // Start with the largest possible entry. - Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("maxsize") - 1); - cache.put("maxsize", entry); - - assertThatEntriesAreEqual(cache.get("maxsize"), entry); - - // Now any new entry should cause the first one to be cleared. - entry = randomData(0); - cache.put("bit", entry); - - assertThat(cache.get("goodsize"), is(nullValue())); - assertThatEntriesAreEqual(cache.get("bit"), entry); - } - - @Test - public void testTrimWithMultipleEvictions_underHysteresisThreshold() { - Cache.Entry entry1 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry1") - 1); - cache.put("entry1", entry1); - Cache.Entry entry2 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry2") - 1); - cache.put("entry2", entry2); - Cache.Entry entry3 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry3") - 1); - cache.put("entry3", entry3); - - assertThatEntriesAreEqual(cache.get("entry1"), entry1); - assertThatEntriesAreEqual(cache.get("entry2"), entry2); - assertThatEntriesAreEqual(cache.get("entry3"), entry3); - - Cache.Entry entry = - randomData( - (int) (DiskBasedCache.HYSTERESIS_FACTOR * MAX_SIZE) - - getEntrySizeOnDisk("max")); - cache.put("max", entry); - - assertThat(cache.get("entry1"), is(nullValue())); - assertThat(cache.get("entry2"), is(nullValue())); - assertThat(cache.get("entry3"), is(nullValue())); - assertThatEntriesAreEqual(cache.get("max"), entry); - } - - @Test - public void testTrimWithMultipleEvictions_atHysteresisThreshold() { - Cache.Entry entry1 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry1") - 1); - cache.put("entry1", entry1); - Cache.Entry entry2 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry2") - 1); - cache.put("entry2", entry2); - Cache.Entry entry3 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry3") - 1); - cache.put("entry3", entry3); - - assertThatEntriesAreEqual(cache.get("entry1"), entry1); - assertThatEntriesAreEqual(cache.get("entry2"), entry2); - assertThatEntriesAreEqual(cache.get("entry3"), entry3); - - Cache.Entry entry = - randomData( - (int) (DiskBasedCache.HYSTERESIS_FACTOR * MAX_SIZE) - - getEntrySizeOnDisk("max") - + 1); - cache.put("max", entry); - - assertThat(cache.get("entry1"), is(nullValue())); - assertThat(cache.get("entry2"), is(nullValue())); - assertThat(cache.get("entry3"), is(nullValue())); - assertThat(cache.get("max"), is(nullValue())); - } - - @Test - public void testTrimWithPartialEvictions() { - Cache.Entry entry1 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry1") - 1); - cache.put("entry1", entry1); - Cache.Entry entry2 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry2") - 1); - cache.put("entry2", entry2); - Cache.Entry entry3 = randomData(MAX_SIZE / 3 - getEntrySizeOnDisk("entry3") - 1); - cache.put("entry3", entry3); - - assertThatEntriesAreEqual(cache.get("entry1"), entry1); - assertThatEntriesAreEqual(cache.get("entry2"), entry2); - assertThatEntriesAreEqual(cache.get("entry3"), entry3); - - Cache.Entry entry4 = randomData((MAX_SIZE - getEntrySizeOnDisk("entry4") - 1) / 2); - cache.put("entry4", entry4); - - assertThat(cache.get("entry1"), is(nullValue())); - assertThat(cache.get("entry2"), is(nullValue())); - assertThatEntriesAreEqual(cache.get("entry3"), entry3); - assertThatEntriesAreEqual(cache.get("entry4"), entry4); - } - - @Test - public void testLargeEntryDoesntClearCache() { - // Writing a large entry to an empty cache should succeed - Cache.Entry largeEntry = randomData(MAX_SIZE - getEntrySizeOnDisk("largeEntry") - 1); - cache.put("largeEntry", largeEntry); - - assertThatEntriesAreEqual(cache.get("largeEntry"), largeEntry); - - // Reset and fill up ~half the cache. - cache.clear(); - Cache.Entry entry = randomData(MAX_SIZE / 2 - getEntrySizeOnDisk("entry") - 1); - cache.put("entry", entry); - - assertThatEntriesAreEqual(cache.get("entry"), entry); - - // Writing the large entry should no-op, because otherwise the pruning algorithm would clear - // the whole cache, since the large entry is above the hysteresis threshold. - cache.put("largeEntry", largeEntry); - - assertThat(cache.get("largeEntry"), is(nullValue())); - assertThatEntriesAreEqual(cache.get("entry"), entry); - } - - @Test - @SuppressWarnings("TryFinallyCanBeTryWithResources") - public void testGetBadMagic() throws IOException { - // Cache something - Cache.Entry entry = randomData(1023); - cache.put("key", entry); - assertThatEntriesAreEqual(cache.get("key"), entry); - - // Overwrite the magic header - File cacheFolder = temporaryFolder.getRoot(); - File file = cacheFolder.listFiles()[0]; - FileOutputStream fos = new FileOutputStream(file); - try { - DiskBasedCache.writeInt(fos, 0); // overwrite magic - } finally { - //noinspection ThrowFromFinallyBlock - fos.close(); - } - - assertThat(cache.get("key"), is(nullValue())); - assertThat(listCachedFiles(), is(emptyArray())); - } - - @Test - @SuppressWarnings("TryFinallyCanBeTryWithResources") - public void testGetWrongKey() throws IOException { - // Cache something - Cache.Entry entry = randomData(1023); - cache.put("key", entry); - assertThatEntriesAreEqual(cache.get("key"), entry); - - // Access the cached file - File cacheFolder = temporaryFolder.getRoot(); - File file = cacheFolder.listFiles()[0]; - FileOutputStream fos = new FileOutputStream(file); - try { - // Overwrite with a different key - CacheHeader wrongHeader = new CacheHeader("bad", entry); - wrongHeader.writeHeader(fos); - } finally { - //noinspection ThrowFromFinallyBlock - fos.close(); - } - - // key is gone, but file is still there - assertThat(cache.get("key"), is(nullValue())); - assertThat(listCachedFiles(), is(arrayWithSize(1))); - - // Note: file is now a zombie because its key does not map to its name - } - - @Test - public void testStreamToBytesNegativeLength() throws IOException { - byte[] data = new byte[1]; - CountingInputStream cis = - new CountingInputStream(new ByteArrayInputStream(data), data.length); - exception.expect(IOException.class); - DiskBasedCache.streamToBytes(cis, -1); - } - - @Test - public void testStreamToBytesExcessiveLength() throws IOException { - byte[] data = new byte[1]; - CountingInputStream cis = - new CountingInputStream(new ByteArrayInputStream(data), data.length); - exception.expect(IOException.class); - DiskBasedCache.streamToBytes(cis, 2); - } - - @Test - public void testStreamToBytesOverflow() throws IOException { - byte[] data = new byte[0]; - CountingInputStream cis = - new CountingInputStream(new ByteArrayInputStream(data), 0x100000000L); - exception.expect(IOException.class); - DiskBasedCache.streamToBytes(cis, 0x100000000L); // int value is 0 - } - - @Test - public void testReadHeaderListWithNegativeSize() throws IOException { - // If a cached header list is corrupted and begins with a negative size, - // verify that readHeaderList will throw an IOException. - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DiskBasedCache.writeInt(baos, -1); // negative size - CountingInputStream cis = - new CountingInputStream( - new ByteArrayInputStream(baos.toByteArray()), Integer.MAX_VALUE); - // Expect IOException due to negative size - exception.expect(IOException.class); - DiskBasedCache.readHeaderList(cis); - } - - @Test - public void testReadHeaderListWithGinormousSize() throws IOException { - // If a cached header list is corrupted and begins with 2GB size, verify - // that readHeaderList will throw EOFException rather than OutOfMemoryError. - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DiskBasedCache.writeInt(baos, Integer.MAX_VALUE); // 2GB size - CountingInputStream cis = - new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size()); - // Expect EOFException when end of stream is reached - exception.expect(EOFException.class); - DiskBasedCache.readHeaderList(cis); - } - - @Test - public void testFileIsDeletedWhenWriteHeaderFails() throws IOException { - // Create DataOutputStream that throws IOException - OutputStream mockedOutputStream = spy(OutputStream.class); - doThrow(IOException.class).when(mockedOutputStream).write(anyInt()); - - // Create read-only copy that fails to write anything - DiskBasedCache readonly = spy((DiskBasedCache) cache); - doReturn(mockedOutputStream).when(readonly).createOutputStream(any(File.class)); - - // Attempt to write - readonly.put("key", randomData(1111)); - - // write is called at least once because each linked stream flushes when closed - verify(mockedOutputStream, atLeastOnce()).write(anyInt()); - assertThat(readonly.get("key"), is(nullValue())); - assertThat(listCachedFiles(), is(emptyArray())); - - // Note: original cache will try (without success) to read from file - assertThat(cache.get("key"), is(nullValue())); - } - - @Test - public void testIOExceptionInInitialize() throws IOException { - // Cache a few kilobytes - cache.put("kilobyte", randomData(1024)); - cache.put("kilobyte2", randomData(1024)); - cache.put("kilobyte3", randomData(1024)); - - // Create DataInputStream that throws IOException - InputStream mockedInputStream = spy(InputStream.class); - //noinspection ResultOfMethodCallIgnored - doThrow(IOException.class).when(mockedInputStream).read(); - - // Create broken cache that fails to read anything - DiskBasedCache broken = spy(new DiskBasedCache(temporaryFolder.getRoot())); - doReturn(mockedInputStream).when(broken).createInputStream(any(File.class)); - - // Attempt to initialize - broken.initialize(); - - // Everything is gone - assertThat(broken.get("kilobyte"), is(nullValue())); - assertThat(broken.get("kilobyte2"), is(nullValue())); - assertThat(broken.get("kilobyte3"), is(nullValue())); - assertThat(listCachedFiles(), is(emptyArray())); - - // Verify that original cache can cope with missing files - assertThat(cache.get("kilobyte"), is(nullValue())); - assertThat(cache.get("kilobyte2"), is(nullValue())); - assertThat(cache.get("kilobyte3"), is(nullValue())); - } - - @Test - public void testManyResponseHeaders() { - Cache.Entry entry = new Cache.Entry(); - entry.data = new byte[0]; - entry.responseHeaders = new HashMap<>(); - for (int i = 0; i < 0xFFFF; i++) { - entry.responseHeaders.put(Integer.toString(i), ""); - } - cache.put("key", entry); - } - - @Test - @SuppressWarnings("TryFinallyCanBeTryWithResources") - public void testCountingInputStreamByteCount() throws IOException { - // Write some bytes - ByteArrayOutputStream out = new ByteArrayOutputStream(); - //noinspection ThrowFromFinallyBlock - try { - DiskBasedCache.writeInt(out, 1); - DiskBasedCache.writeLong(out, -1L); - DiskBasedCache.writeString(out, "hamburger"); - } finally { - //noinspection ThrowFromFinallyBlock - out.close(); - } - long bytesWritten = out.size(); - - // Read the bytes and compare the counts - CountingInputStream cis = - new CountingInputStream(new ByteArrayInputStream(out.toByteArray()), bytesWritten); - try { - assertThat(cis.bytesRemaining(), is(bytesWritten)); - assertThat(cis.bytesRead(), is(0L)); - assertThat(DiskBasedCache.readInt(cis), is(1)); - assertThat(DiskBasedCache.readLong(cis), is(-1L)); - assertThat(DiskBasedCache.readString(cis), is("hamburger")); - assertThat(cis.bytesRead(), is(bytesWritten)); - assertThat(cis.bytesRemaining(), is(0L)); - } finally { - //noinspection ThrowFromFinallyBlock - cis.close(); - } - } - - /* Serialization tests */ - - @Test - public void testEmptyReadThrowsEOF() throws IOException { - ByteArrayInputStream empty = new ByteArrayInputStream(new byte[] {}); - exception.expect(EOFException.class); - DiskBasedCache.readInt(empty); - } - - @Test - public void serializeInt() throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DiskBasedCache.writeInt(baos, 0); - DiskBasedCache.writeInt(baos, 19791214); - DiskBasedCache.writeInt(baos, -20050711); - DiskBasedCache.writeInt(baos, Integer.MIN_VALUE); - DiskBasedCache.writeInt(baos, Integer.MAX_VALUE); - ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); - assertEquals(DiskBasedCache.readInt(bais), 0); - assertEquals(DiskBasedCache.readInt(bais), 19791214); - assertEquals(DiskBasedCache.readInt(bais), -20050711); - assertEquals(DiskBasedCache.readInt(bais), Integer.MIN_VALUE); - assertEquals(DiskBasedCache.readInt(bais), Integer.MAX_VALUE); - } - - @Test - public void serializeLong() throws Exception { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DiskBasedCache.writeLong(baos, 0); - DiskBasedCache.writeLong(baos, 31337); - DiskBasedCache.writeLong(baos, -4160); - DiskBasedCache.writeLong(baos, 4295032832L); - DiskBasedCache.writeLong(baos, -4314824046L); - DiskBasedCache.writeLong(baos, Long.MIN_VALUE); - DiskBasedCache.writeLong(baos, Long.MAX_VALUE); - ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); - assertEquals(DiskBasedCache.readLong(bais), 0); - assertEquals(DiskBasedCache.readLong(bais), 31337); - assertEquals(DiskBasedCache.readLong(bais), -4160); - assertEquals(DiskBasedCache.readLong(bais), 4295032832L); - assertEquals(DiskBasedCache.readLong(bais), -4314824046L); - assertEquals(DiskBasedCache.readLong(bais), Long.MIN_VALUE); - assertEquals(DiskBasedCache.readLong(bais), Long.MAX_VALUE); - } - - @Test - public void serializeString() throws Exception { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DiskBasedCache.writeString(baos, ""); - DiskBasedCache.writeString(baos, "This is a string."); - DiskBasedCache.writeString(baos, "ファイカス"); - CountingInputStream cis = - new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size()); - assertEquals(DiskBasedCache.readString(cis), ""); - assertEquals(DiskBasedCache.readString(cis), "This is a string."); - assertEquals(DiskBasedCache.readString(cis), "ファイカス"); - } - - @Test - public void serializeHeaders() throws Exception { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - List
empty = new ArrayList<>(); - DiskBasedCache.writeHeaderList(empty, baos); - DiskBasedCache.writeHeaderList(null, baos); - List
twoThings = new ArrayList<>(); - twoThings.add(new Header("first", "thing")); - twoThings.add(new Header("second", "item")); - DiskBasedCache.writeHeaderList(twoThings, baos); - List
emptyKey = new ArrayList<>(); - emptyKey.add(new Header("", "value")); - DiskBasedCache.writeHeaderList(emptyKey, baos); - List
emptyValue = new ArrayList<>(); - emptyValue.add(new Header("key", "")); - DiskBasedCache.writeHeaderList(emptyValue, baos); - List
sameKeys = new ArrayList<>(); - sameKeys.add(new Header("key", "value")); - sameKeys.add(new Header("key", "value2")); - DiskBasedCache.writeHeaderList(sameKeys, baos); - CountingInputStream cis = - new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size()); - assertEquals(DiskBasedCache.readHeaderList(cis), empty); - assertEquals(DiskBasedCache.readHeaderList(cis), empty); // null reads back empty - assertEquals(DiskBasedCache.readHeaderList(cis), twoThings); - assertEquals(DiskBasedCache.readHeaderList(cis), emptyKey); - assertEquals(DiskBasedCache.readHeaderList(cis), emptyValue); - assertEquals(DiskBasedCache.readHeaderList(cis), sameKeys); - } - - @Test - public void publicMethods() throws Exception { - // Catch-all test to find API-breaking changes. - assertNotNull(DiskBasedCache.class.getConstructor(File.class, int.class)); - assertNotNull( - DiskBasedCache.class.getConstructor(DiskBasedCache.FileSupplier.class, int.class)); - assertNotNull(DiskBasedCache.class.getConstructor(File.class)); - assertNotNull(DiskBasedCache.class.getConstructor(DiskBasedCache.FileSupplier.class)); - - assertNotNull(DiskBasedCache.class.getMethod("getFileForKey", String.class)); - } - - @Test - public void initializeIfRootDirectoryDeleted() { - temporaryFolder.delete(); - - Cache.Entry entry = randomData(101); - cache.put("key1", entry); - - assertThat(cache.get("key1"), is(nullValue())); - - // confirm that we can now store entries - cache.put("key2", entry); - assertThatEntriesAreEqual(cache.get("key2"), entry); - } - - /* Test helpers */ - - private void assertThatEntriesAreEqual(Cache.Entry actual, Cache.Entry expected) { - assertThat(actual.data, is(equalTo(expected.data))); - assertThat(actual.etag, is(equalTo(expected.etag))); - assertThat(actual.lastModified, is(equalTo(expected.lastModified))); - assertThat(actual.responseHeaders, is(equalTo(expected.responseHeaders))); - assertThat(actual.serverDate, is(equalTo(expected.serverDate))); - assertThat(actual.softTtl, is(equalTo(expected.softTtl))); - assertThat(actual.ttl, is(equalTo(expected.ttl))); - } - - private Cache.Entry randomData(int length) { - Cache.Entry entry = new Cache.Entry(); - byte[] data = new byte[length]; - new Random(42).nextBytes(data); // explicit seed for reproducible results - entry.data = data; - return entry; - } - - private File[] listCachedFiles() { - return temporaryFolder.getRoot().listFiles(); - } - - private int getEntrySizeOnDisk(String key) { - // Header size is: - // 4 bytes for magic int - // 8 + len(key) bytes for key (long length) - // 8 bytes for etag (long length + 0 characters) - // 32 bytes for serverDate, lastModified, ttl, and softTtl longs - // 4 bytes for length of header list int - // == 56 + len(key) bytes total. - return 56 + key.length(); - } -} diff --git a/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java b/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java deleted file mode 100644 index 2a451dc..0000000 --- a/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import com.android.volley.Request.Method; -import com.android.volley.mock.TestRequest; -import com.android.volley.toolbox.HttpClientStack.HttpPatch; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpHead; -import org.apache.http.client.methods.HttpOptions; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpTrace; -import org.apache.http.client.methods.HttpUriRequest; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class HttpClientStackTest { - - @Test - public void createDeprecatedGetRequest() throws Exception { - TestRequest.DeprecatedGet request = new TestRequest.DeprecatedGet(); - assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpGet); - } - - @Test - public void createDeprecatedPostRequest() throws Exception { - TestRequest.DeprecatedPost request = new TestRequest.DeprecatedPost(); - assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpPost); - } - - @Test - public void createGetRequest() throws Exception { - TestRequest.Get request = new TestRequest.Get(); - assertEquals(request.getMethod(), Method.GET); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpGet); - } - - @Test - public void createPostRequest() throws Exception { - TestRequest.Post request = new TestRequest.Post(); - assertEquals(request.getMethod(), Method.POST); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpPost); - } - - @Test - public void createPostRequestWithBody() throws Exception { - TestRequest.PostWithBody request = new TestRequest.PostWithBody(); - assertEquals(request.getMethod(), Method.POST); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpPost); - } - - @Test - public void createPutRequest() throws Exception { - TestRequest.Put request = new TestRequest.Put(); - assertEquals(request.getMethod(), Method.PUT); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpPut); - } - - @Test - public void createPutRequestWithBody() throws Exception { - TestRequest.PutWithBody request = new TestRequest.PutWithBody(); - assertEquals(request.getMethod(), Method.PUT); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpPut); - } - - @Test - public void createDeleteRequest() throws Exception { - TestRequest.Delete request = new TestRequest.Delete(); - assertEquals(request.getMethod(), Method.DELETE); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpDelete); - } - - @Test - public void createHeadRequest() throws Exception { - TestRequest.Head request = new TestRequest.Head(); - assertEquals(request.getMethod(), Method.HEAD); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpHead); - } - - @Test - public void createOptionsRequest() throws Exception { - TestRequest.Options request = new TestRequest.Options(); - assertEquals(request.getMethod(), Method.OPTIONS); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpOptions); - } - - @Test - public void createTraceRequest() throws Exception { - TestRequest.Trace request = new TestRequest.Trace(); - assertEquals(request.getMethod(), Method.TRACE); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpTrace); - } - - @Test - public void createPatchRequest() throws Exception { - TestRequest.Patch request = new TestRequest.Patch(); - assertEquals(request.getMethod(), Method.PATCH); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpPatch); - } - - @Test - public void createPatchRequestWithBody() throws Exception { - TestRequest.PatchWithBody request = new TestRequest.PatchWithBody(); - assertEquals(request.getMethod(), Method.PATCH); - - HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); - assertTrue(httpRequest instanceof HttpPatch); - } -} diff --git a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java deleted file mode 100644 index 7780c3e..0000000 --- a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import com.android.volley.Cache; -import com.android.volley.Header; -import com.android.volley.NetworkResponse; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class HttpHeaderParserTest { - - private static long ONE_MINUTE_MILLIS = 1000L * 60; - private static long ONE_HOUR_MILLIS = 1000L * 60 * 60; - private static long ONE_DAY_MILLIS = ONE_HOUR_MILLIS * 24; - private static long ONE_WEEK_MILLIS = ONE_DAY_MILLIS * 7; - - private NetworkResponse response; - private Map headers; - - @Before - public void setUp() throws Exception { - headers = new HashMap(); - response = new NetworkResponse(0, null, headers, false); - } - - @Test - public void parseCacheHeaders_noHeaders() { - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - - assertNotNull(entry); - assertNull(entry.etag); - assertEquals(0, entry.serverDate); - assertEquals(0, entry.lastModified); - assertEquals(0, entry.ttl); - assertEquals(0, entry.softTtl); - } - - @Test - public void parseCacheHeaders_nullHeaders() { - response = new NetworkResponse(0, null, null, false); - assertNull(HttpHeaderParser.parseCacheHeaders(response)); - } - - @Test - public void parseCacheHeaders_headersSet() { - headers.put("MyCustomHeader", "42"); - - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - - assertNotNull(entry); - assertNotNull(entry.responseHeaders); - assertEquals(1, entry.responseHeaders.size()); - assertEquals("42", entry.responseHeaders.get("MyCustomHeader")); - } - - @Test - public void parseCacheHeaders_etag() { - headers.put("ETag", "Yow!"); - - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - - assertNotNull(entry); - assertEquals("Yow!", entry.etag); - } - - @Test - public void parseCacheHeaders_normalExpire() { - long now = System.currentTimeMillis(); - headers.put("Date", rfc1123Date(now)); - headers.put("Last-Modified", rfc1123Date(now - ONE_DAY_MILLIS)); - headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); - - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - - assertNotNull(entry); - assertNull(entry.etag); - assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS); - assertEqualsWithin(entry.lastModified, (now - ONE_DAY_MILLIS), ONE_MINUTE_MILLIS); - assertTrue(entry.softTtl >= (now + ONE_HOUR_MILLIS)); - assertTrue(entry.ttl == entry.softTtl); - } - - @Test - public void parseCacheHeaders_expiresInPast() { - long now = System.currentTimeMillis(); - headers.put("Date", rfc1123Date(now)); - headers.put("Expires", rfc1123Date(now - ONE_HOUR_MILLIS)); - - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - - assertNotNull(entry); - assertNull(entry.etag); - assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS); - assertEquals(0, entry.ttl); - assertEquals(0, entry.softTtl); - } - - @Test - public void parseCacheHeaders_serverRelative() { - - long now = System.currentTimeMillis(); - // Set "current" date as one hour in the future - headers.put("Date", rfc1123Date(now + ONE_HOUR_MILLIS)); - // TTL four hours in the future, so should be three hours from now - headers.put("Expires", rfc1123Date(now + 4 * ONE_HOUR_MILLIS)); - - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - - assertEqualsWithin(now + 3 * ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); - assertEquals(entry.softTtl, entry.ttl); - } - - @Test - public void parseCacheHeaders_cacheControlOverridesExpires() { - long now = System.currentTimeMillis(); - headers.put("Date", rfc1123Date(now)); - headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); - headers.put("Cache-Control", "public, max-age=86400"); - - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - - assertNotNull(entry); - assertNull(entry.etag); - assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); - assertEquals(entry.softTtl, entry.ttl); - } - - @Test - public void testParseCacheHeaders_staleWhileRevalidate() { - long now = System.currentTimeMillis(); - headers.put("Date", rfc1123Date(now)); - headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); - - // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day - // - stale-while-revalidate (entry.ttl) indicates that the asset may - // continue to be served stale for up to additional 7 days - headers.put("Cache-Control", "max-age=86400, stale-while-revalidate=604800"); - - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - - assertNotNull(entry); - assertNull(entry.etag); - assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS); - assertEqualsWithin(now + ONE_DAY_MILLIS + ONE_WEEK_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); - } - - @Test - public void parseCacheHeaders_cacheControlNoCache() { - long now = System.currentTimeMillis(); - headers.put("Date", rfc1123Date(now)); - headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); - headers.put("Cache-Control", "no-cache"); - - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - - assertNull(entry); - } - - @Test - public void parseCacheHeaders_cacheControlMustRevalidateNoMaxAge() { - long now = System.currentTimeMillis(); - headers.put("Date", rfc1123Date(now)); - headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); - headers.put("Cache-Control", "must-revalidate"); - - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - assertNotNull(entry); - assertNull(entry.etag); - assertEqualsWithin(now, entry.ttl, ONE_MINUTE_MILLIS); - assertEquals(entry.softTtl, entry.ttl); - } - - @Test - public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAge() { - long now = System.currentTimeMillis(); - headers.put("Date", rfc1123Date(now)); - headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); - headers.put("Cache-Control", "must-revalidate, max-age=3600"); - - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - assertNotNull(entry); - assertNull(entry.etag); - assertEqualsWithin(now + ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); - assertEquals(entry.softTtl, entry.ttl); - } - - @Test - public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAgeAndStale() { - long now = System.currentTimeMillis(); - headers.put("Date", rfc1123Date(now)); - headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); - - // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day - // - stale-while-revalidate (entry.ttl) indicates that the asset may - // continue to be served stale for up to additional 7 days, but this is - // ignored in this case because of the must-revalidate header. - headers.put( - "Cache-Control", "must-revalidate, max-age=86400, stale-while-revalidate=604800"); - - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - assertNotNull(entry); - assertNull(entry.etag); - assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS); - assertEquals(entry.softTtl, entry.ttl); - } - - private void assertEqualsWithin(long expected, long value, long fudgeFactor) { - long diff = Math.abs(expected - value); - assertTrue(diff < fudgeFactor); - } - - private static String rfc1123Date(long millis) { - DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH); - return df.format(new Date(millis)); - } - - // -------------------------- - - @Test - public void parseCharset() { - // Like the ones we usually see - headers.put("Content-Type", "text/plain; charset=utf-8"); - assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); - - // Charset specified, ignore default charset - headers.put("Content-Type", "text/plain; charset=utf-8"); - assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "ISO-8859-1")); - - // Extra whitespace - headers.put("Content-Type", "text/plain; charset=utf-8 "); - assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); - - // Extra parameters - headers.put("Content-Type", "text/plain; charset=utf-8; frozzle=bar"); - assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); - - // No Content-Type header - headers.clear(); - assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); - - // No Content-Type header, use default charset - headers.clear(); - assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "utf-8")); - - // Empty value - headers.put("Content-Type", "text/plain; charset="); - assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); - - // None specified - headers.put("Content-Type", "text/plain"); - assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); - - // None charset specified, use default charset - headers.put("Content-Type", "application/json"); - assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "utf-8")); - - // None specified, extra semicolon - headers.put("Content-Type", "text/plain;"); - assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); - - // No headers, use default charset - assertEquals("utf-8", HttpHeaderParser.parseCharset(null, "utf-8")); - } - - @Test - public void parseCaseInsensitive() { - long now = System.currentTimeMillis(); - - List
headers = new ArrayList<>(); - headers.add(new Header("eTAG", "Yow!")); - headers.add(new Header("DATE", rfc1123Date(now))); - headers.add(new Header("expires", rfc1123Date(now + ONE_HOUR_MILLIS))); - headers.add(new Header("cache-control", "public, max-age=86400")); - headers.add(new Header("content-type", "text/plain")); - - NetworkResponse response = new NetworkResponse(0, null, false, 0, headers); - Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); - - assertNotNull(entry); - assertEquals("Yow!", entry.etag); - assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); - assertEquals(entry.softTtl, entry.ttl); - assertEquals( - "ISO-8859-1", HttpHeaderParser.parseCharset(HttpHeaderParser.toHeaderMap(headers))); - } -} diff --git a/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java b/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java deleted file mode 100644 index 6794af8..0000000 --- a/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.android.volley.toolbox; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import com.android.volley.Request; -import com.android.volley.RetryPolicy; -import java.io.IOException; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.apache.http.Header; -import org.apache.http.HttpRequest; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpUriRequest; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.Spy; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.robolectric.RobolectricTestRunner; - -/** Tests to validate that HttpStack implementations conform with expected behavior. */ -@RunWith(RobolectricTestRunner.class) -public class HttpStackConformanceTest { - @Mock private RetryPolicy mMockRetryPolicy; - @Mock private Request mMockRequest; - - @Mock private HttpURLConnection mMockConnection; - @Mock private OutputStream mMockOutputStream; - @Spy private HurlStack mHurlStack = new HurlStack(); - - @Mock private HttpClient mMockHttpClient; - private HttpClientStack mHttpClientStack; - - private final TestCase[] mTestCases = - new TestCase[] { - // TestCase for HurlStack. - new TestCase() { - @Override - public HttpStack getStack() { - return mHurlStack; - } - - @Override - public void setOutputHeaderMap(final Map outputHeaderMap) { - doAnswer( - new Answer() { - @Override - public Void answer(InvocationOnMock invocation) { - outputHeaderMap.put( - invocation.getArgument(0), - invocation.getArgument(1)); - return null; - } - }) - .when(mMockConnection) - .setRequestProperty(anyString(), anyString()); - doAnswer( - new Answer>>() { - @Override - public Map> answer( - InvocationOnMock invocation) { - Map> result = new HashMap<>(); - for (Map.Entry entry : - outputHeaderMap.entrySet()) { - result.put( - entry.getKey(), - Collections.singletonList( - entry.getValue())); - } - return result; - } - }) - .when(mMockConnection) - .getRequestProperties(); - } - }, - - // TestCase for HttpClientStack. - new TestCase() { - @Override - public HttpStack getStack() { - return mHttpClientStack; - } - - @Override - public void setOutputHeaderMap(final Map outputHeaderMap) { - try { - doAnswer( - new Answer() { - @Override - public Void answer(InvocationOnMock invocation) - throws Throwable { - HttpRequest request = invocation.getArgument(0); - for (Header header : request.getAllHeaders()) { - if (outputHeaderMap.containsKey( - header.getName())) { - fail( - "Multiple values for header " - + header.getName()); - } - outputHeaderMap.put( - header.getName(), - header.getValue()); - } - return null; - } - }) - .when(mMockHttpClient) - .execute(any(HttpUriRequest.class)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - }; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - mHttpClientStack = spy(new HttpClientStack(mMockHttpClient)); - - doReturn(mMockConnection).when(mHurlStack).createConnection(any(URL.class)); - doReturn(mMockOutputStream).when(mMockConnection).getOutputStream(); - when(mMockRequest.getUrl()).thenReturn("http://127.0.0.1"); - when(mMockRequest.getRetryPolicy()).thenReturn(mMockRetryPolicy); - } - - @Test - public void headerPrecedence() throws Exception { - Map additionalHeaders = new HashMap<>(); - additionalHeaders.put("A", "AddlA"); - additionalHeaders.put("B", "AddlB"); - - Map requestHeaders = new HashMap<>(); - requestHeaders.put("A", "RequestA"); - requestHeaders.put("C", "RequestC"); - when(mMockRequest.getHeaders()).thenReturn(requestHeaders); - - when(mMockRequest.getMethod()).thenReturn(Request.Method.POST); - when(mMockRequest.getBody()).thenReturn(new byte[0]); - when(mMockRequest.getBodyContentType()).thenReturn("BodyContentType"); - - for (TestCase testCase : mTestCases) { - // Test once without a Content-Type header in getHeaders(). - Map combinedHeaders = new HashMap<>(); - testCase.setOutputHeaderMap(combinedHeaders); - - testCase.getStack().performRequest(mMockRequest, additionalHeaders); - - Map expectedHeaders = new HashMap<>(); - expectedHeaders.put("A", "RequestA"); - expectedHeaders.put("B", "AddlB"); - expectedHeaders.put("C", "RequestC"); - expectedHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "BodyContentType"); - - assertEquals(expectedHeaders, combinedHeaders); - - // Reset and test again with a Content-Type header in getHeaders(). - combinedHeaders.clear(); - - requestHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "RequestContentType"); - expectedHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "RequestContentType"); - - testCase.getStack().performRequest(mMockRequest, additionalHeaders); - assertEquals(expectedHeaders, combinedHeaders); - - // Clear the Content-Type header for the next TestCase. - requestHeaders.remove(HttpHeaderParser.HEADER_CONTENT_TYPE); - } - } - - private interface TestCase { - HttpStack getStack(); - - void setOutputHeaderMap(Map outputHeaderMap); - } -} diff --git a/src/test/java/com/android/volley/toolbox/HurlStackTest.java b/src/test/java/com/android/volley/toolbox/HurlStackTest.java deleted file mode 100644 index 7508244..0000000 --- a/src/test/java/com/android/volley/toolbox/HurlStackTest.java +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.android.volley.Header; -import com.android.volley.Request; -import com.android.volley.Request.Method; -import com.android.volley.mock.TestRequest; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FilterInputStream; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class HurlStackTest { - - @Mock private HttpURLConnection mMockConnection; - private HurlStack mHurlStack; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - when(mMockConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); - - mHurlStack = - new HurlStack() { - @Override - protected HttpURLConnection createConnection(URL url) { - return mMockConnection; - } - - @Override - protected InputStream createInputStream( - Request request, HttpURLConnection connection) { - return new MonitoringInputStream( - super.createInputStream(request, connection)); - } - - @Override - protected OutputStream createOutputStream( - Request request, HttpURLConnection connection, int length) - throws IOException { - if (request instanceof MonitoredRequest) { - return new MonitoringOutputStream( - super.createOutputStream(request, connection, length), - (MonitoredRequest) request, - length); - } - return super.createOutputStream(request, connection, length); - } - }; - } - - @Test - public void connectionForDeprecatedGetRequest() throws Exception { - TestRequest.DeprecatedGet request = new TestRequest.DeprecatedGet(); - assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection, never()).setRequestMethod(anyString()); - verify(mMockConnection, never()).setDoOutput(true); - } - - @Test - public void connectionForDeprecatedPostRequest() throws Exception { - TestRequest.DeprecatedPost request = new TestRequest.DeprecatedPost(); - assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection).setRequestMethod("POST"); - verify(mMockConnection).setDoOutput(true); - } - - @Test - public void connectionForGetRequest() throws Exception { - TestRequest.Get request = new TestRequest.Get(); - assertEquals(request.getMethod(), Method.GET); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection).setRequestMethod("GET"); - verify(mMockConnection, never()).setDoOutput(true); - } - - @Test - public void connectionForPostRequest() throws Exception { - TestRequest.Post request = new TestRequest.Post(); - assertEquals(request.getMethod(), Method.POST); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection).setRequestMethod("POST"); - verify(mMockConnection, never()).setDoOutput(true); - } - - @Test - public void connectionForPostWithBodyRequest() throws Exception { - TestRequest.PostWithBody request = new TestRequest.PostWithBody(); - assertEquals(request.getMethod(), Method.POST); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection).setRequestMethod("POST"); - verify(mMockConnection).setDoOutput(true); - } - - @Test - public void connectionForPutRequest() throws Exception { - TestRequest.Put request = new TestRequest.Put(); - assertEquals(request.getMethod(), Method.PUT); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection).setRequestMethod("PUT"); - verify(mMockConnection, never()).setDoOutput(true); - } - - @Test - public void connectionForPutWithBodyRequest() throws Exception { - TestRequest.PutWithBody request = new TestRequest.PutWithBody(); - assertEquals(request.getMethod(), Method.PUT); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection).setRequestMethod("PUT"); - verify(mMockConnection).setDoOutput(true); - } - - @Test - public void connectionForDeleteRequest() throws Exception { - TestRequest.Delete request = new TestRequest.Delete(); - assertEquals(request.getMethod(), Method.DELETE); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection).setRequestMethod("DELETE"); - verify(mMockConnection, never()).setDoOutput(true); - } - - @Test - public void connectionForHeadRequest() throws Exception { - TestRequest.Head request = new TestRequest.Head(); - assertEquals(request.getMethod(), Method.HEAD); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection).setRequestMethod("HEAD"); - verify(mMockConnection, never()).setDoOutput(true); - } - - @Test - public void connectionForOptionsRequest() throws Exception { - TestRequest.Options request = new TestRequest.Options(); - assertEquals(request.getMethod(), Method.OPTIONS); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection).setRequestMethod("OPTIONS"); - verify(mMockConnection, never()).setDoOutput(true); - } - - @Test - public void connectionForTraceRequest() throws Exception { - TestRequest.Trace request = new TestRequest.Trace(); - assertEquals(request.getMethod(), Method.TRACE); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection).setRequestMethod("TRACE"); - verify(mMockConnection, never()).setDoOutput(true); - } - - @Test - public void connectionForPatchRequest() throws Exception { - TestRequest.Patch request = new TestRequest.Patch(); - assertEquals(request.getMethod(), Method.PATCH); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection).setRequestMethod("PATCH"); - verify(mMockConnection, never()).setDoOutput(true); - } - - @Test - public void connectionForPatchWithBodyRequest() throws Exception { - TestRequest.PatchWithBody request = new TestRequest.PatchWithBody(); - assertEquals(request.getMethod(), Method.PATCH); - - mHurlStack.setConnectionParametersForRequest(mMockConnection, request); - verify(mMockConnection).setRequestMethod("PATCH"); - verify(mMockConnection).setDoOutput(true); - } - - @Test - public void executeRequestClosesConnection_connectionError() throws Exception { - when(mMockConnection.getResponseCode()).thenThrow(new SocketTimeoutException()); - try { - mHurlStack.executeRequest( - new TestRequest.Get(), Collections.emptyMap()); - fail("Should have thrown exception"); - } catch (IOException e) { - verify(mMockConnection).disconnect(); - } - } - - @Test - public void executeRequestClosesConnection_invalidResponseCode() throws Exception { - when(mMockConnection.getResponseCode()).thenReturn(-1); - try { - mHurlStack.executeRequest( - new TestRequest.Get(), Collections.emptyMap()); - fail("Should have thrown exception"); - } catch (IOException e) { - verify(mMockConnection).disconnect(); - } - } - - @Test - public void executeRequestClosesConnection_noResponseBody() throws Exception { - when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NO_CONTENT); - mHurlStack.executeRequest(new TestRequest.Get(), Collections.emptyMap()); - verify(mMockConnection).disconnect(); - } - - @Test - public void executeRequestClosesConnection_hasResponseBody() throws Exception { - when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); - when(mMockConnection.getInputStream()) - .thenReturn(new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8))); - HttpResponse response = - mHurlStack.executeRequest( - new TestRequest.Get(), Collections.emptyMap()); - // Shouldn't be disconnected until the stream is consumed. - verify(mMockConnection, never()).disconnect(); - response.getContent().close(); - verify(mMockConnection).disconnect(); - } - - @Test - public void convertHeaders() { - Map> headers = new HashMap<>(); - headers.put(null, Collections.singletonList("Ignored")); - headers.put("HeaderA", Collections.singletonList("ValueA")); - List values = new ArrayList<>(); - values.add("ValueB_1"); - values.add("ValueB_2"); - headers.put("HeaderB", values); - List
result = HurlStack.convertHeaders(headers); - List
expected = new ArrayList<>(); - expected.add(new Header("HeaderA", "ValueA")); - expected.add(new Header("HeaderB", "ValueB_1")); - expected.add(new Header("HeaderB", "ValueB_2")); - assertEquals(expected, result); - } - - @Test - public void interceptResponseStream() throws Exception { - when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); - when(mMockConnection.getInputStream()) - .thenReturn(new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8))); - HttpResponse response = - mHurlStack.executeRequest( - new TestRequest.Get(), Collections.emptyMap()); - assertTrue(response.getContent() instanceof MonitoringInputStream); - } - - @Test - public void interceptRequestStream() throws Exception { - MonitoredRequest request = new MonitoredRequest(); - mHurlStack.executeRequest(request, Collections.emptyMap()); - assertTrue(request.totalRequestBytes > 0); - assertEquals(request.totalRequestBytes, request.requestBytesRead); - } - - private static class MonitoringInputStream extends FilterInputStream { - private MonitoringInputStream(InputStream in) { - super(in); - } - } - - private static class MonitoringOutputStream extends FilterOutputStream { - private MonitoredRequest request; - - private MonitoringOutputStream(OutputStream out, MonitoredRequest request, int length) { - super(out); - this.request = request; - this.request.totalRequestBytes = length; - } - - @Override - public void write(int b) throws IOException { - this.request.requestBytesRead++; - out.write(b); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - this.request.requestBytesRead += len; - out.write(b, off, len); - } - } - - private static class MonitoredRequest extends TestRequest.PostWithBody { - int requestBytesRead = 0; - int totalRequestBytes = 0; - } -} diff --git a/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java b/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java deleted file mode 100644 index 59a0b1b..0000000 --- a/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.graphics.Bitmap; -import android.widget.ImageView; -import com.android.volley.Request; -import com.android.volley.RequestQueue; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class ImageLoaderTest { - private RequestQueue mRequestQueue; - private ImageLoader.ImageCache mImageCache; - private ImageLoader mImageLoader; - - @Before - public void setUp() { - mRequestQueue = mock(RequestQueue.class); - mImageCache = mock(ImageLoader.ImageCache.class); - mImageLoader = new ImageLoader(mRequestQueue, mImageCache); - } - - @Test - public void isCachedChecksCache() throws Exception { - when(mImageCache.getBitmap(anyString())).thenReturn(null); - Assert.assertFalse(mImageLoader.isCached("http://foo", 0, 0)); - } - - @Test - public void getWithCacheHit() throws Exception { - Bitmap bitmap = Bitmap.createBitmap(1, 1, null); - ImageLoader.ImageListener listener = mock(ImageLoader.ImageListener.class); - when(mImageCache.getBitmap(anyString())).thenReturn(bitmap); - ImageLoader.ImageContainer ic = mImageLoader.get("http://foo", listener); - Assert.assertSame(bitmap, ic.getBitmap()); - verify(listener).onResponse(ic, true); - } - - @Test - public void getWithCacheMiss() throws Exception { - when(mImageCache.getBitmap(anyString())).thenReturn(null); - ImageLoader.ImageListener listener = mock(ImageLoader.ImageListener.class); - // Ask for the image to be loaded. - mImageLoader.get("http://foo", listener); - // Second pass to test deduping logic. - mImageLoader.get("http://foo", listener); - // Response callback should be called both times. - verify(listener, times(2)).onResponse(any(ImageLoader.ImageContainer.class), eq(true)); - // But request should be enqueued only once. - verify(mRequestQueue, times(1)).add(Mockito.>any()); - } - - @Test - public void publicMethods() throws Exception { - // Catch API breaking changes. - ImageLoader.getImageListener(null, -1, -1); - mImageLoader.setBatchedResponseDelay(1000); - - assertNotNull( - ImageLoader.class.getConstructor(RequestQueue.class, ImageLoader.ImageCache.class)); - - assertNotNull( - ImageLoader.class.getMethod( - "getImageListener", ImageView.class, int.class, int.class)); - assertNotNull(ImageLoader.class.getMethod("isCached", String.class, int.class, int.class)); - assertNotNull( - ImageLoader.class.getMethod( - "isCached", String.class, int.class, int.class, ImageView.ScaleType.class)); - assertNotNull( - ImageLoader.class.getMethod("get", String.class, ImageLoader.ImageListener.class)); - assertNotNull( - ImageLoader.class.getMethod( - "get", - String.class, - ImageLoader.ImageListener.class, - int.class, - int.class)); - assertNotNull( - ImageLoader.class.getMethod( - "get", - String.class, - ImageLoader.ImageListener.class, - int.class, - int.class, - ImageView.ScaleType.class)); - assertNotNull(ImageLoader.class.getMethod("setBatchedResponseDelay", int.class)); - - assertNotNull( - ImageLoader.ImageListener.class.getMethod( - "onResponse", ImageLoader.ImageContainer.class, boolean.class)); - } -} diff --git a/src/test/java/com/android/volley/toolbox/ImageRequestTest.java b/src/test/java/com/android/volley/toolbox/ImageRequestTest.java deleted file mode 100644 index 6b50319..0000000 --- a/src/test/java/com/android/volley/toolbox/ImageRequestTest.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; -import android.widget.ImageView; -import android.widget.ImageView.ScaleType; -import com.android.volley.NetworkResponse; -import com.android.volley.Response; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.shadows.ShadowBitmapFactory; - -@RunWith(RobolectricTestRunner.class) -public class ImageRequestTest { - - @Test - public void parseNetworkResponse_resizing() throws Exception { - // This is a horrible hack but Robolectric doesn't have a way to provide - // width and height hints for decodeByteArray. It works because the byte array - // "file:fake" is ASCII encodable and thus the name in Robolectric's fake - // bitmap creator survives as-is, and provideWidthAndHeightHints puts - // "file:" + name in its lookaside map. I write all this because it will - // probably break mysteriously at some point and I feel terrible about your - // having to debug it. - byte[] jpegBytes = "file:fake".getBytes(StandardCharsets.UTF_8); - ShadowBitmapFactory.provideWidthAndHeightHints("fake", 1024, 500); - NetworkResponse jpeg = new NetworkResponse(jpegBytes); - - // Scale the image uniformly (maintain the image's aspect ratio) so that - // both dimensions (width and height) of the image will be equal to or - // less than the corresponding dimension of the view. - ScaleType scalteType = ScaleType.CENTER_INSIDE; - - // Exact sizes - verifyResize(jpeg, 512, 250, scalteType, 512, 250); // exactly half - verifyResize(jpeg, 511, 249, scalteType, 509, 249); // just under half - verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); // larger - verifyResize(jpeg, 500, 500, scalteType, 500, 244); // keep same ratio - - // Specify only width, preserve aspect ratio - verifyResize(jpeg, 512, 0, scalteType, 512, 250); - verifyResize(jpeg, 800, 0, scalteType, 800, 390); - verifyResize(jpeg, 1024, 0, scalteType, 1024, 500); - - // Specify only height, preserve aspect ratio - verifyResize(jpeg, 0, 250, scalteType, 512, 250); - verifyResize(jpeg, 0, 391, scalteType, 800, 391); - verifyResize(jpeg, 0, 500, scalteType, 1024, 500); - - // No resize - verifyResize(jpeg, 0, 0, scalteType, 1024, 500); - - // Scale the image uniformly (maintain the image's aspect ratio) so that - // both dimensions (width and height) of the image will be equal to or - // larger than the corresponding dimension of the view. - scalteType = ScaleType.CENTER_CROP; - - // Exact sizes - verifyResize(jpeg, 512, 250, scalteType, 512, 250); - verifyResize(jpeg, 511, 249, scalteType, 511, 249); - verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); - verifyResize(jpeg, 500, 500, scalteType, 1024, 500); - - // Specify only width - verifyResize(jpeg, 512, 0, scalteType, 512, 250); - verifyResize(jpeg, 800, 0, scalteType, 800, 390); - verifyResize(jpeg, 1024, 0, scalteType, 1024, 500); - - // Specify only height - verifyResize(jpeg, 0, 250, scalteType, 512, 250); - verifyResize(jpeg, 0, 391, scalteType, 800, 391); - verifyResize(jpeg, 0, 500, scalteType, 1024, 500); - - // No resize - verifyResize(jpeg, 0, 0, scalteType, 1024, 500); - - // Scale in X and Y independently, so that src matches dst exactly. This - // may change the aspect ratio of the src. - scalteType = ScaleType.FIT_XY; - - // Exact sizes - verifyResize(jpeg, 512, 250, scalteType, 512, 250); - verifyResize(jpeg, 511, 249, scalteType, 511, 249); - verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); - verifyResize(jpeg, 500, 500, scalteType, 500, 500); - - // Specify only width - verifyResize(jpeg, 512, 0, scalteType, 512, 500); - verifyResize(jpeg, 800, 0, scalteType, 800, 500); - verifyResize(jpeg, 1024, 0, scalteType, 1024, 500); - - // Specify only height - verifyResize(jpeg, 0, 250, scalteType, 1024, 250); - verifyResize(jpeg, 0, 391, scalteType, 1024, 391); - verifyResize(jpeg, 0, 500, scalteType, 1024, 500); - - // No resize - verifyResize(jpeg, 0, 0, scalteType, 1024, 500); - } - - private void verifyResize( - NetworkResponse networkResponse, - int maxWidth, - int maxHeight, - ScaleType scaleType, - int expectedWidth, - int expectedHeight) { - ImageRequest request = - new ImageRequest("", null, maxWidth, maxHeight, scaleType, Config.RGB_565, null); - Response response = request.parseNetworkResponse(networkResponse); - assertNotNull(response); - assertTrue(response.isSuccess()); - Bitmap bitmap = response.result; - assertNotNull(bitmap); - assertEquals(expectedWidth, bitmap.getWidth()); - assertEquals(expectedHeight, bitmap.getHeight()); - } - - @Test - public void findBestSampleSize() { - // desired == actual == 1 - assertEquals(1, ImageRequest.findBestSampleSize(100, 150, 100, 150)); - - // exactly half == 2 - assertEquals(2, ImageRequest.findBestSampleSize(280, 160, 140, 80)); - - // just over half == 1 - assertEquals(1, ImageRequest.findBestSampleSize(1000, 800, 501, 401)); - - // just under 1/4 == 4 - assertEquals(4, ImageRequest.findBestSampleSize(100, 200, 24, 50)); - } - - private static byte[] readInputStream(InputStream in) throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int count; - while ((count = in.read(buffer)) != -1) { - bytes.write(buffer, 0, count); - } - in.close(); - return bytes.toByteArray(); - } - - @Test - public void publicMethods() throws Exception { - // Catch-all test to find API-breaking changes. - assertNotNull( - ImageRequest.class.getConstructor( - String.class, - Response.Listener.class, - int.class, - int.class, - Bitmap.Config.class, - Response.ErrorListener.class)); - assertNotNull( - ImageRequest.class.getConstructor( - String.class, - Response.Listener.class, - int.class, - int.class, - ImageView.ScaleType.class, - Bitmap.Config.class, - Response.ErrorListener.class)); - assertEquals(ImageRequest.DEFAULT_IMAGE_TIMEOUT_MS, 1000); - assertEquals(ImageRequest.DEFAULT_IMAGE_MAX_RETRIES, 2); - assertEquals(ImageRequest.DEFAULT_IMAGE_BACKOFF_MULT, 2f, 0); - } -} diff --git a/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java b/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java deleted file mode 100644 index 70bb2ea..0000000 --- a/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import com.android.volley.NetworkResponse; -import com.android.volley.Response; -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; -import org.json.JSONArray; -import org.json.JSONObject; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class JsonRequestCharsetTest { - - /** String in Czech - "Retezec v cestine." */ - private static final String TEXT_VALUE = "\u0158et\u011bzec v \u010de\u0161tin\u011b."; - - private static final String TEXT_NAME = "text"; - private static final int TEXT_INDEX = 0; - - /** - * Copyright symbol has different encoding in utf-8 and ISO-8859-1, and it doesn't exists in - * ISO-8859-2 - */ - private static final String COPY_VALUE = "\u00a9"; - - private static final String COPY_NAME = "copyright"; - private static final int COPY_INDEX = 1; - - @Test - public void defaultCharsetJsonObject() throws Exception { - // UTF-8 is default charset for JSON - byte[] data = jsonObjectString().getBytes(Charset.forName("UTF-8")); - NetworkResponse network = new NetworkResponse(data); - JsonObjectRequest objectRequest = new JsonObjectRequest("", null, null, null); - Response objectResponse = objectRequest.parseNetworkResponse(network); - - assertNotNull(objectResponse); - assertTrue(objectResponse.isSuccess()); - assertEquals(TEXT_VALUE, objectResponse.result.getString(TEXT_NAME)); - assertEquals(COPY_VALUE, objectResponse.result.getString(COPY_NAME)); - } - - @Test - public void defaultCharsetJsonArray() throws Exception { - // UTF-8 is default charset for JSON - byte[] data = jsonArrayString().getBytes(Charset.forName("UTF-8")); - NetworkResponse network = new NetworkResponse(data); - JsonArrayRequest arrayRequest = new JsonArrayRequest("", null, null); - Response arrayResponse = arrayRequest.parseNetworkResponse(network); - - assertNotNull(arrayResponse); - assertTrue(arrayResponse.isSuccess()); - assertEquals(TEXT_VALUE, arrayResponse.result.getString(TEXT_INDEX)); - assertEquals(COPY_VALUE, arrayResponse.result.getString(COPY_INDEX)); - } - - @Test - public void specifiedCharsetJsonObject() throws Exception { - byte[] data = jsonObjectString().getBytes(Charset.forName("ISO-8859-1")); - Map headers = new HashMap(); - headers.put("Content-Type", "application/json; charset=iso-8859-1"); - NetworkResponse network = new NetworkResponse(data, headers); - JsonObjectRequest objectRequest = new JsonObjectRequest("", null, null, null); - Response objectResponse = objectRequest.parseNetworkResponse(network); - - assertNotNull(objectResponse); - assertTrue(objectResponse.isSuccess()); - // don't check the text in Czech, ISO-8859-1 doesn't support some Czech characters - assertEquals(COPY_VALUE, objectResponse.result.getString(COPY_NAME)); - } - - @Test - public void specifiedCharsetJsonArray() throws Exception { - byte[] data = jsonArrayString().getBytes(Charset.forName("ISO-8859-2")); - Map headers = new HashMap(); - headers.put("Content-Type", "application/json; charset=iso-8859-2"); - NetworkResponse network = new NetworkResponse(data, headers); - JsonArrayRequest arrayRequest = new JsonArrayRequest("", null, null); - Response arrayResponse = arrayRequest.parseNetworkResponse(network); - - assertNotNull(arrayResponse); - assertTrue(arrayResponse.isSuccess()); - assertEquals(TEXT_VALUE, arrayResponse.result.getString(TEXT_INDEX)); - // don't check the copyright symbol, ISO-8859-2 doesn't have it, but it has Czech characters - } - - private static String jsonObjectString() throws Exception { - JSONObject json = new JSONObject().put(TEXT_NAME, TEXT_VALUE).put(COPY_NAME, COPY_VALUE); - return json.toString(); - } - - private static String jsonArrayString() throws Exception { - JSONArray json = new JSONArray().put(TEXT_INDEX, TEXT_VALUE).put(COPY_INDEX, COPY_VALUE); - return json.toString(); - } -} diff --git a/src/test/java/com/android/volley/toolbox/JsonRequestTest.java b/src/test/java/com/android/volley/toolbox/JsonRequestTest.java deleted file mode 100644 index 44c0ad9..0000000 --- a/src/test/java/com/android/volley/toolbox/JsonRequestTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertNotNull; - -import com.android.volley.Response; -import org.json.JSONArray; -import org.json.JSONObject; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class JsonRequestTest { - - @Test - public void publicMethods() throws Exception { - // Catch-all test to find API-breaking changes. - assertNotNull( - JsonRequest.class.getConstructor( - String.class, - String.class, - Response.Listener.class, - Response.ErrorListener.class)); - assertNotNull( - JsonRequest.class.getConstructor( - int.class, - String.class, - String.class, - Response.Listener.class, - Response.ErrorListener.class)); - - assertNotNull( - JsonArrayRequest.class.getConstructor( - String.class, Response.Listener.class, Response.ErrorListener.class)); - assertNotNull( - JsonArrayRequest.class.getConstructor( - int.class, - String.class, - JSONArray.class, - Response.Listener.class, - Response.ErrorListener.class)); - - assertNotNull( - JsonObjectRequest.class.getConstructor( - String.class, - JSONObject.class, - Response.Listener.class, - Response.ErrorListener.class)); - assertNotNull( - JsonObjectRequest.class.getConstructor( - int.class, - String.class, - JSONObject.class, - Response.Listener.class, - Response.ErrorListener.class)); - } -} diff --git a/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java b/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java deleted file mode 100644 index fd2073e..0000000 --- a/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.ViewGroup.LayoutParams; -import android.widget.ImageView.ScaleType; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; - -@RunWith(RobolectricTestRunner.class) -public class NetworkImageViewTest { - private NetworkImageView mNIV; - private MockImageLoader mMockImageLoader; - - @Before - public void setUp() throws Exception { - mMockImageLoader = new MockImageLoader(); - mNIV = new NetworkImageView(RuntimeEnvironment.application); - } - - @Test - public void setImageUrl_requestsImage() { - mNIV.setLayoutParams( - new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); - mNIV.setImageUrl("http://foo", mMockImageLoader); - assertEquals("http://foo", mMockImageLoader.lastRequestUrl); - assertEquals(0, mMockImageLoader.lastMaxWidth); - assertEquals(0, mMockImageLoader.lastMaxHeight); - } - - // public void testSetImageUrl_setsMaxSize() { - // // TODO: Not sure how to make getWidth() return something from an - // // instrumentation test. Write this test once it's figured out. - // } - - private static class MockImageLoader extends ImageLoader { - public MockImageLoader() { - super(null, null); - } - - public String lastRequestUrl; - public int lastMaxWidth; - public int lastMaxHeight; - - @Override - public ImageContainer get( - String requestUrl, - ImageListener imageListener, - int maxWidth, - int maxHeight, - ScaleType scaleType) { - lastRequestUrl = requestUrl; - lastMaxWidth = maxWidth; - lastMaxHeight = maxHeight; - return null; - } - } - - @Test - public void publicMethods() throws Exception { - // Catch-all test to find API-breaking changes. - assertNotNull(NetworkImageView.class.getConstructor(Context.class)); - assertNotNull(NetworkImageView.class.getConstructor(Context.class, AttributeSet.class)); - assertNotNull( - NetworkImageView.class.getConstructor( - Context.class, AttributeSet.class, int.class)); - - assertNotNull( - NetworkImageView.class.getMethod("setImageUrl", String.class, ImageLoader.class)); - assertNotNull(NetworkImageView.class.getMethod("setDefaultImageDrawable", Drawable.class)); - assertNotNull(NetworkImageView.class.getMethod("setDefaultImageBitmap", Bitmap.class)); - assertNotNull(NetworkImageView.class.getMethod("setDefaultImageResId", int.class)); - assertNotNull(NetworkImageView.class.getMethod("setErrorImageDrawable", Drawable.class)); - assertNotNull(NetworkImageView.class.getMethod("setErrorImageBitmap", Bitmap.class)); - assertNotNull(NetworkImageView.class.getMethod("setErrorImageResId", int.class)); - } -} diff --git a/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java b/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java deleted file mode 100644 index 266edcd..0000000 --- a/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertTrue; - -import java.io.IOException; -import java.util.Arrays; -import org.junit.Test; - -public class PoolingByteArrayOutputStreamTest { - @Test - public void pooledOneBuffer() throws IOException { - ByteArrayPool pool = new ByteArrayPool(32768); - writeOneBuffer(pool); - writeOneBuffer(pool); - writeOneBuffer(pool); - } - - @Test - public void pooledIndividualWrites() throws IOException { - ByteArrayPool pool = new ByteArrayPool(32768); - writeBytesIndividually(pool); - writeBytesIndividually(pool); - writeBytesIndividually(pool); - } - - @Test - public void unpooled() throws IOException { - ByteArrayPool pool = new ByteArrayPool(0); - writeOneBuffer(pool); - writeOneBuffer(pool); - writeOneBuffer(pool); - } - - @Test - public void unpooledIndividualWrites() throws IOException { - ByteArrayPool pool = new ByteArrayPool(0); - writeBytesIndividually(pool); - writeBytesIndividually(pool); - writeBytesIndividually(pool); - } - - private void writeOneBuffer(ByteArrayPool pool) throws IOException { - byte[] data = new byte[16384]; - for (int i = 0; i < data.length; i++) { - data[i] = (byte) (i & 0xff); - } - PoolingByteArrayOutputStream os = new PoolingByteArrayOutputStream(pool); - os.write(data); - - assertTrue(Arrays.equals(data, os.toByteArray())); - } - - private void writeBytesIndividually(ByteArrayPool pool) { - byte[] data = new byte[16384]; - for (int i = 0; i < data.length; i++) { - data[i] = (byte) (i & 0xff); - } - PoolingByteArrayOutputStream os = new PoolingByteArrayOutputStream(pool); - for (int i = 0; i < data.length; i++) { - os.write(data[i]); - } - - assertTrue(Arrays.equals(data, os.toByteArray())); - } -} diff --git a/src/test/java/com/android/volley/toolbox/RequestFutureTest.java b/src/test/java/com/android/volley/toolbox/RequestFutureTest.java deleted file mode 100644 index 5b5c975..0000000 --- a/src/test/java/com/android/volley/toolbox/RequestFutureTest.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertNotNull; - -import com.android.volley.Request; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class RequestFutureTest { - - @Test - public void publicMethods() throws Exception { - // Catch-all test to find API-breaking changes. - assertNotNull(RequestFuture.class.getMethod("newFuture")); - assertNotNull(RequestFuture.class.getMethod("setRequest", Request.class)); - } -} diff --git a/src/test/java/com/android/volley/toolbox/RequestQueueTest.java b/src/test/java/com/android/volley/toolbox/RequestQueueTest.java deleted file mode 100644 index 1899b71..0000000 --- a/src/test/java/com/android/volley/toolbox/RequestQueueTest.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertNotNull; - -import com.android.volley.Cache; -import com.android.volley.Network; -import com.android.volley.Request; -import com.android.volley.RequestQueue; -import com.android.volley.ResponseDelivery; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class RequestQueueTest { - - @Test - public void publicMethods() throws Exception { - // Catch-all test to find API-breaking changes. - assertNotNull( - RequestQueue.class.getConstructor( - Cache.class, Network.class, int.class, ResponseDelivery.class)); - assertNotNull(RequestQueue.class.getConstructor(Cache.class, Network.class, int.class)); - assertNotNull(RequestQueue.class.getConstructor(Cache.class, Network.class)); - - assertNotNull(RequestQueue.class.getMethod("start")); - assertNotNull(RequestQueue.class.getMethod("stop")); - assertNotNull(RequestQueue.class.getMethod("getSequenceNumber")); - assertNotNull(RequestQueue.class.getMethod("getCache")); - assertNotNull(RequestQueue.class.getMethod("cancelAll", RequestQueue.RequestFilter.class)); - assertNotNull(RequestQueue.class.getMethod("cancelAll", Object.class)); - assertNotNull(RequestQueue.class.getMethod("add", Request.class)); - assertNotNull(RequestQueue.class.getDeclaredMethod("finish", Request.class)); - } -} diff --git a/src/test/java/com/android/volley/toolbox/RequestTest.java b/src/test/java/com/android/volley/toolbox/RequestTest.java deleted file mode 100644 index 0911ad6..0000000 --- a/src/test/java/com/android/volley/toolbox/RequestTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertNotNull; - -import com.android.volley.Cache; -import com.android.volley.NetworkResponse; -import com.android.volley.Request; -import com.android.volley.RequestQueue; -import com.android.volley.Response; -import com.android.volley.RetryPolicy; -import com.android.volley.VolleyError; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class RequestTest { - - @Test - public void publicMethods() throws Exception { - // Catch-all test to find API-breaking changes. - assertNotNull( - Request.class.getConstructor( - int.class, String.class, Response.ErrorListener.class)); - - assertNotNull(Request.class.getMethod("getMethod")); - assertNotNull(Request.class.getMethod("setTag", Object.class)); - assertNotNull(Request.class.getMethod("getTag")); - assertNotNull(Request.class.getMethod("getErrorListener")); - assertNotNull(Request.class.getMethod("getTrafficStatsTag")); - assertNotNull(Request.class.getMethod("setRetryPolicy", RetryPolicy.class)); - assertNotNull(Request.class.getMethod("addMarker", String.class)); - assertNotNull(Request.class.getDeclaredMethod("finish", String.class)); - assertNotNull(Request.class.getMethod("setRequestQueue", RequestQueue.class)); - assertNotNull(Request.class.getMethod("setSequence", int.class)); - assertNotNull(Request.class.getMethod("getSequence")); - assertNotNull(Request.class.getMethod("getUrl")); - assertNotNull(Request.class.getMethod("getCacheKey")); - assertNotNull(Request.class.getMethod("setCacheEntry", Cache.Entry.class)); - assertNotNull(Request.class.getMethod("getCacheEntry")); - assertNotNull(Request.class.getMethod("cancel")); - assertNotNull(Request.class.getMethod("isCanceled")); - assertNotNull(Request.class.getMethod("getHeaders")); - assertNotNull(Request.class.getDeclaredMethod("getParams")); - assertNotNull(Request.class.getDeclaredMethod("getParamsEncoding")); - assertNotNull(Request.class.getMethod("getBodyContentType")); - assertNotNull(Request.class.getMethod("getBody")); - assertNotNull(Request.class.getMethod("setShouldCache", boolean.class)); - assertNotNull(Request.class.getMethod("shouldCache")); - assertNotNull(Request.class.getMethod("getPriority")); - assertNotNull(Request.class.getMethod("getTimeoutMs")); - assertNotNull(Request.class.getMethod("getRetryPolicy")); - assertNotNull(Request.class.getMethod("markDelivered")); - assertNotNull(Request.class.getMethod("hasHadResponseDelivered")); - assertNotNull( - Request.class.getDeclaredMethod("parseNetworkResponse", NetworkResponse.class)); - assertNotNull(Request.class.getDeclaredMethod("parseNetworkError", VolleyError.class)); - assertNotNull(Request.class.getDeclaredMethod("deliverResponse", Object.class)); - assertNotNull(Request.class.getMethod("deliverError", VolleyError.class)); - } -} diff --git a/src/test/java/com/android/volley/toolbox/ResponseTest.java b/src/test/java/com/android/volley/toolbox/ResponseTest.java deleted file mode 100644 index 44438fa..0000000 --- a/src/test/java/com/android/volley/toolbox/ResponseTest.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertNotNull; - -import com.android.volley.Cache; -import com.android.volley.NetworkResponse; -import com.android.volley.Response; -import com.android.volley.VolleyError; -import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class ResponseTest { - - @Test - public void publicMethods() throws Exception { - // Catch-all test to find API-breaking changes. - assertNotNull(Response.class.getMethod("success", Object.class, Cache.Entry.class)); - assertNotNull(Response.class.getMethod("error", VolleyError.class)); - assertNotNull(Response.class.getMethod("isSuccess")); - - assertNotNull(Response.Listener.class.getDeclaredMethod("onResponse", Object.class)); - - assertNotNull( - Response.ErrorListener.class.getDeclaredMethod( - "onErrorResponse", VolleyError.class)); - - assertNotNull( - NetworkResponse.class.getConstructor( - int.class, byte[].class, Map.class, boolean.class, long.class)); - assertNotNull( - NetworkResponse.class.getConstructor( - int.class, byte[].class, Map.class, boolean.class)); - assertNotNull(NetworkResponse.class.getConstructor(byte[].class)); - assertNotNull(NetworkResponse.class.getConstructor(byte[].class, Map.class)); - } -} diff --git a/src/test/java/com/android/volley/toolbox/StringRequestTest.java b/src/test/java/com/android/volley/toolbox/StringRequestTest.java deleted file mode 100644 index 0ecb06b..0000000 --- a/src/test/java/com/android/volley/toolbox/StringRequestTest.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.toolbox; - -import static org.junit.Assert.assertNotNull; - -import com.android.volley.Response; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class StringRequestTest { - - @Test - public void publicMethods() throws Exception { - // Catch-all test to find API-breaking changes. - assertNotNull( - StringRequest.class.getConstructor( - String.class, Response.Listener.class, Response.ErrorListener.class)); - assertNotNull( - StringRequest.class.getConstructor( - int.class, - String.class, - Response.Listener.class, - Response.ErrorListener.class)); - } -} diff --git a/src/test/java/com/android/volley/utils/CacheTestUtils.java b/src/test/java/com/android/volley/utils/CacheTestUtils.java deleted file mode 100644 index 5980712..0000000 --- a/src/test/java/com/android/volley/utils/CacheTestUtils.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.utils; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; - -import com.android.volley.Cache; -import java.util.Random; - -public class CacheTestUtils { - - /** - * Makes a random cache entry. - * - * @param data Data to use, or null to use random data - * @param isExpired Whether the TTLs should be set such that this entry is expired - * @param needsRefresh Whether the TTLs should be set such that this entry needs refresh - */ - public static Cache.Entry makeRandomCacheEntry( - byte[] data, boolean isExpired, boolean needsRefresh) { - Random random = new Random(); - Cache.Entry entry = new Cache.Entry(); - if (data != null) { - entry.data = data; - } else { - entry.data = new byte[random.nextInt(1024)]; - } - entry.etag = String.valueOf(random.nextLong()); - entry.lastModified = random.nextLong(); - entry.ttl = isExpired ? 0 : Long.MAX_VALUE; - entry.softTtl = needsRefresh ? 0 : Long.MAX_VALUE; - return entry; - } - - /** - * Like {@link #makeRandomCacheEntry(byte[], boolean, boolean)} but defaults to an unexpired - * entry. - */ - public static Cache.Entry makeRandomCacheEntry(byte[] data) { - return makeRandomCacheEntry(data, false, false); - } - - public static void assertThatEntriesAreEqual(Cache.Entry actual, Cache.Entry expected) { - assertNotNull(actual); - assertThat(actual.data, is(equalTo(expected.data))); - assertThat(actual.etag, is(equalTo(expected.etag))); - assertThat(actual.lastModified, is(equalTo(expected.lastModified))); - assertThat(actual.responseHeaders, is(equalTo(expected.responseHeaders))); - assertThat(actual.serverDate, is(equalTo(expected.serverDate))); - assertThat(actual.softTtl, is(equalTo(expected.softTtl))); - assertThat(actual.ttl, is(equalTo(expected.ttl))); - } - - public static Cache.Entry randomData(int length) { - Cache.Entry entry = new Cache.Entry(); - byte[] data = new byte[length]; - new Random(42).nextBytes(data); // explicit seed for reproducible results - entry.data = data; - return entry; - } - - public static int getEntrySizeOnDisk(String key) { - // Header size is: - // 4 bytes for magic int - // 8 + len(key) bytes for key (long length) - // 8 bytes for etag (long length + 0 characters) - // 32 bytes for serverDate, lastModified, ttl, and softTtl longs - // 4 bytes for length of header list int - // == 56 + len(key) bytes total. - return 56 + key.length(); - } -} diff --git a/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java b/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java deleted file mode 100644 index 67e5923..0000000 --- a/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.volley.utils; - -import com.android.volley.ExecutorDelivery; -import java.util.concurrent.Executor; - -/** - * A ResponseDelivery for testing that immediately delivers responses instead of posting back to the - * main thread. - */ -public class ImmediateResponseDelivery extends ExecutorDelivery { - - public ImmediateResponseDelivery() { - super( - new Executor() { - @Override - public void execute(Runnable command) { - command.run(); - } - }); - } -} diff --git a/src/test/resources/org.robolectric.Config.properties b/src/test/resources/org.robolectric.Config.properties deleted file mode 100644 index 9daf692..0000000 --- a/src/test/resources/org.robolectric.Config.properties +++ /dev/null @@ -1 +0,0 @@ -manifest=src/main/AndroidManifest.xml diff --git a/testing/build.gradle b/testing/build.gradle new file mode 100644 index 0000000..b374088 --- /dev/null +++ b/testing/build.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation project(":core") +} + diff --git a/testing/src/main/AndroidManifest.xml b/testing/src/main/AndroidManifest.xml new file mode 100644 index 0000000..24b1376 --- /dev/null +++ b/testing/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/testing/src/main/java/com/android/volley/mock/TestRequest.java b/testing/src/main/java/com/android/volley/mock/TestRequest.java new file mode 100644 index 0000000..f397f01 --- /dev/null +++ b/testing/src/main/java/com/android/volley/mock/TestRequest.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; +import java.util.HashMap; +import java.util.Map; + +public class TestRequest { + private static final String TEST_URL = "http://foo.com"; + + /** Base Request class for testing allowing both the deprecated and new constructor. */ + private static class Base extends Request { + @SuppressWarnings("deprecation") + public Base(String url, Response.ErrorListener listener) { + super(url, listener); + } + + public Base(int method, String url, Response.ErrorListener listener) { + super(method, url, listener); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected void deliverResponse(byte[] response) {} + } + + /** Test example of a GET request in the deprecated style. */ + public static class DeprecatedGet extends Base { + public DeprecatedGet() { + super(TEST_URL, null); + } + } + + /** Test example of a POST request in the deprecated style. */ + public static class DeprecatedPost extends Base { + private final Map mPostParams; + + public DeprecatedPost() { + super(TEST_URL, null); + mPostParams = new HashMap(); + mPostParams.put("requestpost", "foo"); + } + + @Override + protected Map getPostParams() { + return mPostParams; + } + } + + /** Test example of a GET request in the new style. */ + public static class Get extends Base { + public Get() { + super(Method.GET, TEST_URL, null); + } + } + + /** + * Test example of a POST request in the new style. In the new style, it is possible to have a + * POST with no body. + */ + public static class Post extends Base { + public Post() { + super(Method.POST, TEST_URL, null); + } + } + + /** Test example of a POST request in the new style with a body. */ + public static class PostWithBody extends Post { + private final Map mParams; + + public PostWithBody() { + mParams = new HashMap(); + mParams.put("testKey", "testValue"); + } + + @Override + public Map getParams() { + return mParams; + } + } + + /** + * Test example of a PUT request in the new style. In the new style, it is possible to have a + * PUT with no body. + */ + public static class Put extends Base { + public Put() { + super(Method.PUT, TEST_URL, null); + } + } + + /** Test example of a PUT request in the new style with a body. */ + public static class PutWithBody extends Put { + private Map mParams = new HashMap(); + + public PutWithBody() { + mParams = new HashMap(); + mParams.put("testKey", "testValue"); + } + + @Override + public Map getParams() { + return mParams; + } + } + + /** Test example of a DELETE request in the new style. */ + public static class Delete extends Base { + public Delete() { + super(Method.DELETE, TEST_URL, null); + } + } + + /** Test example of a HEAD request in the new style. */ + public static class Head extends Base { + public Head() { + super(Method.HEAD, TEST_URL, null); + } + } + + /** Test example of a OPTIONS request in the new style. */ + public static class Options extends Base { + public Options() { + super(Method.OPTIONS, TEST_URL, null); + } + } + + /** Test example of a TRACE request in the new style. */ + public static class Trace extends Base { + public Trace() { + super(Method.TRACE, TEST_URL, null); + } + } + + /** Test example of a PATCH request in the new style. */ + public static class Patch extends Base { + public Patch() { + super(Method.PATCH, TEST_URL, null); + } + } + + /** Test example of a PATCH request in the new style with a body. */ + public static class PatchWithBody extends Patch { + private Map mParams = new HashMap(); + + public PatchWithBody() { + mParams = new HashMap(); + mParams.put("testKey", "testValue"); + } + + @Override + public Map getParams() { + return mParams; + } + } +} -- cgit v1.2.3