Import of Volley from GitHub to AOSP.  - 27f4207774f7a0b59168f5b3b53d8c0c3841de64 Merge pull request #71 from google/fix-visibility by Jeff Davidson <jpd236@cornell.edu>
  - e1d75bc49fff2f43b4304688f22ccb57f0c2782c Merge pull request #66 from joebowbeer/dbc_overflow by Jeff Davidson <jpd236@cornell.edu>
  - d40b75f52f0f8e3467d63294142610a040ff8cd5 Merge pull request #64 from xyhuangjinfu/master by Jeff Davidson <jpd236@cornell.edu>
  - aafd2e5d1279d68cd2c39b02a915e501974daf2f Merge pull request #52 from joebowbeer/dbc by Jeff Davidson <jpd236@cornell.edu>
  - baefc4e4f7ad42ce84972716486928ab77b413fc Merge pull request #23 from google/javadoc-fixes by Jeff Davidson <jpd236@cornell.edu>
  - dea97629925acea8942c06a7ff52938eba5dc78c Merge pull request #39 from google/publish by Jeff Davidson <jpd236@cornell.edu>
  - f52e1e0d4a18c8e3e7004a3d60c8cbec9a04f3d4 Merge pull request #38 from SamuelYvon/master by Jeff Davidson <jpd236@cornell.edu>
  - f0ab9a527d3a525325139026a7da0318b506abb4 Merge pull request #41 from LappleApple/patch-1 by Jeff Davidson <jpd236@cornell.edu>
  - a444f81d099179ab7c5fa5f460262a206125cd37 Merge pull request #18 from Ericliu001/master by Jeff Davidson <jpd236@cornell.edu>

GitOrigin-RevId: 27f4207774f7a0b59168f5b3b53d8c0c3841de64
Change-Id: Idf47e2b8caff4c0d4a3f00a5d8c22878e36c59ba
diff --git a/.travis.yml b/.travis.yml
index 5f6fa73..4c80cfd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -25,3 +25,12 @@
     - $HOME/.gradle/caches/
     - $HOME/.gradle/wrapper/
     - $HOME/.android/build-cache
+
+env:
+  global:
+  - secure: pQb1CpD3heCma1831L4rnZGHgn6acFoBMlRrMBWc7nXXM5WwQ4cMSwZ49TVGMMVobA76vKm6dZj4n5ut5/i9amS9Ohlxbpjw5jVp1dqgev6PMupimkYfeTjTVdfGDlian8J7g4kQcVBXghq2WR+rUVh+VlhenE7r7lzEAscWRAKmo0Dzv1E3idI9ik8gkcAdE0ICJmYoq7vxxBhI3XzofHdq0bkzaCzU4P4OSX5HFN8Z4EloTeEeKZj0f02NwS+rrYzhNDs+8619W1gffAjJ9+C0im7iU8axoxMDE9e5dO4PEO03ol/U7F1NNmwFJ+Q78f9qmNw8u4EYnfzUn7nkH+GvYvX9jGlJoNqXPet0eKsJVIShx5eZjpgtUO0ICZKxYuqtKVfj55MPicMLhKFr1VvUTI4noomQVraUAtnI/H0Ia28795WLry4lbMRuYAwCpsmKJKKNQ5sj3dkokpMmn7SH64zNIxKHpAEkrg3UUWh0LUvk7ePn45RAPXS5M0EX0z6LvQEi5dRVfSc2b69CX9vXyqoG/29xwdaYV+BSZjc1zDFN9BXAwDWhfs86ONSmovi5SXoKd/7FWoEAQVX/ZAW93HuScq5pMq7RYbjN4IThqhCC/K6TgwC2PRtPaIK/Rucc9Gvs8Gh5nphabSDzIPoGUEXXPG+lsG10G9qxGKU=
+  - secure: NhFfhYu6WNuFNpJt3GoGMYpWdYv3xfS7FExeEidvIol+GbuglCv/FOVYVFdHnE52+lvmLz1UTdOSvMhbQe6mhL+ZJQYCILH5i3ion92MlH6YWaJisafzBvSrhUjMFzo0LEJwJ9k3FsAARc0QR+GIsyd0un4MrybeZwFX0VzLElxRl5bEXnxXqYjdw7Mzs3fyROnxees1waz6Ksu3v8NPk45ooy633DGl3txVI/05rT89WwtUnvz0ieAAMiOUTOWs6lAkb45hAzdBng+U5YNL/BiK1y+o4PFYCF7nNJ2A2s6qG/hZYiTa3tLR2tgb5sDHQl4KZ2YEVdEGCZ52nY5vNXAWWZIp1thJYOmaVXFHn/YG8+Qpr2+maUHjzlttIOWl41JSN54emyrK5ZbbAKzMOYKZeRcDP41oP9YlqnsPSL2GBbxqG2GAzv5btHXQPTJVpbqwZUOjYM6jetzRifIWisreN8qQ3cWXLe5l/ra0E6TUbOBH2fC0tZwYe6tkmSGnskA81KwmX7qqKdeJvlAYbGS6MylOSlMmrYqmpKCpofatT1/ZYrnZZSPNRZGQt0L+D1LaYMYlEkSqboxb/OCBL+oD6ncsJWT2YYTQSPdqfmUHF8pNi3hCJe04pqVCbaoEYyMrPlZ+PeZnYDuNSC+Moyu+z7Cqikqg2flzH+F0Az0=
+
+# Publish a SNAPSHOT build for all commits to master.
+after_success:
+  - ./publish-snapshot-on-commit.sh
\ No newline at end of file
diff --git a/README.md b/README.md
index 3ea6982..78ca41e 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 ### Volley
 
-Volley is an HTTP library that makes networking for Android apps easier and most
+Volley is an HTTP library that makes networking for Android apps easier and, most
 importantly, faster.
 
-For more about Volley and how to use it, visit the [Android developer training
+For more information about Volley and how to use it, visit the [Android developer training
 page](https://developer.android.com/training/volley/index.html).
diff --git a/bintray-info-template.json b/bintray-info-template.json
new file mode 100644
index 0000000..4021964
--- /dev/null
+++ b/bintray-info-template.json
@@ -0,0 +1,21 @@
+{
+  "package": {
+    "user_org": "android",
+    "repo": "android-utils",
+    "group": "com.android.volley",
+    "name": "com.android.volley.volley",
+    "desc": "Volley Android library",
+    "licenses": ["Apache-2.0"],
+    "website_url": "https://github.com/google/volley",
+    "issue_tracker_url": "https://github.com/google/volley/issues",
+    "vcs_url": "https://github.com/google/volley.git",
+    "labels": ["android", "volley", "network"],
+    "public_download_numbers": true
+  },
+  "version": {
+    "name": "$VERSION$",
+    "desc": "Volley Android library version $VERSION$",
+    "gpgSign": false
+  },
+  "publish": true
+}
\ No newline at end of file
diff --git a/bintray.gradle b/bintray.gradle
index f07693e..8914c1d 100644
--- a/bintray.gradle
+++ b/bintray.gradle
@@ -3,7 +3,7 @@
         jcenter()
     }
     dependencies {
-        classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2"
+        classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.0.0"
     }
 }
 
@@ -12,13 +12,12 @@
 // 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: com.jfrog.bintray.gradle.BintrayPlugin
+apply plugin: org.jfrog.gradle.plugin.artifactory.ArtifactoryPlugin
 apply plugin: 'maven-publish'
 
-project.ext.group = 'com.android.volley'
-project.ext.archivesBaseName = 'volley'
-project.ext.version = '1.0.0'
-project.ext.pomDesc = 'Volley Android library'
+def bintrayInfoFilePath = "$buildDir/outputs/bintray-descriptor.bintray-info.json"
+
+project.ext.version = '1.0.1-SNAPSHOT'
 
 task sourcesJar(type: Jar) {
     classifier = 'sources'
@@ -35,6 +34,16 @@
     from javadoc.destinationDir
 }
 
+task bintrayInfoFile {
+    outputs.file(bintrayInfoFilePath)
+    doLast {
+        println 'Creating bintray-info.json'
+        String fileContent = new File("$rootDir/bintray-info-template.json").getText('UTF-8')
+        fileContent = fileContent.replace('$VERSION$', project.ext.version)
+        ((new File(bintrayInfoFilePath))).write(fileContent)
+    }
+}
+
 artifacts {
     archives javadocJar
     archives sourcesJar
@@ -43,45 +52,36 @@
 publishing {
     publications {
         library(MavenPublication) {
-            groupId project.ext.group
-            artifactId project.ext.archivesBaseName
+            groupId 'com.android.volley'
+            artifactId 'volley'
             version project.ext.version
 
             // Release AAR, Sources, and JavaDoc
             artifact "$buildDir/outputs/aar/volley-release.aar"
             artifact sourcesJar
             artifact javadocJar
-        }
-    }
-}
-
-bintray {
-    user = System.env.BINTRAY_USER
-    key = System.env.BINTRAY_USER_KEY
-
-    publications = [ 'library' ]
-
-    publish = project.hasProperty("release")
-    pkg {
-        userOrg = 'android'
-        repo = 'android-utils'
-        group = project.ext.group
-        name = project.ext.group + '.' + project.ext.archivesBaseName
-        desc = project.ext.pomDesc
-        licenses = [ 'Apache-2.0' ]
-        websiteUrl = 'https://tools.android.com'
-        issueTrackerUrl = 'https://code.google.com/p/android/'
-        vcsUrl = 'https://android.googlesource.com/platform/frameworks/volley.git'
-        labels = ['android', 'volley', 'network']
-        publicDownloadNumbers = true
-
-        version {
-            name = project.ext.version
-            desc = project.ext.pomDesc + ' version ' + project.ext.version
-            gpg {
-                sign = true
-                passphrase = System.env.GPG_PASSPHRASE
+            artifact(bintrayInfoFilePath) {
+                builtBy bintrayInfoFile
+                extension "bintray-info.json"
             }
         }
     }
 }
+
+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'
+    }
+}
\ No newline at end of file
diff --git a/build.xml b/build.xml
deleted file mode 100644
index 219c63c..0000000
--- a/build.xml
+++ /dev/null
@@ -1,92 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project name="volley" default="help">
-
-    <!-- The local.properties file is created and updated by the 'android' tool.
-         It contains the path to the SDK. It should *NOT* be checked into
-         Version Control Systems. -->
-    <property file="local.properties" />
-
-    <!-- The ant.properties file can be created by you. It is only edited by the
-         'android' tool to add properties to it.
-         This is the place to change some Ant specific build properties.
-         Here are some properties you may want to change/update:
-
-         source.dir
-             The name of the source directory. Default is 'src'.
-         out.dir
-             The name of the output directory. Default is 'bin'.
-
-         For other overridable properties, look at the beginning of the rules
-         files in the SDK, at tools/ant/build.xml
-
-         Properties related to the SDK location or the project target should
-         be updated using the 'android' tool with the 'update' action.
-
-         This file is an integral part of the build system for your
-         application and should be checked into Version Control Systems.
-
-         -->
-    <property file="ant.properties" />
-
-    <!-- if sdk.dir was not set from one of the property file, then
-         get it from the ANDROID_HOME env var.
-         This must be done before we load project.properties since
-         the proguard config can use sdk.dir -->
-    <property environment="env" />
-    <condition property="sdk.dir" value="${env.ANDROID_HOME}">
-        <isset property="env.ANDROID_HOME" />
-    </condition>
-
-    <!-- The project.properties file is created and updated by the 'android'
-         tool, as well as ADT.
-
-         This contains project specific properties such as project target, and library
-         dependencies. Lower level build properties are stored in ant.properties
-         (or in .classpath for Eclipse projects).
-
-         This file is an integral part of the build system for your
-         application and should be checked into Version Control Systems. -->
-    <loadproperties srcFile="project.properties" />
-
-    <!-- quick check on sdk.dir -->
-    <fail
-            message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
-            unless="sdk.dir"
-    />
-
-    <!--
-        Import per project custom build rules if present at the root of the project.
-        This is the place to put custom intermediary targets such as:
-            -pre-build
-            -pre-compile
-            -post-compile (This is typically used for code obfuscation.
-                           Compiled code location: ${out.classes.absolute.dir}
-                           If this is not done in place, override ${out.dex.input.absolute.dir})
-            -post-package
-            -post-build
-            -pre-clean
-    -->
-    <import file="custom_rules.xml" optional="true" />
-
-    <!-- Import the actual build file.
-
-         To customize existing targets, there are two options:
-         - Customize only one target:
-             - copy/paste the target into this file, *before* the
-               <import> task.
-             - customize it to your needs.
-         - Customize the whole content of build.xml
-             - copy/paste the content of the rules files (minus the top node)
-               into this file, replacing the <import> task.
-             - customize to your needs.
-
-         ***********************
-         ****** IMPORTANT ******
-         ***********************
-         In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
-         in order to avoid having your file be overridden by tools such as "android update project"
-    -->
-    <!-- version-tag: 1 -->
-    <import file="${sdk.dir}/tools/ant/build.xml" />
-
-</project>
diff --git a/custom_rules.xml b/custom_rules.xml
deleted file mode 100644
index 1b94e5d..0000000
--- a/custom_rules.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project name="volley-rules" default="help">
-
-  <!-- A rule to generate the JAR for inclusion in an Android
-       application. Output file will be bin/volley.jar -->
-  <target name="jar" depends="-compile">
-    <jar destfile="bin/volley.jar"
-         basedir="bin/classes" />
-  </target>
-</project>
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index 7c37e0f..0000000
--- a/pom.xml
+++ /dev/null
@@ -1,168 +0,0 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <groupId>com.android.volley</groupId>
-  <artifactId>volley</artifactId>
-  <version>1.0-SNAPSHOT</version>
-  <packaging>jar</packaging>
-
-  <name>volley</name>
-  <url>http://android.com</url>
-
-  <properties>
-    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-
-    <java.version>1.6</java.version>
-  </properties>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.android</groupId>
-      <artifactId>android</artifactId>
-      <version>4.1.1.4</version>
-    </dependency>
-    <dependency>
-      <groupId>junit</groupId>
-      <artifactId>junit</artifactId>
-      <version>4.10</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.robolectric</groupId>
-      <artifactId>robolectric</artifactId>
-      <version>3.0</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.mockito</groupId>
-       <artifactId>mockito-core</artifactId>
-       <version>1.9.5</version>
-      <scope>test</scope>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <pluginManagement>
-      <plugins>
-        <plugin>
-          <groupId>com.jayway.maven.plugins.android.generation2</groupId>
-          <artifactId>android-maven-plugin</artifactId>
-          <version>3.8.1</version>
-          <configuration>
-            <sdk>
-              <platform>19</platform>
-            </sdk>
-          </configuration>
-        </plugin>
-
-        <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-compiler-plugin</artifactId>
-          <version>3.0</version>
-          <configuration>
-            <source>${java.version}</source>
-            <target>${java.version}</target>
-          </configuration>
-        </plugin>
-      </plugins>
-    </pluginManagement>
-  </build>
-
-  <profiles>
-    <profile>
-      <id>debug</id>
-      <activation>
-        <activeByDefault>true</activeByDefault>
-        <property>
-          <name>performDebugBuild</name>
-          <value>true</value>
-        </property>
-      </activation>
-      <build>
-        <plugins>
-          <plugin>
-            <groupId>org.apache.maven.plugins</groupId>
-            <artifactId>maven-surefire-plugin</artifactId>
-            <version>2.18.1</version>
-            <executions>
-              <execution>
-                <id>default-test</id>
-                <configuration>
-                  <argLine>${surefireArgLine}</argLine>
-                </configuration>
-              </execution>
-            </executions>
-          </plugin>
-          <plugin>
-            <groupId>org.jacoco</groupId>
-            <artifactId>jacoco-maven-plugin</artifactId>
-            <!-- don't upgrade the version. newer versions generate different results
-             see https://github.com/jacoco/jacoco/issues/286 -->
-            <version>0.7.2.201409121644</version>
-            <executions>
-              <execution>
-                <id>pre-unit-test</id>
-                <goals>
-                  <goal>prepare-agent</goal>
-                </goals>
-                <configuration>
-                  <destFile>${project.build.directory}/surefire-reports/jacoco-ut.exec</destFile>
-                  <propertyName>surefireArgLine</propertyName>
-                </configuration>
-              </execution>
-              <execution>
-                <id>jacoco-report</id>
-                <phase>post-integration-test</phase>
-                <goals>
-                  <goal>report</goal>
-                  <goal>check</goal>
-                </goals>
-                <configuration>
-                  <dataFile>${project.build.directory}/surefire-reports/jacoco-ut.exec</dataFile>
-                  <outputDirectory>${project.build.directory}/jacoco-report</outputDirectory>
-                  <rules>
-                    <rule>
-                      <element>BUNDLE</element>
-                      <limits>
-                        <limit>
-                          <counter>INSTRUCTION</counter>
-                          <value>COVEREDRATIO</value>
-                          <minimum>0.40</minimum>
-                        </limit>
-                        <!-- enable this if you want that the build breaks if there is a class without a test -->
-                        <!--
-                        <limit>
-                          <counter>CLASS</counter>
-                          <value>MISSEDCOUNT</value>
-                          <maximum>0</maximum>
-                        </limit>
-                        -->
-                      </limits>
-                    </rule>
-                    <!-- enable this if you want a limit for each java class -->
-                    <!--
-                    <rule>
-                      <element>CLASS</element>
-                      <excludes>
-                        <exclude>*Test</exclude>
-                      </excludes>
-                      <limits>
-                        <limit>
-                          <counter>LINE</counter>
-                          <value>COVEREDRATIO</value>
-                          <minimum>0.10</minimum>
-                        </limit>
-                      </limits>
-                    </rule>
-                    -->
-                  </rules>
-                </configuration>
-              </execution>
-            </executions>
-          </plugin>
-        </plugins>
-      </build>
-    </profile>
-  </profiles>
-</project>
diff --git a/publish-snapshot-on-commit.sh b/publish-snapshot-on-commit.sh
new file mode 100755
index 0000000..0d0e034
--- /dev/null
+++ b/publish-snapshot-on-commit.sh
@@ -0,0 +1,13 @@
+set -eu
+
+if [ "$TRAVIS_REPO_SLUG" == "google/volley" ] && \
+   [ "$TRAVIS_PULL_REQUEST" == "false" ] && \
+   [ "$TRAVIS_BRANCH" == "master" ]; then
+  echo -e "Publishing snapshot build to OJO...\n"
+
+  ./gradlew artifactoryPublish
+
+  echo -e "Published snapshot build to OJO"
+else
+  echo -e "Not publishing snapshot"
+fi
\ No newline at end of file
diff --git a/rules.gradle b/rules.gradle
index c7c0f70..af81ac2 100644
--- a/rules.gradle
+++ b/rules.gradle
@@ -14,17 +14,9 @@
 // Check if the android plugin version supports unit testing.
 if (configurations.findByName("testCompile")) {
   dependencies {
-    testCompile "junit:junit:4.10"
-    testCompile "org.mockito:mockito-core:1.9.5"
+    testCompile "junit:junit:4.12"
+    testCompile "org.hamcrest:hamcrest-library:1.3"
+    testCompile "org.mockito:mockito-core:2.2.29"
     testCompile "org.robolectric:robolectric:3.0"
   }
 }
-
-// TODO(#4): Remove this once Javadoc errors are fixed
-if (JavaVersion.current().isJava8Compatible()) {
-  allprojects {
-    tasks.withType(Javadoc) {
-      options.addStringOption('Xdoclint:none', '-quiet')
-    }
-  }
-}
diff --git a/src/main/java/com/android/volley/Cache.java b/src/main/java/com/android/volley/Cache.java
index f1ec757..8482c22 100644
--- a/src/main/java/com/android/volley/Cache.java
+++ b/src/main/java/com/android/volley/Cache.java
@@ -28,43 +28,43 @@
      * @param key Cache key
      * @return An {@link Entry} or null in the event of a cache miss
      */
-    public Entry get(String key);
+    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.
      */
-    public void put(String key, Entry entry);
+    void put(String key, Entry entry);
 
     /**
      * Performs any potentially long-running actions needed to initialize the cache;
      * will be called from a worker thread.
      */
-    public void initialize();
+    void initialize();
 
     /**
      * Invalidates an entry in the cache.
      * @param key Cache key
      * @param fullExpire True to fully expire the entry, false to soft expire
      */
-    public void invalidate(String key, boolean fullExpire);
+    void invalidate(String key, boolean fullExpire);
 
     /**
      * Removes an entry from the cache.
      * @param key Cache key
      */
-    public void remove(String key);
+    void remove(String key);
 
     /**
      * Empties the cache.
      */
-    public void clear();
+    void clear();
 
     /**
      * Data and metadata for an entry returned by the cache.
      */
-    public static class Entry {
+    class Entry {
         /** The data returned from cache. */
         public byte[] data;
 
diff --git a/src/main/java/com/android/volley/CacheDispatcher.java b/src/main/java/com/android/volley/CacheDispatcher.java
index 18d219b..1e7dfc4 100644
--- a/src/main/java/com/android/volley/CacheDispatcher.java
+++ b/src/main/java/com/android/volley/CacheDispatcher.java
@@ -151,7 +151,6 @@
                 if (mQuit) {
                     return;
                 }
-                continue;
             }
         }
     }
diff --git a/src/main/java/com/android/volley/Network.java b/src/main/java/com/android/volley/Network.java
index ab45830..1e367c8 100644
--- a/src/main/java/com/android/volley/Network.java
+++ b/src/main/java/com/android/volley/Network.java
@@ -26,5 +26,5 @@
      * @return A {@link NetworkResponse} with data and caching metadata; will never be null
      * @throws VolleyError on errors
      */
-    public NetworkResponse performRequest(Request<?> request) throws VolleyError;
+    NetworkResponse performRequest(Request<?> request) throws VolleyError;
 }
diff --git a/src/main/java/com/android/volley/RequestQueue.java b/src/main/java/com/android/volley/RequestQueue.java
index 4324590..0f2e756 100644
--- a/src/main/java/com/android/volley/RequestQueue.java
+++ b/src/main/java/com/android/volley/RequestQueue.java
@@ -40,13 +40,13 @@
 public class RequestQueue {
 
     /** Callback interface for completed requests. */
-    public static interface RequestFinishedListener<T> {
+    public interface RequestFinishedListener<T> {
         /** Called when a request has finished processing. */
-        public void onRequestFinished(Request<T> request);
+        void onRequestFinished(Request<T> request);
     }
 
     /** Used for generating monotonically-increasing sequence numbers for requests. */
-    private AtomicInteger mSequenceGenerator = new AtomicInteger();
+    private final AtomicInteger mSequenceGenerator = new AtomicInteger();
 
     /**
      * Staging area for requests that already have a duplicate request in flight.
@@ -59,7 +59,7 @@
      * </ul>
      */
     private final Map<String, Queue<Request<?>>> mWaitingRequests =
-            new HashMap<String, Queue<Request<?>>>();
+            new HashMap<>();
 
     /**
      * The set of all requests currently being processed by this RequestQueue. A Request
@@ -70,11 +70,11 @@
 
     /** The cache triage queue. */
     private final PriorityBlockingQueue<Request<?>> mCacheQueue =
-        new PriorityBlockingQueue<Request<?>>();
+            new PriorityBlockingQueue<>();
 
     /** The queue of requests that are actually going out to the network. */
     private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
-        new PriorityBlockingQueue<Request<?>>();
+            new PriorityBlockingQueue<>();
 
     /** Number of network request dispatcher threads to start. */
     private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
@@ -89,13 +89,13 @@
     private final ResponseDelivery mDelivery;
 
     /** The network dispatchers. */
-    private NetworkDispatcher[] mDispatchers;
+    private final NetworkDispatcher[] mDispatchers;
 
     /** The cache dispatcher. */
     private CacheDispatcher mCacheDispatcher;
 
-    private List<RequestFinishedListener> mFinishedListeners =
-            new ArrayList<RequestFinishedListener>();
+    private final List<RequestFinishedListener> mFinishedListeners =
+            new ArrayList<>();
 
     /**
      * Creates the worker pool. Processing will not begin until {@link #start()} is called.
@@ -160,9 +160,9 @@
         if (mCacheDispatcher != null) {
             mCacheDispatcher.quit();
         }
-        for (int i = 0; i < mDispatchers.length; i++) {
-            if (mDispatchers[i] != null) {
-                mDispatchers[i].quit();
+        for (final NetworkDispatcher mDispatcher : mDispatchers) {
+            if (mDispatcher != null) {
+                mDispatcher.quit();
             }
         }
     }
@@ -186,7 +186,7 @@
      * {@link RequestQueue#cancelAll(RequestFilter)}.
      */
     public interface RequestFilter {
-        public boolean apply(Request<?> request);
+        boolean apply(Request<?> request);
     }
 
     /**
@@ -248,7 +248,7 @@
                 // There is already a request in flight. Queue up.
                 Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
                 if (stagedRequests == null) {
-                    stagedRequests = new LinkedList<Request<?>>();
+                    stagedRequests = new LinkedList<>();
                 }
                 stagedRequests.add(request);
                 mWaitingRequests.put(cacheKey, stagedRequests);
diff --git a/src/main/java/com/android/volley/Response.java b/src/main/java/com/android/volley/Response.java
index 1165595..1fe7215 100644
--- a/src/main/java/com/android/volley/Response.java
+++ b/src/main/java/com/android/volley/Response.java
@@ -26,7 +26,7 @@
     /** Callback interface for delivering parsed responses. */
     public interface Listener<T> {
         /** Called when a response is received. */
-        public void onResponse(T response);
+        void onResponse(T response);
     }
 
     /** Callback interface for delivering error responses. */
@@ -35,7 +35,7 @@
          * Callback method that an error has been occurred with the
          * provided error code and optional user-readable message.
          */
-        public void onErrorResponse(VolleyError error);
+        void onErrorResponse(VolleyError error);
     }
 
     /** Returns a successful response containing the parsed result. */
diff --git a/src/main/java/com/android/volley/ResponseDelivery.java b/src/main/java/com/android/volley/ResponseDelivery.java
index 87706af..bef3df5 100644
--- a/src/main/java/com/android/volley/ResponseDelivery.java
+++ b/src/main/java/com/android/volley/ResponseDelivery.java
@@ -20,16 +20,16 @@
     /**
      * Parses a response from the network or cache and delivers it.
      */
-    public void postResponse(Request<?> request, Response<?> response);
+    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.
      */
-    public void postResponse(Request<?> request, Response<?> response, Runnable runnable);
+    void postResponse(Request<?> request, Response<?> response, Runnable runnable);
 
     /**
      * Posts an error for the given request.
      */
-    public void postError(Request<?> request, VolleyError error);
+    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
index 0dd198b..f58678d 100644
--- a/src/main/java/com/android/volley/RetryPolicy.java
+++ b/src/main/java/com/android/volley/RetryPolicy.java
@@ -24,12 +24,12 @@
     /**
      * Returns the current timeout (used for logging).
      */
-    public int getCurrentTimeout();
+    int getCurrentTimeout();
 
     /**
      * Returns the current retry count (used for logging).
      */
-    public int getCurrentRetryCount();
+    int getCurrentRetryCount();
 
     /**
      * Prepares for the next retry by applying a backoff to the timeout.
@@ -37,5 +37,5 @@
      * @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.
      */
-    public void retry(VolleyError error) throws VolleyError;
+    void retry(VolleyError error) throws VolleyError;
 }
diff --git a/src/main/java/com/android/volley/VolleyLog.java b/src/main/java/com/android/volley/VolleyLog.java
index ffe9eb8..fc776e5 100644
--- a/src/main/java/com/android/volley/VolleyLog.java
+++ b/src/main/java/com/android/volley/VolleyLog.java
@@ -25,8 +25,8 @@
 
 /**
  * Logging helper class.
- * <p/>
- * to see Volley logs call:<br/>
+ * <p>
+ * to see Volley logs call:<br>
  * {@code <android-sdk>/platform-tools/adb shell setprop log.tag.Volley VERBOSE}
  */
 public class VolleyLog {
@@ -37,9 +37,9 @@
     /**
      * Customize the log tag for your application, so that other apps
      * using Volley don't mix their logs with yours.
-     * <br />
+     * <br>
      * Enable the log property for your tag before starting your app:
-     * <br />
+     * <br>
      * {@code adb shell setprop log.tag.&lt;tag&gt;}
      */
     public static void setTag(String tag) {
diff --git a/src/main/java/com/android/volley/toolbox/Authenticator.java b/src/main/java/com/android/volley/toolbox/Authenticator.java
index d9f5e3c..adfc996 100644
--- a/src/main/java/com/android/volley/toolbox/Authenticator.java
+++ b/src/main/java/com/android/volley/toolbox/Authenticator.java
@@ -27,10 +27,10 @@
      *
      * @throws AuthFailureError If authentication did not succeed
      */
-    public String getAuthToken() throws AuthFailureError;
+    String getAuthToken() throws AuthFailureError;
 
     /**
      * Invalidates the provided auth token.
      */
-    public void invalidateAuthToken(String authToken);
+    void invalidateAuthToken(String authToken);
 }
diff --git a/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
index 37c35ec..96fb66e 100644
--- a/src/main/java/com/android/volley/toolbox/BasicNetwork.java
+++ b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
@@ -57,9 +57,9 @@
 public class BasicNetwork implements Network {
     protected static final boolean DEBUG = VolleyLog.DEBUG;
 
-    private static int SLOW_REQUEST_THRESHOLD_MS = 3000;
+    private static final int SLOW_REQUEST_THRESHOLD_MS = 3000;
 
-    private static int DEFAULT_POOL_SIZE = 4096;
+    private static final int DEFAULT_POOL_SIZE = 4096;
 
     protected final HttpStack mHttpStack;
 
@@ -257,7 +257,7 @@
             } catch (IOException e) {
                 // This can happen if there was an exception above that left the entity in
                 // an invalid state.
-                VolleyLog.v("Error occured when calling consumingContent");
+                VolleyLog.v("Error occurred when calling consumingContent");
             }
             mPool.returnBuf(buffer);
             bytes.close();
@@ -265,7 +265,7 @@
     }
 
     /**
-     * Converts Headers[] to Map<String, String>.
+     * Converts Headers[] to Map&lt;String, String&gt;.
      */
     protected static Map<String, String> convertHeaders(Header[] headers) {
         Map<String, String> result = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
diff --git a/src/main/java/com/android/volley/toolbox/ByteArrayPool.java b/src/main/java/com/android/volley/toolbox/ByteArrayPool.java
index af95076..c8ca2c2 100644
--- a/src/main/java/com/android/volley/toolbox/ByteArrayPool.java
+++ b/src/main/java/com/android/volley/toolbox/ByteArrayPool.java
@@ -53,8 +53,8 @@
  */
 public class ByteArrayPool {
     /** The buffer pool, arranged both by last use and by buffer size */
-    private List<byte[]> mBuffersByLastUse = new LinkedList<byte[]>();
-    private List<byte[]> mBuffersBySize = new ArrayList<byte[]>(64);
+    private final List<byte[]> mBuffersByLastUse = new LinkedList<byte[]>();
+    private final List<byte[]> mBuffersBySize = new ArrayList<byte[]>(64);
 
     /** The total size of the buffers in the pool */
     private int mCurrentSize = 0;
diff --git a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
index f724d72..0e65183 100644
--- a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
+++ b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
@@ -17,15 +17,18 @@
 package com.android.volley.toolbox;
 
 import android.os.SystemClock;
+import android.text.TextUtils;
 
 import com.android.volley.Cache;
 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;
@@ -110,30 +113,31 @@
         if (entry == null) {
             return null;
         }
-
         File file = getFileForKey(key);
-        CountingInputStream cis = null;
         try {
-            cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
-            CacheHeader.readHeader(cis); // eat header
-            byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
-            return entry.toCacheEntry(data);
+            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;
-        }  catch (NegativeArraySizeException e) {
-            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
-            remove(key);
-            return null;
-        } finally {
-            if (cis != null) {
-                try {
-                    cis.close();
-                } catch (IOException ioe) {
-                    return null;
-                }
-            }
         }
     }
 
@@ -149,28 +153,29 @@
             }
             return;
         }
-
         File[] files = mRootDirectory.listFiles();
         if (files == null) {
             return;
         }
         for (File file : files) {
-            BufferedInputStream fis = null;
             try {
-                fis = new BufferedInputStream(new FileInputStream(file));
-                CacheHeader entry = CacheHeader.readHeader(fis);
-                entry.size = file.length();
-                putEntry(entry.key, entry);
-            } catch (IOException e) {
-                if (file != null) {
-                   file.delete();
-                }
-            } finally {
+                long entrySize = file.length();
+                CountingInputStream cis = new CountingInputStream(
+                        new BufferedInputStream(createInputStream(file)), entrySize);
                 try {
-                    if (fis != null) {
-                        fis.close();
-                    }
-                } catch (IOException ignored) { }
+                    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 {
+                    // Any IOException thrown here is handled by the below catch block by design.
+                    //noinspection ThrowFromFinallyBlock
+                    cis.close();
+                }
+            } catch (IOException e) {
+                //noinspection ResultOfMethodCallIgnored
+                file.delete();
             }
         }
     }
@@ -190,7 +195,6 @@
             }
             put(key, entry);
         }
-
     }
 
     /**
@@ -201,7 +205,7 @@
         pruneIfNeeded(entry.data.length);
         File file = getFileForKey(key);
         try {
-            BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
+            BufferedOutputStream fos = new BufferedOutputStream(createOutputStream(file));
             CacheHeader e = new CacheHeader(key, entry);
             boolean success = e.writeHeader(fos);
             if (!success) {
@@ -313,107 +317,118 @@
      * Removes the entry identified by 'key' from the cache.
      */
     private void removeEntry(String key) {
-        CacheHeader entry = mEntries.get(key);
-        if (entry != null) {
-            mTotalSize -= entry.size;
-            mEntries.remove(key);
+        CacheHeader removed = mEntries.remove(key);
+        if (removed != null) {
+            mTotalSize -= removed.size;
         }
     }
 
     /**
-     * Reads the contents of an InputStream into a byte[].
-     * */
-    private static byte[] streamToBytes(InputStream in, int length) throws IOException {
-        byte[] bytes = new byte[length];
-        int count;
-        int pos = 0;
-        while (pos < length && ((count = in.read(bytes, pos, length - pos)) != -1)) {
-            pos += count;
+     * 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);
         }
-        if (pos != length) {
-            throw new IOException("Expected " + length + " bytes, read " + pos + " bytes");
-        }
+        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.
      */
-    // Visible for testing.
+    //VisibleForTesting
     static class CacheHeader {
         /** The size of the data identified by this CacheHeader. (This is not
          * serialized to disk. */
-        public long size;
+        long size;
 
         /** The key that identifies the cache entry. */
-        public String key;
+        final String key;
 
         /** ETag for cache coherence. */
-        public String etag;
+        final String etag;
 
         /** Date of this response as reported by the server. */
-        public long serverDate;
+        final long serverDate;
 
         /** The last modified date for the requested object. */
-        public long lastModified;
+        final long lastModified;
 
         /** TTL for this record. */
-        public long ttl;
+        final long ttl;
 
         /** Soft TTL for this record. */
-        public long softTtl;
+        final long softTtl;
 
         /** Headers from the response resulting in this cache entry. */
-        public Map<String, String> responseHeaders;
+        final Map<String, String> responseHeaders;
 
-        private CacheHeader() { }
+        private CacheHeader(String key, String etag, long serverDate, long lastModified, long ttl,
+                           long softTtl, Map<String, String> responseHeaders) {
+            this.key = key;
+            this.etag = ("".equals(etag)) ? null : etag;
+            this.serverDate = serverDate;
+            this.lastModified = lastModified;
+            this.ttl = ttl;
+            this.softTtl = softTtl;
+            this.responseHeaders = responseHeaders;
+        }
 
         /**
          * Instantiates a new CacheHeader object
          * @param key The key that identifies the cache entry
          * @param entry The cache entry.
          */
-        public CacheHeader(String key, Entry entry) {
-            this.key = key;
-            this.size = entry.data.length;
-            this.etag = entry.etag;
-            this.serverDate = entry.serverDate;
-            this.lastModified = entry.lastModified;
-            this.ttl = entry.ttl;
-            this.softTtl = entry.softTtl;
-            this.responseHeaders = entry.responseHeaders;
+        CacheHeader(String key, Entry entry) {
+            this(key, entry.etag, entry.serverDate, entry.lastModified, entry.ttl, entry.softTtl,
+                    entry.responseHeaders);
+            size = entry.data.length;
         }
 
         /**
-         * Reads the header off of an InputStream and returns a CacheHeader object.
+         * Reads the header from a CountingInputStream and returns a CacheHeader object.
          * @param is The InputStream to read from.
-         * @throws IOException
+         * @throws IOException if fails to read header
          */
-        public static CacheHeader readHeader(InputStream is) throws IOException {
-            CacheHeader entry = new CacheHeader();
+        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();
             }
-            entry.key = readString(is);
-            entry.etag = readString(is);
-            if (entry.etag.equals("")) {
-                entry.etag = null;
-            }
-            entry.serverDate = readLong(is);
-            entry.lastModified = readLong(is);
-            entry.ttl = readLong(is);
-            entry.softTtl = readLong(is);
-            entry.responseHeaders = readStringStringMap(is);
-
-            return entry;
+            String key = readString(is);
+            String etag = readString(is);
+            long serverDate = readLong(is);
+            long lastModified = readLong(is);
+            long ttl = readLong(is);
+            long softTtl = readLong(is);
+            Map<String, String> responseHeaders = readStringStringMap(is);
+            return new CacheHeader(
+                    key, etag, serverDate, lastModified, ttl, softTtl, responseHeaders);
         }
 
         /**
          * Creates a cache entry for the specified data.
          */
-        public Entry toCacheEntry(byte[] data) {
+        Entry toCacheEntry(byte[] data) {
             Entry e = new Entry();
             e.data = data;
             e.etag = etag;
@@ -429,7 +444,7 @@
         /**
          * Writes the contents of this CacheHeader to the specified OutputStream.
          */
-        public boolean writeHeader(OutputStream os) {
+        boolean writeHeader(OutputStream os) {
             try {
                 writeInt(os, CACHE_MAGIC);
                 writeString(os, key);
@@ -446,14 +461,16 @@
                 return false;
             }
         }
-
     }
 
-    private static class CountingInputStream extends FilterInputStream {
-        private int bytesRead = 0;
+    //VisibleForTesting
+    static class CountingInputStream extends FilterInputStream {
+        private final long length;
+        private long bytesRead;
 
-        private CountingInputStream(InputStream in) {
+        CountingInputStream(InputStream in, long length) {
             super(in);
+            this.length = length;
         }
 
         @Override
@@ -473,6 +490,15 @@
             }
             return result;
         }
+
+        //VisibleForTesting
+        long bytesRead() {
+            return bytesRead;
+        }
+
+        long bytesRemaining() {
+            return length - bytesRead;
+        }
     }
 
     /*
@@ -480,6 +506,8 @@
      * 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.
      */
 
     /**
@@ -540,9 +568,9 @@
         os.write(b, 0, b.length);
     }
 
-    static String readString(InputStream is) throws IOException {
-        int n = (int) readLong(is);
-        byte[] b = streamToBytes(is, n);
+    static String readString(CountingInputStream cis) throws IOException {
+        long n = readLong(cis);
+        byte[] b = streamToBytes(cis, n);
         return new String(b, "UTF-8");
     }
 
@@ -558,18 +586,17 @@
         }
     }
 
-    static Map<String, String> readStringStringMap(InputStream is) throws IOException {
-        int size = readInt(is);
+    static Map<String, String> readStringStringMap(CountingInputStream cis) throws IOException {
+        int size = readInt(cis);
         Map<String, String> result = (size == 0)
                 ? Collections.<String, String>emptyMap()
                 : new HashMap<String, String>(size);
         for (int i = 0; i < size; i++) {
-            String key = readString(is).intern();
-            String value = readString(is).intern();
+            String key = readString(cis).intern();
+            String value = readString(cis).intern();
             result.put(key, value);
         }
         return result;
     }
 
-
 }
diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
index c3b48d8..f53063c 100644
--- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
+++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
@@ -31,7 +31,7 @@
 public class HttpHeaderParser {
 
     /**
-     * Extracts a {@link Cache.Entry} from a {@link NetworkResponse}.
+     * 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.
diff --git a/src/main/java/com/android/volley/toolbox/HttpStack.java b/src/main/java/com/android/volley/toolbox/HttpStack.java
index a52fd06..06f6017 100644
--- a/src/main/java/com/android/volley/toolbox/HttpStack.java
+++ b/src/main/java/com/android/volley/toolbox/HttpStack.java
@@ -39,7 +39,7 @@
      *         {@link Request#getHeaders()}
      * @return the HTTP response
      */
-    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+    HttpResponse performRequest(Request<?> request, Map<String, String> 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
index c53d5e0..66f441d 100644
--- a/src/main/java/com/android/volley/toolbox/HurlStack.java
+++ b/src/main/java/com/android/volley/toolbox/HurlStack.java
@@ -59,7 +59,7 @@
          * Returns a URL to use instead of the provided one, or null to indicate
          * this URL should not be used at all.
          */
-        public String rewriteUrl(String originalUrl);
+        String rewriteUrl(String originalUrl);
     }
 
     private final UrlRewriter mUrlRewriter;
@@ -209,16 +209,8 @@
                 // GET.  Otherwise, it is assumed that the request is a POST.
                 byte[] postBody = request.getPostBody();
                 if (postBody != null) {
-                    // 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);
                     connection.setRequestMethod("POST");
-                    connection.addRequestProperty(HEADER_CONTENT_TYPE,
-                            request.getPostBodyContentType());
-                    DataOutputStream out = new DataOutputStream(connection.getOutputStream());
-                    out.write(postBody);
-                    out.close();
+                    addBody(connection, request, postBody);
                 }
                 break;
             case Method.GET:
@@ -259,11 +251,19 @@
             throws IOException, AuthFailureError {
         byte[] body = request.getBody();
         if (body != null) {
-            connection.setDoOutput(true);
-            connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType());
-            DataOutputStream out = new DataOutputStream(connection.getOutputStream());
-            out.write(body);
-            out.close();
+            addBody(connection, request, body);
         }
     }
+
+    private static void addBody(HttpURLConnection connection, Request<?> request, byte[] body)
+            throws IOException, AuthFailureError {
+        // 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);
+        connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType());
+        DataOutputStream out = new DataOutputStream(connection.getOutputStream());
+        out.write(body);
+        out.close();
+    }
 }
diff --git a/src/main/java/com/android/volley/toolbox/ImageLoader.java b/src/main/java/com/android/volley/toolbox/ImageLoader.java
index d5305e3..33a119b 100644
--- a/src/main/java/com/android/volley/toolbox/ImageLoader.java
+++ b/src/main/java/com/android/volley/toolbox/ImageLoader.java
@@ -72,8 +72,8 @@
      * must not block. Implementation with an LruCache is recommended.
      */
     public interface ImageCache {
-        public Bitmap getBitmap(String url);
-        public void putBitmap(String url, Bitmap bitmap);
+        Bitmap getBitmap(String url);
+        void putBitmap(String url, Bitmap bitmap);
     }
 
     /**
@@ -139,7 +139,7 @@
          * image loading in order to, for example, run an animation to fade in network loaded
          * images.
          */
-        public void onResponse(ImageContainer response, boolean isImmediate);
+        void onResponse(ImageContainer response, boolean isImmediate);
     }
 
     /**
diff --git a/src/main/java/com/android/volley/toolbox/ImageRequest.java b/src/main/java/com/android/volley/toolbox/ImageRequest.java
index d663f5f..0f33cd8 100644
--- a/src/main/java/com/android/volley/toolbox/ImageRequest.java
+++ b/src/main/java/com/android/volley/toolbox/ImageRequest.java
@@ -46,7 +46,7 @@
     private final Config mDecodeConfig;
     private final int mMaxWidth;
     private final int mMaxHeight;
-    private ScaleType mScaleType;
+    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();
@@ -216,7 +216,9 @@
 
     @Override
     protected void deliverResponse(Bitmap response) {
-        mListener.onResponse(response);
+        if (mListener != null) {
+            mListener.onResponse(response);
+        }
     }
 
     /**
diff --git a/src/main/java/com/android/volley/toolbox/JsonRequest.java b/src/main/java/com/android/volley/toolbox/JsonRequest.java
index 2d58f40..40877b1 100644
--- a/src/main/java/com/android/volley/toolbox/JsonRequest.java
+++ b/src/main/java/com/android/volley/toolbox/JsonRequest.java
@@ -63,7 +63,9 @@
 
     @Override
     protected void deliverResponse(T response) {
-        mListener.onResponse(response);
+        if (mListener != null) {
+            mListener.onResponse(response);
+        }
     }
 
     @Override
diff --git a/src/main/java/com/android/volley/toolbox/NetworkImageView.java b/src/main/java/com/android/volley/toolbox/NetworkImageView.java
index 324dbc0..60e4815 100644
--- a/src/main/java/com/android/volley/toolbox/NetworkImageView.java
+++ b/src/main/java/com/android/volley/toolbox/NetworkImageView.java
@@ -147,7 +147,9 @@
 
         // The pre-existing content of this view didn't match the current URL. Load the new image
         // from the network.
-        ImageContainer newContainer = mImageLoader.get(mUrl,
+
+        // update the ImageContainer to be the new bitmap container.
+        mImageContainer = mImageLoader.get(mUrl,
                 new ImageListener() {
                     @Override
                     public void onErrorResponse(VolleyError error) {
@@ -179,9 +181,6 @@
                         }
                     }
                 }, maxWidth, maxHeight, scaleType);
-
-        // update the ImageContainer to be the new bitmap container.
-        mImageContainer = newContainer;
     }
 
     private void setDefaultImageOrNull() {
diff --git a/src/main/java/com/android/volley/toolbox/StringRequest.java b/src/main/java/com/android/volley/toolbox/StringRequest.java
index 6b3dfcf..05a62f6 100644
--- a/src/main/java/com/android/volley/toolbox/StringRequest.java
+++ b/src/main/java/com/android/volley/toolbox/StringRequest.java
@@ -57,7 +57,9 @@
 
     @Override
     protected void deliverResponse(String response) {
-        mListener.onResponse(response);
+        if (mListener != null) {
+            mListener.onResponse(response);
+        }
     }
 
     @Override
diff --git a/src/test/java/com/android/volley/mock/TestRequest.java b/src/test/java/com/android/volley/mock/TestRequest.java
index dfc4dc1..16bf79e 100644
--- a/src/test/java/com/android/volley/mock/TestRequest.java
+++ b/src/test/java/com/android/volley/mock/TestRequest.java
@@ -56,7 +56,7 @@
 
     /** Test example of a POST request in the deprecated style. */
     public static class DeprecatedPost extends Base {
-        private Map<String, String> mPostParams;
+        private final Map<String, String> mPostParams;
 
         public DeprecatedPost() {
             super(TEST_URL, null);
@@ -89,7 +89,7 @@
 
     /** Test example of a POST request in the new style with a body. */
     public static class PostWithBody extends Post {
-        private Map<String, String> mParams;
+        private final Map<String, String> mParams;
 
         public PostWithBody() {
             mParams = new HashMap<String, String>();
diff --git a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
index 0a8be77..3d8d1f1 100644
--- a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
+++ b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
@@ -18,46 +18,371 @@
 
 import com.android.volley.Cache;
 import com.android.volley.toolbox.DiskBasedCache.CacheHeader;
+import com.android.volley.toolbox.DiskBasedCache.CountingInputStream;
+
+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;
 
 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.HashMap;
 import java.util.Map;
+import java.util.Random;
 
-import static org.junit.Assert.*;
+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;
 
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest="src/main/AndroidManifest.xml", sdk=16)
 public class DiskBasedCacheTest {
 
-    // Simple end-to-end serialize/deserialize test.
-    @Test public void cacheHeaderSerialization() throws Exception {
-        Cache.Entry e = new Cache.Entry();
-        e.data = new byte[8];
-        e.serverDate = 1234567L;
-        e.lastModified = 13572468L;
-        e.ttl = 9876543L;
-        e.softTtl = 8765432L;
-        e.etag = "etag";
-        e.responseHeaders = new HashMap<String, String>();
-        e.responseHeaders.put("fruit", "banana");
+    private static final int MAX_SIZE = 1024 * 1024;
 
-        CacheHeader first = new CacheHeader("my-magical-key", e);
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        first.writeHeader(baos);
-        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-        CacheHeader second = CacheHeader.readHeader(bais);
+    private Cache cache;
 
-        assertEquals(first.key, second.key);
-        assertEquals(first.serverDate, second.serverDate);
-        assertEquals(first.lastModified, second.lastModified);
-        assertEquals(first.ttl, second.ttl);
-        assertEquals(first.softTtl, second.softTtl);
-        assertEquals(first.etag, second.etag);
-        assertEquals(first.responseHeaders, second.responseHeaders);
+    @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();
     }
 
-    @Test public void serializeInt() throws Exception {
+    @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 testTrim() {
+        Cache.Entry entry = randomData(2 * MAX_SIZE);
+        cache.put("oversize", entry);
+
+        assertThatEntriesAreEqual(cache.get("oversize"), entry);
+
+        entry = randomData(1024);
+        cache.put("kilobyte", entry);
+
+        assertThat(cache.get("oversize"), is(nullValue()));
+        assertThatEntriesAreEqual(cache.get("kilobyte"), entry);
+
+        Cache.Entry entry2 = randomData(1024);
+        cache.put("kilobyte2", entry2);
+        Cache.Entry entry3 = randomData(1024);
+        cache.put("kilobyte3", entry3);
+
+        assertThatEntriesAreEqual(cache.get("kilobyte"), entry);
+        assertThatEntriesAreEqual(cache.get("kilobyte2"), entry2);
+        assertThatEntriesAreEqual(cache.get("kilobyte3"), entry3);
+
+        entry = randomData(MAX_SIZE);
+        cache.put("max", entry);
+
+        assertThat(cache.get("kilobyte"), is(nullValue()));
+        assertThat(cache.get("kilobyte2"), is(nullValue()));
+        assertThat(cache.get("kilobyte3"), is(nullValue()));
+        assertThatEntriesAreEqual(cache.get("max"), 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 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);
@@ -96,33 +421,35 @@
         DiskBasedCache.writeString(baos, "");
         DiskBasedCache.writeString(baos, "This is a string.");
         DiskBasedCache.writeString(baos, "ファイカス");
-        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-        assertEquals(DiskBasedCache.readString(bais), "");
-        assertEquals(DiskBasedCache.readString(bais), "This is a string.");
-        assertEquals(DiskBasedCache.readString(bais), "ファイカス");
+        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 serializeMap() throws Exception {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        Map<String, String> empty = new HashMap<String, String>();
+        Map<String, String> empty = new HashMap<>();
         DiskBasedCache.writeStringStringMap(empty, baos);
         DiskBasedCache.writeStringStringMap(null, baos);
-        Map<String, String> twoThings = new HashMap<String, String>();
+        Map<String, String> twoThings = new HashMap<>();
         twoThings.put("first", "thing");
         twoThings.put("second", "item");
         DiskBasedCache.writeStringStringMap(twoThings, baos);
-        Map<String, String> emptyKey = new HashMap<String, String>();
+        Map<String, String> emptyKey = new HashMap<>();
         emptyKey.put("", "value");
         DiskBasedCache.writeStringStringMap(emptyKey, baos);
-        Map<String, String> emptyValue = new HashMap<String, String>();
+        Map<String, String> emptyValue = new HashMap<>();
         emptyValue.put("key", "");
         DiskBasedCache.writeStringStringMap(emptyValue, baos);
-        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-        assertEquals(DiskBasedCache.readStringStringMap(bais), empty);
-        assertEquals(DiskBasedCache.readStringStringMap(bais), empty); // null reads back empty
-        assertEquals(DiskBasedCache.readStringStringMap(bais), twoThings);
-        assertEquals(DiskBasedCache.readStringStringMap(bais), emptyKey);
-        assertEquals(DiskBasedCache.readStringStringMap(bais), emptyValue);
+        CountingInputStream cis =
+                new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size());
+        assertEquals(DiskBasedCache.readStringStringMap(cis), empty);
+        assertEquals(DiskBasedCache.readStringStringMap(cis), empty); // null reads back empty
+        assertEquals(DiskBasedCache.readStringStringMap(cis), twoThings);
+        assertEquals(DiskBasedCache.readStringStringMap(cis), emptyKey);
+        assertEquals(DiskBasedCache.readStringStringMap(cis), emptyValue);
     }
 
     @Test
@@ -133,4 +460,28 @@
 
         assertNotNull(DiskBasedCache.class.getMethod("getFileForKey", String.class));
     }
+
+    /* 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();
+    }
 }