From ec9c6e0d0e8e5e947017e24c270ca9f13487f937 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 30 Apr 2020 10:56:07 -0700 Subject: Import of Volley from GitHub to AOSP. - c9b2623cb524d2ec29a5a7f012528115f45b24cd Tolerate null header maps in HttpHeaderParser. (#334) by Jeff Davidson - 455a16125ae9d01176d68a2ea7d4747130e55801 Allow creation of custom input/output streams in HurlStac... by Nicolas - addc11dbd37bd3a5d8136557076af0e8d0a995a4 Annotate more nullable methods and fields (#333) by Julien Biral - d41f34a43520cd1d30fe71ada3271161adb0e9c0 Add @Nullable annotations to Response (#325) by justin-morey - 8a3a7baa07dda3fb4c5d561664470311c13d215b Send request to network when cache entry parsing fails (#... by Ulrike Hager - 514eac8c33492f69f62799dcd3669c877474626c Re-initialize the cache if the directory was deleted. (#2... by Ulrike Hager - b95b0159a14f053590cb18fd1cf3a1b159e478a1 Fix timezone formatting for RFC1123 on some Android devic... by Jeff Davidson - 96c88101bcb6139fc9dd2ded4ef7c0008085d4b1 httpheaderparser: log invalid date headers only verbosely... by Florens - 8e7a6dbf4dfbea56da48814ecf6e7270dcec9b0f Allow Volley.newRequestQueue() to be called on main threa... by Yuhan Zhao GitOrigin-RevId: c9b2623cb524d2ec29a5a7f012528115f45b24cd Change-Id: I8e677f68212e6ba0812f398744895a1a12b6ed70 --- src/main/java/com/android/volley/Cache.java | 2 + .../java/com/android/volley/CacheDispatcher.java | 9 ++ .../java/com/android/volley/NetworkResponse.java | 26 ++++-- src/main/java/com/android/volley/Request.java | 5 +- src/main/java/com/android/volley/Response.java | 16 ++-- .../com/android/volley/toolbox/DiskBasedCache.java | 75 ++++++++++++--- .../android/volley/toolbox/HttpHeaderParser.java | 37 ++++++-- .../java/com/android/volley/toolbox/HurlStack.java | 41 +++++++- .../com/android/volley/toolbox/ImageLoader.java | 2 + .../java/com/android/volley/toolbox/Volley.java | 18 +++- .../com/android/volley/CacheDispatcherTest.java | 19 ++++ .../android/volley/toolbox/DiskBasedCacheTest.java | 17 ++++ .../volley/toolbox/HttpHeaderParserTest.java | 9 ++ .../com/android/volley/toolbox/HurlStackTest.java | 104 ++++++++++++++++++--- 14 files changed, 319 insertions(+), 61 deletions(-) diff --git a/src/main/java/com/android/volley/Cache.java b/src/main/java/com/android/volley/Cache.java index 35b2a96..b8908ac 100644 --- a/src/main/java/com/android/volley/Cache.java +++ b/src/main/java/com/android/volley/Cache.java @@ -16,6 +16,7 @@ package com.android.volley; +import androidx.annotation.Nullable; import java.util.Collections; import java.util.List; import java.util.Map; @@ -28,6 +29,7 @@ public interface Cache { * @param key Cache key * @return An {@link Entry} or null in the event of a cache miss */ + @Nullable Entry get(String key); /** diff --git a/src/main/java/com/android/volley/CacheDispatcher.java b/src/main/java/com/android/volley/CacheDispatcher.java index be06d1f..12b1035 100644 --- a/src/main/java/com/android/volley/CacheDispatcher.java +++ b/src/main/java/com/android/volley/CacheDispatcher.java @@ -159,6 +159,15 @@ public class CacheDispatcher extends Thread { 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); diff --git a/src/main/java/com/android/volley/NetworkResponse.java b/src/main/java/com/android/volley/NetworkResponse.java index 01f48c6..cfbc371 100644 --- a/src/main/java/com/android/volley/NetworkResponse.java +++ b/src/main/java/com/android/volley/NetworkResponse.java @@ -16,6 +16,7 @@ package com.android.volley; +import androidx.annotation.Nullable; import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Collections; @@ -42,7 +43,7 @@ public class NetworkResponse { public NetworkResponse( int statusCode, byte[] data, - Map headers, + @Nullable Map headers, boolean notModified, long networkTimeMs) { this(statusCode, data, headers, toAllHeaderList(headers), notModified, networkTimeMs); @@ -62,7 +63,7 @@ public class NetworkResponse { byte[] data, boolean notModified, long networkTimeMs, - List
allHeaders) { + @Nullable List
allHeaders) { this(statusCode, data, toHeaderMap(allHeaders), allHeaders, notModified, networkTimeMs); } @@ -79,7 +80,10 @@ public class NetworkResponse { */ @Deprecated public NetworkResponse( - int statusCode, byte[] data, Map headers, boolean notModified) { + int statusCode, + byte[] data, + @Nullable Map headers, + boolean notModified) { this(statusCode, data, headers, notModified, /* networkTimeMs= */ 0); } @@ -107,7 +111,7 @@ public class NetworkResponse { * constructor may be removed in a future release of Volley. */ @Deprecated - public NetworkResponse(byte[] data, Map headers) { + public NetworkResponse(byte[] data, @Nullable Map headers) { this( HttpURLConnection.HTTP_OK, data, @@ -119,8 +123,8 @@ public class NetworkResponse { private NetworkResponse( int statusCode, byte[] data, - Map headers, - List
allHeaders, + @Nullable Map headers, + @Nullable List
allHeaders, boolean notModified, long networkTimeMs) { this.statusCode = statusCode; @@ -150,10 +154,10 @@ public class NetworkResponse { * map will only contain the last one. Use {@link #allHeaders} to inspect all headers returned * by the server. */ - public final Map headers; + @Nullable public final Map headers; /** All response headers. Must not be mutated directly. */ - public final List
allHeaders; + @Nullable public final List
allHeaders; /** True if the server returned a 304 (Not Modified). */ public final boolean notModified; @@ -161,7 +165,8 @@ public class NetworkResponse { /** Network roundtrip time in milliseconds. */ public final long networkTimeMs; - private static Map toHeaderMap(List
allHeaders) { + @Nullable + private static Map toHeaderMap(@Nullable List
allHeaders) { if (allHeaders == null) { return null; } @@ -176,7 +181,8 @@ public class NetworkResponse { return headers; } - private static List
toAllHeaderList(Map headers) { + @Nullable + private static List
toAllHeaderList(@Nullable Map headers) { if (headers == null) { return null; } diff --git a/src/main/java/com/android/volley/Request.java b/src/main/java/com/android/volley/Request.java index 104b046..2b53f96 100644 --- a/src/main/java/com/android/volley/Request.java +++ b/src/main/java/com/android/volley/Request.java @@ -115,7 +115,7 @@ public abstract class Request implements Comparable> { * 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. */ - private Cache.Entry mCacheEntry = null; + @Nullable private Cache.Entry mCacheEntry = null; /** An opaque token tagging this request; used for bulk cancellation. */ private Object mTag; @@ -319,6 +319,7 @@ public abstract class Request implements Comparable> { } /** Returns the annotated cache entry, or null if there isn't one. */ + @Nullable public Cache.Entry getCacheEntry() { return mCacheEntry; } @@ -374,6 +375,7 @@ public abstract class Request implements Comparable> { * @deprecated Use {@link #getParams()} instead. */ @Deprecated + @Nullable protected Map getPostParams() throws AuthFailureError { return getParams(); } @@ -431,6 +433,7 @@ public abstract class Request implements Comparable> { * * @throws AuthFailureError in the event of auth failure */ + @Nullable protected Map getParams() throws AuthFailureError { return null; } diff --git a/src/main/java/com/android/volley/Response.java b/src/main/java/com/android/volley/Response.java index 2f50e2d..622bdc4 100644 --- a/src/main/java/com/android/volley/Response.java +++ b/src/main/java/com/android/volley/Response.java @@ -16,6 +16,8 @@ package com.android.volley; +import androidx.annotation.Nullable; + /** * Encapsulates a parsed response for delivery. * @@ -39,7 +41,7 @@ public class Response { } /** Returns a successful response containing the parsed result. */ - public static Response success(T result, Cache.Entry cacheEntry) { + public static Response success(@Nullable T result, @Nullable Cache.Entry cacheEntry) { return new Response<>(result, cacheEntry); } @@ -51,14 +53,14 @@ public class Response { return new Response<>(error); } - /** Parsed response, or null in the case of error. */ - public final T result; + /** Parsed response, can be null; always null in the case of error. */ + @Nullable public final T result; - /** Cache metadata for this response, or null in the case of error. */ - public final Cache.Entry cacheEntry; + /** 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. */ - public final VolleyError error; + @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; @@ -68,7 +70,7 @@ public class Response { return error == null; } - private Response(T result, Cache.Entry cacheEntry) { + private Response(@Nullable T result, @Nullable Cache.Entry cacheEntry) { this.result = result; this.cacheEntry = cacheEntry; this.error = null; diff --git a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java index a6a0c83..d4310e0 100644 --- a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java +++ b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java @@ -55,8 +55,8 @@ public class DiskBasedCache implements Cache { /** Total amount of space currently used by the cache in bytes. */ private long mTotalSize = 0; - /** The root directory to use for the cache. */ - private final File mRootDirectory; + /** 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; @@ -78,8 +78,27 @@ public class DiskBasedCache implements Cache { * briefly exceed this size on disk when writing a new entry that pushes it over the limit * until the ensuing pruning completes. */ - public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) { - mRootDirectory = rootDirectory; + 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; } @@ -93,10 +112,20 @@ public class DiskBasedCache implements Cache { 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 = mRootDirectory.listFiles(); + File[] files = mRootDirectorySupplier.get().listFiles(); if (files != null) { for (File file : files) { file.delete(); @@ -150,13 +179,14 @@ public class DiskBasedCache implements Cache { */ @Override public synchronized void initialize() { - if (!mRootDirectory.exists()) { - if (!mRootDirectory.mkdirs()) { - VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath()); + File rootDirectory = mRootDirectorySupplier.get(); + if (!rootDirectory.exists()) { + if (!rootDirectory.mkdirs()) { + VolleyLog.e("Unable to create cache dir %s", rootDirectory.getAbsolutePath()); } return; } - File[] files = mRootDirectory.listFiles(); + File[] files = rootDirectory.listFiles(); if (files == null) { return; } @@ -226,12 +256,12 @@ public class DiskBasedCache implements Cache { e.size = file.length(); putEntry(key, e); pruneIfNeeded(); - return; } catch (IOException e) { - } - boolean deleted = file.delete(); - if (!deleted) { - VolleyLog.d("Could not clean up file %s", file.getAbsolutePath()); + boolean deleted = file.delete(); + if (!deleted) { + VolleyLog.d("Could not clean up file %s", file.getAbsolutePath()); + } + initializeIfRootDirectoryDeleted(); } } @@ -262,7 +292,22 @@ public class DiskBasedCache implements Cache { /** Returns a file object for the given cache key. */ public File getFileForKey(String key) { - return new File(mRootDirectory, getFilenameForKey(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. */ diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java index 27d1268..1b410af 100644 --- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java +++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java @@ -16,6 +16,7 @@ package com.android.volley.toolbox; +import androidx.annotation.Nullable; import com.android.volley.Cache; import com.android.volley.Header; import com.android.volley.NetworkResponse; @@ -37,7 +38,11 @@ public class HttpHeaderParser { private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1"; - private static final String RFC1123_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; + 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}. @@ -45,10 +50,14 @@ public class HttpHeaderParser { * @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; @@ -132,21 +141,29 @@ public class HttpHeaderParser { public static long parseDateAsEpoch(String dateStr) { try { // Parse date in RFC1123 format if this header contains one - return newRfc1123Formatter().parse(dateStr).getTime(); + return newUsGmtFormatter(RFC1123_PARSE_FORMAT).parse(dateStr).getTime(); } catch (ParseException e) { // Date in invalid format, fallback to 0 - VolleyLog.e(e, "Unable to parse dateStr: %s, falling back to 0", dateStr); + // 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 newRfc1123Formatter().format(new Date(epoch)); + return newUsGmtFormatter(RFC1123_OUTPUT_FORMAT).format(new Date(epoch)); } - private static SimpleDateFormat newRfc1123Formatter() { - SimpleDateFormat formatter = new SimpleDateFormat(RFC1123_FORMAT, Locale.US); + private static SimpleDateFormat newUsGmtFormatter(String format) { + SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.US); formatter.setTimeZone(TimeZone.getTimeZone("GMT")); return formatter; } @@ -159,7 +176,11 @@ public class HttpHeaderParser { * @return Returns the charset specified in the Content-Type of this header, or the * defaultCharset if none can be found. */ - public static String parseCharset(Map headers, String defaultCharset) { + 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); @@ -180,7 +201,7 @@ public class HttpHeaderParser { * 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(Map headers) { + public static String parseCharset(@Nullable Map headers) { return parseCharset(headers, DEFAULT_CONTENT_CHARSET); } diff --git a/src/main/java/com/android/volley/toolbox/HurlStack.java b/src/main/java/com/android/volley/toolbox/HurlStack.java index f85d42c..9c38023 100644 --- a/src/main/java/com/android/volley/toolbox/HurlStack.java +++ b/src/main/java/com/android/volley/toolbox/HurlStack.java @@ -25,6 +25,7 @@ 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; @@ -111,7 +112,7 @@ public class HurlStack extends BaseHttpStack { responseCode, convertHeaders(connection.getHeaderFields()), connection.getContentLength(), - new UrlConnectionInputStream(connection)); + createInputStream(request, connection)); } finally { if (!keepConnectionOpen) { connection.disconnect(); @@ -168,6 +169,19 @@ public class HurlStack extends BaseHttpStack { } } + /** + * 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}. * @@ -223,7 +237,7 @@ public class HurlStack extends BaseHttpStack { // 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 */ static void setConnectionParametersForRequest( + /* package */ void setConnectionParametersForRequest( HttpURLConnection connection, Request request) throws IOException, AuthFailureError { switch (request.getMethod()) { case Method.DEPRECATED_GET_OR_POST: @@ -270,7 +284,7 @@ public class HurlStack extends BaseHttpStack { } } - private static void addBodyIfExists(HttpURLConnection connection, Request request) + private void addBodyIfExists(HttpURLConnection connection, Request request) throws IOException, AuthFailureError { byte[] body = request.getBody(); if (body != null) { @@ -278,7 +292,7 @@ public class HurlStack extends BaseHttpStack { } } - private static void addBody(HttpURLConnection connection, Request request, byte[] 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 @@ -289,8 +303,25 @@ public class HurlStack extends BaseHttpStack { connection.setRequestProperty( HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType()); } - DataOutputStream out = new DataOutputStream(connection.getOutputStream()); + 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 index b80072b..eece2cf 100644 --- a/src/main/java/com/android/volley/toolbox/ImageLoader.java +++ b/src/main/java/com/android/volley/toolbox/ImageLoader.java @@ -20,6 +20,7 @@ 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; @@ -70,6 +71,7 @@ public class ImageLoader { * LruCache is recommended. */ public interface ImageCache { + @Nullable Bitmap getBitmap(String url); void putBitmap(String url, Bitmap bitmap); diff --git a/src/main/java/com/android/volley/toolbox/Volley.java b/src/main/java/com/android/volley/toolbox/Volley.java index 1982802..bc65c9c 100644 --- a/src/main/java/com/android/volley/toolbox/Volley.java +++ b/src/main/java/com/android/volley/toolbox/Volley.java @@ -86,8 +86,22 @@ public class Volley { } private static RequestQueue newRequestQueue(Context context, Network network) { - File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR); - RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), 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; } diff --git a/src/test/java/com/android/volley/CacheDispatcherTest.java b/src/test/java/com/android/volley/CacheDispatcherTest.java index 2592a0b..aef6785 100644 --- a/src/test/java/com/android/volley/CacheDispatcherTest.java +++ b/src/test/java/com/android/volley/CacheDispatcherTest.java @@ -140,6 +140,25 @@ public class CacheDispatcherTest { 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 = diff --git a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java index e499a37..ccf68fa 100644 --- a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java +++ b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java @@ -587,11 +587,28 @@ public class DiskBasedCacheTest { 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) { diff --git a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java index 9b670f9..7780c3e 100644 --- a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java +++ b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java @@ -66,6 +66,12 @@ public class HttpHeaderParserTest { 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"); @@ -282,6 +288,9 @@ public class HttpHeaderParserTest { // 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 diff --git a/src/test/java/com/android/volley/toolbox/HurlStackTest.java b/src/test/java/com/android/volley/toolbox/HurlStackTest.java index c1fc92d..7508244 100644 --- a/src/test/java/com/android/volley/toolbox/HurlStackTest.java +++ b/src/test/java/com/android/volley/toolbox/HurlStackTest.java @@ -17,6 +17,7 @@ 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; @@ -24,11 +25,16 @@ 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; @@ -62,6 +68,26 @@ public class HurlStackTest { 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); + } }; } @@ -70,7 +96,7 @@ public class HurlStackTest { TestRequest.DeprecatedGet request = new TestRequest.DeprecatedGet(); assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection, never()).setRequestMethod(anyString()); verify(mMockConnection, never()).setDoOutput(true); } @@ -80,7 +106,7 @@ public class HurlStackTest { TestRequest.DeprecatedPost request = new TestRequest.DeprecatedPost(); assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection).setRequestMethod("POST"); verify(mMockConnection).setDoOutput(true); } @@ -90,7 +116,7 @@ public class HurlStackTest { TestRequest.Get request = new TestRequest.Get(); assertEquals(request.getMethod(), Method.GET); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection).setRequestMethod("GET"); verify(mMockConnection, never()).setDoOutput(true); } @@ -100,7 +126,7 @@ public class HurlStackTest { TestRequest.Post request = new TestRequest.Post(); assertEquals(request.getMethod(), Method.POST); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection).setRequestMethod("POST"); verify(mMockConnection, never()).setDoOutput(true); } @@ -110,7 +136,7 @@ public class HurlStackTest { TestRequest.PostWithBody request = new TestRequest.PostWithBody(); assertEquals(request.getMethod(), Method.POST); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection).setRequestMethod("POST"); verify(mMockConnection).setDoOutput(true); } @@ -120,7 +146,7 @@ public class HurlStackTest { TestRequest.Put request = new TestRequest.Put(); assertEquals(request.getMethod(), Method.PUT); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection).setRequestMethod("PUT"); verify(mMockConnection, never()).setDoOutput(true); } @@ -130,7 +156,7 @@ public class HurlStackTest { TestRequest.PutWithBody request = new TestRequest.PutWithBody(); assertEquals(request.getMethod(), Method.PUT); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection).setRequestMethod("PUT"); verify(mMockConnection).setDoOutput(true); } @@ -140,7 +166,7 @@ public class HurlStackTest { TestRequest.Delete request = new TestRequest.Delete(); assertEquals(request.getMethod(), Method.DELETE); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection).setRequestMethod("DELETE"); verify(mMockConnection, never()).setDoOutput(true); } @@ -150,7 +176,7 @@ public class HurlStackTest { TestRequest.Head request = new TestRequest.Head(); assertEquals(request.getMethod(), Method.HEAD); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection).setRequestMethod("HEAD"); verify(mMockConnection, never()).setDoOutput(true); } @@ -160,7 +186,7 @@ public class HurlStackTest { TestRequest.Options request = new TestRequest.Options(); assertEquals(request.getMethod(), Method.OPTIONS); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection).setRequestMethod("OPTIONS"); verify(mMockConnection, never()).setDoOutput(true); } @@ -170,7 +196,7 @@ public class HurlStackTest { TestRequest.Trace request = new TestRequest.Trace(); assertEquals(request.getMethod(), Method.TRACE); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection).setRequestMethod("TRACE"); verify(mMockConnection, never()).setDoOutput(true); } @@ -180,7 +206,7 @@ public class HurlStackTest { TestRequest.Patch request = new TestRequest.Patch(); assertEquals(request.getMethod(), Method.PATCH); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection).setRequestMethod("PATCH"); verify(mMockConnection, never()).setDoOutput(true); } @@ -190,7 +216,7 @@ public class HurlStackTest { TestRequest.PatchWithBody request = new TestRequest.PatchWithBody(); assertEquals(request.getMethod(), Method.PATCH); - HurlStack.setConnectionParametersForRequest(mMockConnection, request); + mHurlStack.setConnectionParametersForRequest(mMockConnection, request); verify(mMockConnection).setRequestMethod("PATCH"); verify(mMockConnection).setDoOutput(true); } @@ -256,4 +282,56 @@ public class HurlStackTest { 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; + } } -- cgit v1.2.3