aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnonymous <no-reply@google.com>2018-10-12 10:06:55 -0700
committerandroid-build-merger <android-build-merger@google.com>2018-10-12 10:06:55 -0700
commit4c781b3890ccf5cf04d0b9bce92b8851b07498e7 (patch)
tree26a0438ea15ce7651d1c54cb99f13e2d8d1951dd
parentb878921cbdb28cf454e33abdfd7f1bc82ff476cc (diff)
parent2b2e1b9d5f84b7a7351f577fc676b61250a3b658 (diff)
downloadvolley-4c781b3890ccf5cf04d0b9bce92b8851b07498e7.tar.gz
Import of Volley from GitHub to AOSP. am: d998d6e2d2 am: 093983c192
am: 2b2e1b9d5f Change-Id: I7f14f69e5335d0120d2ae86db50dc96c4135c79f
-rw-r--r--build.gradle2
-rw-r--r--src/main/java/com/android/volley/toolbox/DiskBasedCache.java50
-rw-r--r--src/main/java/com/android/volley/toolbox/ImageLoader.java5
-rw-r--r--src/main/java/com/android/volley/toolbox/NetworkImageView.java72
-rw-r--r--src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java144
-rw-r--r--src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java3
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));
}
}