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 --- .../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 +++++- 5 files changed, 143 insertions(+), 30 deletions(-) (limited to 'src/main/java/com/android/volley/toolbox') 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; } -- cgit v1.2.3