diff options
author | Anonymous <no-reply@google.com> | 2018-10-12 10:06:55 -0700 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2018-10-12 10:06:55 -0700 |
commit | 4c781b3890ccf5cf04d0b9bce92b8851b07498e7 (patch) | |
tree | 26a0438ea15ce7651d1c54cb99f13e2d8d1951dd | |
parent | b878921cbdb28cf454e33abdfd7f1bc82ff476cc (diff) | |
parent | 2b2e1b9d5f84b7a7351f577fc676b61250a3b658 (diff) | |
download | volley-4c781b3890ccf5cf04d0b9bce92b8851b07498e7.tar.gz |
Import of Volley from GitHub to AOSP. am: d998d6e2d2 am: 093983c192
am: 2b2e1b9d5f
Change-Id: I7f14f69e5335d0120d2ae86db50dc96c4135c79f
6 files changed, 232 insertions, 44 deletions
diff --git a/build.gradle b/build.gradle index 537b55b..318d4c0 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ repositories { } group = 'com.android.volley' -version = '1.1.1-SNAPSHOT' +version = '1.2.0-SNAPSHOT' android { compileSdkVersion 25 diff --git a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java index c49588f..75c217f 100644 --- a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java +++ b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java @@ -65,7 +65,7 @@ public class DiskBasedCache implements Cache { private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024; /** High water mark percentage for the cache */ - private static final float HYSTERESIS_FACTOR = 0.9f; + @VisibleForTesting static final float HYSTERESIS_FACTOR = 0.9f; /** Magic number for current version of cache file format. */ private static final int CACHE_MAGIC = 0x20150306; @@ -74,7 +74,9 @@ public class DiskBasedCache implements Cache { * 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. + * @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(File rootDirectory, int maxCacheSizeInBytes) { mRootDirectory = rootDirectory; @@ -166,8 +168,6 @@ public class DiskBasedCache implements Cache { new BufferedInputStream(createInputStream(file)), entrySize); try { CacheHeader entry = CacheHeader.readHeader(cis); - // NOTE: When this entry was put, its size was recorded as data.length, but - // when the entry is initialized below, its size is recorded as file.length() entry.size = entrySize; putEntry(entry.key, entry); } finally { @@ -203,7 +203,14 @@ public class DiskBasedCache implements Cache { /** Puts the entry with the specified key into the cache. */ @Override public synchronized void put(String key, Entry entry) { - pruneIfNeeded(entry.data.length); + // 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)); @@ -216,7 +223,9 @@ public class DiskBasedCache implements Cache { } fos.write(entry.data); fos.close(); + e.size = file.length(); putEntry(key, e); + pruneIfNeeded(); return; } catch (IOException e) { } @@ -256,13 +265,9 @@ public class DiskBasedCache implements Cache { return new File(mRootDirectory, getFilenameForKey(key)); } - /** - * Prunes the cache to fit the amount of bytes specified. - * - * @param neededSpace The amount of bytes we are trying to fit into the cache. - */ - private void pruneIfNeeded(int neededSpace) { - if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) { + /** Prunes the cache to fit the maximum size. */ + private void pruneIfNeeded() { + if (mTotalSize < mMaxCacheSizeInBytes) { return; } if (VolleyLog.DEBUG) { @@ -288,7 +293,7 @@ public class DiskBasedCache implements Cache { iterator.remove(); prunedFiles++; - if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) { + if (mTotalSize < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) { break; } } @@ -331,7 +336,7 @@ public class DiskBasedCache implements Cache { * @param length number of bytes to read * @throws IOException if fails to read all bytes */ - // VisibleForTesting + @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. @@ -343,20 +348,26 @@ public class DiskBasedCache implements Cache { return bytes; } - // VisibleForTesting + @VisibleForTesting InputStream createInputStream(File file) throws FileNotFoundException { return new FileInputStream(file); } - // VisibleForTesting + @VisibleForTesting OutputStream createOutputStream(File file) throws FileNotFoundException { return new FileOutputStream(file); } /** Handles holding onto the cache headers for an entry. */ - // VisibleForTesting + @VisibleForTesting static class CacheHeader { - /** The size of the data identified by this CacheHeader. (This is not serialized to disk. */ + /** + * The size of the data identified by this CacheHeader on disk (both header and data). + * + * <p>Must be set by the caller after it has been calculated. + * + * <p>This is not serialized to disk. + */ long size; /** The key that identifies the cache entry. */ @@ -389,7 +400,7 @@ public class DiskBasedCache implements Cache { long softTtl, List<Header> allResponseHeaders) { this.key = key; - this.etag = ("".equals(etag)) ? null : etag; + this.etag = "".equals(etag) ? null : etag; this.serverDate = serverDate; this.lastModified = lastModified; this.ttl = ttl; @@ -412,7 +423,6 @@ public class DiskBasedCache implements Cache { entry.ttl, entry.softTtl, getAllResponseHeaders(entry)); - size = entry.data.length; } private static List<Header> getAllResponseHeaders(Entry entry) { diff --git a/src/main/java/com/android/volley/toolbox/ImageLoader.java b/src/main/java/com/android/volley/toolbox/ImageLoader.java index 076c212..270935f 100644 --- a/src/main/java/com/android/volley/toolbox/ImageLoader.java +++ b/src/main/java/com/android/volley/toolbox/ImageLoader.java @@ -239,8 +239,11 @@ public class ImageLoader { // 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. + // 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); diff --git a/src/main/java/com/android/volley/toolbox/NetworkImageView.java b/src/main/java/com/android/volley/toolbox/NetworkImageView.java index a490a79..6ad1e49 100644 --- a/src/main/java/com/android/volley/toolbox/NetworkImageView.java +++ b/src/main/java/com/android/volley/toolbox/NetworkImageView.java @@ -14,7 +14,9 @@ package com.android.volley.toolbox; import android.content.Context; +import android.graphics.Bitmap; import android.support.annotation.MainThread; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.AttributeSet; import android.view.ViewGroup.LayoutParams; @@ -28,12 +30,30 @@ 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. */ + /** + * 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 mDefaultImageBitmap. + */ private int mDefaultImageId; - /** Resource ID of the image to be used if the network response fails. */ + /** + * 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. + */ + @Nullable Bitmap mDefaultImageBitmap; + + /** + * Resource ID of the image to be used if the network response fails. Won't be set at the same + * time as 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. + */ + @Nullable private Bitmap mErrorImageBitmap; + /** Local copy of the ImageLoader. */ private ImageLoader mImageLoader; @@ -57,8 +77,10 @@ public class NetworkImageView extends ImageView { * immediately either set the cached image (if available) or the default image specified by * {@link NetworkImageView#setDefaultImageResId(int)} on the view. * - * <p>NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} and {@link - * NetworkImageView#setErrorImageResId(int)} should be called prior to calling this function. + * <p>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. * * <p>Must be called from the main thread. * @@ -77,20 +99,56 @@ public class NetworkImageView extends ImageView { /** * Sets the default image resource ID to be used for this view until the attempt to load it * completes. + * + * <p>Cannot be called with {@link NetworkImageView#setDefaultImageBitmap}. */ public void setDefaultImageResId(int defaultImage) { + if (mDefaultImageBitmap != null) { + throw new IllegalArgumentException("Can't have a default image resource ID and bitmap"); + } mDefaultImageId = defaultImage; } /** + * Sets the default image bitmap to be used for this view until the attempt to load it + * completes. + * + * <p>Cannot be called with {@link NetworkImageView#setDefaultImageResId}. + */ + public void setDefaultImageBitmap(Bitmap defaultImage) { + if (mDefaultImageId != 0) { + throw new IllegalArgumentException("Can't have a default image resource ID and bitmap"); + } + mDefaultImageBitmap = defaultImage; + } + + /** * Sets the error image resource ID to be used for this view in the event that the image * requested fails to load. + * + * <p>Cannot be called with {@link NetworkImageView#setErrorImageBitmap}. */ public void setErrorImageResId(int errorImage) { + if (mErrorImageBitmap != null) { + throw new IllegalArgumentException("Can't have an error image resource ID and bitmap"); + } mErrorImageId = errorImage; } /** + * Sets the error image bitmap to be used for this view in the event that the image requested + * fails to load. + * + * <p>Cannot be called with {@link NetworkImageView#setErrorImageResId}. + */ + public void setErrorImageBitmap(Bitmap errorImage) { + if (mErrorImageId != 0) { + throw new IllegalArgumentException("Can't have an error image resource ID and bitmap"); + } + 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. @@ -152,6 +210,8 @@ public class NetworkImageView extends ImageView { public void onErrorResponse(VolleyError error) { if (mErrorImageId != 0) { setImageResource(mErrorImageId); + } else if (mErrorImageBitmap != null) { + setImageBitmap(mErrorImageBitmap); } } @@ -180,6 +240,8 @@ public class NetworkImageView extends ImageView { setImageBitmap(response.getBitmap()); } else if (mDefaultImageId != 0) { setImageResource(mDefaultImageId); + } else if (mDefaultImageBitmap != null) { + setImageBitmap(mDefaultImageBitmap); } } }, @@ -191,6 +253,8 @@ public class NetworkImageView extends ImageView { private void setDefaultImageOrNull() { if (mDefaultImageId != 0) { setImageResource(mDefaultImageId); + } else if (mDefaultImageBitmap != null) { + setImageBitmap(mDefaultImageBitmap); } else { setImageBitmap(null); } diff --git a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java index fb6392c..e499a37 100644 --- a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java +++ b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java @@ -166,37 +166,134 @@ public class DiskBasedCacheTest { } @Test - public void testTrim() { - Cache.Entry entry = randomData(2 * MAX_SIZE); + public void testTooLargeEntry() { + Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("oversize")); cache.put("oversize", entry); - assertThatEntriesAreEqual(cache.get("oversize"), entry); + assertThat(cache.get("oversize"), is(nullValue())); + } - entry = randomData(1024); - cache.put("kilobyte", entry); + @Test + public void testMaxSizeEntry() { + Cache.Entry entry = randomData(MAX_SIZE - getEntrySizeOnDisk("maxsize") - 1); + cache.put("maxsize", entry); - assertThat(cache.get("oversize"), is(nullValue())); - assertThatEntriesAreEqual(cache.get("kilobyte"), 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); - Cache.Entry entry2 = randomData(1024); - cache.put("kilobyte2", entry2); - Cache.Entry entry3 = randomData(1024); - cache.put("kilobyte3", entry3); + // Now any new entry should cause the first one to be cleared. + entry = randomData(0); + cache.put("bit", entry); - assertThatEntriesAreEqual(cache.get("kilobyte"), entry); - assertThatEntriesAreEqual(cache.get("kilobyte2"), entry2); - assertThatEntriesAreEqual(cache.get("kilobyte3"), entry3); + assertThat(cache.get("goodsize"), is(nullValue())); + assertThatEntriesAreEqual(cache.get("bit"), entry); + } - entry = randomData(MAX_SIZE); + @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("kilobyte"), is(nullValue())); - assertThat(cache.get("kilobyte2"), is(nullValue())); - assertThat(cache.get("kilobyte3"), is(nullValue())); + 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 @@ -518,4 +615,15 @@ public class DiskBasedCacheTest { 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/NetworkImageViewTest.java b/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java index af8fad9..7705a8f 100644 --- a/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java +++ b/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import android.content.Context; +import android.graphics.Bitmap; import android.util.AttributeSet; import android.view.ViewGroup.LayoutParams; import android.widget.ImageView.ScaleType; @@ -89,7 +90,9 @@ public class NetworkImageViewTest { assertNotNull( NetworkImageView.class.getMethod("setImageUrl", String.class, ImageLoader.class)); + assertNotNull(NetworkImageView.class.getMethod("setDefaultImageBitmap", Bitmap.class)); assertNotNull(NetworkImageView.class.getMethod("setDefaultImageResId", int.class)); + assertNotNull(NetworkImageView.class.getMethod("setErrorImageBitmap", Bitmap.class)); assertNotNull(NetworkImageView.class.getMethod("setErrorImageResId", int.class)); } } |