Import of Volley from GitHub to AOSP.

Android.bp has been updated to account for the new source directory structure.

  - 0dc50bcfd021c204a9e6c9e7e6befbdfa1027247 Refactor Volley into a multi-module project. (#418) by Jeff Davidson <jpd@google.com>
  - 763c86b0bc9f66a8bb499f6a8b7fd3bdc87621a8 Remove new constructors from JsonRequests which are break... by Jeff Davidson <jpd@google.com>
  - 8d1b1a59e7cd1b1d3c6d8686f8831cea08f80d1f Add @NonNull annotations to Volley (#413) by Kamal Faraj <kfaraj.dev@gmail.com>
  - 5ba41f8670413973f587e435598f9f1724fa26e9 Allow sending any JSON with JsonArrayRequest & JsonObject... by Paul Smith <paulsmithkc@gmail.com>
  - 784cdd755392a6080e5eb0bf94bd7bf4ea31cf17 Update SNAPSHOT version after 1.2.0 release by Jeff Davidson <jpd@google.com>
  - 0d6497bab417a5f78b3c8e03ea157ada0fbfbc5d Add developers stanza to Volley POM. (#400) by Jeff Davidson <jpd@google.com>
  - 36274bf515a699ae5a7fe3d321206d1b803226d8 API cleanup for Async Volley stack ahead of 1.2.0 release... by Jeff Davidson <jpd@google.com>
  - 03f0144843fcf9ebafe512647c1c588975429452 Update environment variable name for snapshot pushes. (#3... by Jeff Davidson <jpd@google.com>
  - 3bd1975652687d2baa1b11a7f02b135edede8523 Publish SNAPSHOT builds to OSSRH instead of OJO. (#397) by Jeff Davidson <jpd@google.com>
  - 0e0c3d9cfa694f8f1400a9e9abc4bc11761fdb52 Invoke RetryPolicy#retry in the blocking executor. (#393) by Jeff Davidson <jpd@google.com>
  - b51831a48f06ad28f627c3624e5edb41598a2bf8 Use a consistent timebase when evaluating soft/hard TTLs.... by Jeff Davidson <jpd@google.com>
  - cd0839113b100f163df1ebd04ce6d5b9e36e9863 Migrate from Travis CI to GitHub Actions. (#381) by Jeff Davidson <jpd@google.com>
  - bdc0e393199ebf9e67c4e29e665252818eed4639 Clean up cache initialization in AsyncRequestQueue. (#380) by Jeff Davidson <jpd@google.com>
  - 1c0ade36edde15d02844b40351ab6f80c63b71b3 Actually allow applications to provide custom executors. by Jeff Davidson <jpd@google.com>

GitOrigin-RevId: 0dc50bcfd021c204a9e6c9e7e6befbdfa1027247
Change-Id: I4b8e4098ad5c349cb83efc867273fac1d3582a34
diff --git a/.github/workflows/gradle-build.yaml b/.github/workflows/gradle-build.yaml
new file mode 100644
index 0000000..c42648c
--- /dev/null
+++ b/.github/workflows/gradle-build.yaml
@@ -0,0 +1,28 @@
+name: Gradle
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up JDK 1.8
+        uses: actions/setup-java@v1
+        with:
+          java-version: 1.8
+      - name: Cache Gradle packages
+        uses: actions/cache@v2
+        with:
+          path: ~/.gradle/caches
+          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
+          restore-keys: ${{ runner.os }}-gradle
+      - name: Build with Gradle
+        run: ./gradlew --continue verifyGoogleJavaFormat build connectedCheck
+      - name: Publish snapshot
+        if: github.event_name == 'push'
+        env:
+          OSSRH_DEPLOY_USERNAME: ${{ secrets.OSSRH_DEPLOY_USERNAME }}
+          OSSRH_DEPLOY_PASSWORD: ${{ secrets.OSSRH_DEPLOY_PASSWORD }}
+        run: ./publish-snapshot-on-commit.sh
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index fb6481a..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-language: android
-android:
-  licenses:
-    - 'android-sdk-preview-license-.+'
-    - 'android-sdk-license-.+'
-    - 'google-gdk-license-.+'
-
-  components:
-    # Workaround to be able to install v28 SDK and build tools.
-    # See https://github.com/travis-ci/travis-ci/issues/6040
-    - tools # to update the repository XML files
-    - tools # to update the SDK tools themselves
-
-    - platform-tools
-    - build-tools-28.0.3
-    - android-28
-
-jdk:
-  - oraclejdk8
-
-# Avoid uploading the cache after every build
-# See https://docs.travis-ci.com/user/languages/android/
-before_cache:
-  - rm -f  $HOME/.gradle/caches/modules-2/modules-2.lock
-  - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
-  - rm -fr $HOME/.gradle/caches/*/classAnalysis/
-  - rm -fr $HOME/.gradle/caches/*/workerMain/
-cache:
-  directories:
-    - $HOME/.gradle/caches/
-    - $HOME/.gradle/wrapper/
-    - $HOME/.android/build-cache
-
-env:
-  global:
-  - secure: "ObTSgg1H/RoQwhf9735Cr0OEs7r296aQTKiVOmQYSIxZPzc7o2KveSVV8IVeQ+VQtDxPFij6Odk4gaQSKVytliTlgtSsrEaYt67yWWYLWBLWbnzLNYoWMMy+36O5BK78RNtVJ3Xr1mdMcZ2+SRj2TcEvQBqMWlUkJfEcmJttreq0Wd7jiNitv5MbyYRcd2AvKaqNilx5rEm1ihbE0wT3lH20EdAjjjckaBT04r+VXU9e0dg6tmZjqG8dxGzxYHLRTz7nmKXlUnOJ3steNPrmR/AsGqKW6Ppowi31t3iLpL3zdT0+mvzfvnQwBodqHWSU/JhVDGlePv+4a5aU80s+5nN1IKL7tTLGYWoKdoIuQLovRkdcdkuj8UNyftPj6qOO2C9Tk8j64WXwUIDRnmNfjXbFzqN51oiT94G6hPcEDQSxLwuqlmgNm9I9WxZidb5YotIN2BcIKthAvdL4ecxE5INJvss8DVdYUZ53000GqSoMv8WET6jYkSJPKfvmTgpqqYIW0sgMiDfO/ta/MTFG5kSqECL+sAFZNmugwmTc7NIdy29myCyLH/A6oM4n3QeFDDHhOl2cRYlsX3juzw2goRppR9sEosFN0D1T7Fije5RmJsPgVLGwYMxSGukAHufcatKfhccUVl+haSJ3PLQ1z+25Ug2kpJwMAHbUYWCBnew="
-  - secure: "Ac95rbJd5dgNdK8ZlaeXpkKYXHGNj8pm4eNv1Oe8YtsByshHtBAK1m55H4Ex55oRXaLXntvQwnnJfqeTDPuqvd7QP1fjvp4Yjdaqa8MkC6qdVtm6LaqsGuE38uSiU6oxJKfnlCywNe2LfFlbzBtPd3ejNI7tfJcO3s1mD2aBT46vmwUy0t5ESxNdP2zUs6DFcbaOUWeJhQn8iNdRm2VbHEMdevzvvXXIUB9YUdBZQIuAZ5E8NRP5/dzPE8P0CY6/yXqQ/6bkXRV3Pf9QsRzJ+oEQVJAIFfC4JAGasgaBIVpaJ2C2At39jrNFpGYUNbHxvkBEal+WiaPk5TfmVbpyWJOTPRaaY3tIRdBbYf6kklnQk2jRJB6GCi4/yvT6oNjTQQEwsuYlaivkibwNehQqtqyjj12CcTI7lwbgNXeLvIWE6LNLIxrY4pnNy3bKjA0oLFoG/FuP3Wi9WldBtVXwvUBVFFXeOgzP6lCDkzGYTwYZi20lRmSgma3Q5e3/BbPtos3BZ9dSY3lUjttGxvHEDCJ48U1aw6usR91ZKD78Thb5OxWLkvs4rjWEDU2I649wiYSyqFldNEnv+2SJSRB+097XEcnCopGXorlMhBMAlhwHyiRY1u0D+9qrpfIl9Z07j0ZiG5uIscDXQZZjMivqe/+u8NJ3kN0zrDO5BQEgNls="
-
-# Publish a SNAPSHOT build for all commits to master.
-script:
-  - ./gradlew --continue verifyGoogleJavaFormat build connectedCheck && ./publish-snapshot-on-commit.sh
diff --git a/Android.bp b/Android.bp
index 803b4f2..917048f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -36,11 +36,10 @@
     name: "volley",
     sdk_version: "28",
     min_sdk_version: "8",
-    srcs: ["src/main/java/**/*.java"],
+    srcs: ["core/src/main/java/**/*.java"],
 
     // Exclude Cronet support for now. Can be enabled later if/when Cronet is made available as a
     // compilation dependency for Volley clients.
-    exclude_srcs: ["src/main/java/com/android/volley/cronet/**/*"],
 
     libs: [
         // Only needed at compile-time.
diff --git a/bintray.gradle b/bintray.gradle
deleted file mode 100644
index b642b41..0000000
--- a/bintray.gradle
+++ /dev/null
@@ -1,78 +0,0 @@
-buildscript {
-    repositories {
-        jcenter()
-    }
-    dependencies {
-        classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.8.1"
-    }
-}
-
-// apply the plugin with its class name rather than its Id to work around gradle limitation of
-// not being able to find the plugin by Id despite the dependencies being added right above. Gradle
-// 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: org.jfrog.gradle.plugin.artifactory.ArtifactoryPlugin
-apply plugin: 'maven-publish'
-
-task sourcesJar(type: Jar) {
-    classifier = 'sources'
-    from android.sourceSets.main.java.srcDirs
-}
-
-task javadoc(type: Javadoc) {
-    source = android.sourceSets.main.java.srcDirs
-    classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
-}
-
-task javadocJar(type: Jar, dependsOn: javadoc) {
-    classifier = 'javadoc'
-    from javadoc.destinationDir
-}
-
-artifacts {
-    archives javadocJar
-    archives sourcesJar
-}
-
-publishing {
-    publications {
-        library(MavenPublication) {
-            groupId 'com.android.volley'
-            artifactId 'volley'
-            version project.version
-            pom {
-                packaging 'aar'
-                licenses {
-                    license {
-                      name = "The Apache License, Version 2.0"
-                      url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
-                    }
-                }
-            }
-
-            // Release AAR, Sources, and JavaDoc
-            artifact "$buildDir/outputs/aar/volley-release.aar"
-            artifact sourcesJar
-            artifact javadocJar
-        }
-    }
-}
-
-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'
-    }
-}
diff --git a/build.gradle b/build.gradle
index 544771c..b8db952 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,76 +1,72 @@
-// NOTE: The only changes that belong in this file are the definitions
-// of tool versions (gradle plugin, compile SDK, build tools), so that
-// Volley can be built via gradle as a standalone project.
-//
-// Any other changes to the build config belong in rules.gradle, which
-// is used by projects that depend on Volley but define their own
-// tools versions across all dependencies to ensure a consistent build.
-//
-// Most users should just add this line to settings.gradle:
-//     include(":volley")
-//
-// If you have a more complicated Gradle setup you can choose to use
-// this instead:
-//     include(":volley")
-//     project(':volley').buildFileName = 'rules.gradle'
-
-import net.ltgt.gradle.errorprone.CheckSeverity
-
 buildscript {
     repositories {
+        gradlePluginPortal()
         jcenter()
         google()
     }
     dependencies {
         classpath 'com.android.tools.build:gradle:3.2.1'
+        classpath 'gradle.plugin.com.github.sherter.google-java-format:google-java-format-gradle-plugin:0.6'
+        // NOTE: 0.7 or newer will require upgrading to a newer Android gradle plugin:
+        // https://github.com/tbroyer/gradle-errorprone-plugin/commit/65b1026ebeae1b7ed8c28578c7f6eea512c16bea
+        classpath 'net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:0.6.1'
     }
 }
 
-plugins {
-    id "com.github.sherter.google-java-format" version "0.6"
-    // NOTE: 0.7 or newer will require upgrading to a newer Android gradle plugin:
-    // https://github.com/tbroyer/gradle-errorprone-plugin/commit/65b1026ebeae1b7ed8c28578c7f6eea512c16bea
-    id "net.ltgt.errorprone" version "0.6.1"
-}
-
-googleJavaFormat {
-    toolVersion = '1.5'
-    options style: 'AOSP'
-}
-
-apply plugin: 'com.android.library'
-
-repositories {
-    jcenter()
-    google()
-}
-
-dependencies {
-    // NOTE: Updating ErrorProne introduces new checks that may cause the build to fail. Pin to a
-    // specific version to control these updates.
-    errorprone("com.google.errorprone:error_prone_core:2.3.2")
-    // ErrorProne requires a JDK 9 compiler, so pull one in as a dependency since we use Java 8:
-    // https://github.com/tbroyer/gradle-errorprone-plugin#jdk-8-support
-    errorproneJavac("com.google.errorprone:javac:9+181-r4173-1")
-}
-
-group = 'com.android.volley'
-version = '1.2.0-SNAPSHOT'
-
-android {
-    compileSdkVersion 28
-    buildToolsVersion = '28.0.3'
-
-    defaultConfig {
-        minSdkVersion 8
+allprojects {
+    repositories {
+        jcenter()
+        google()
     }
 }
 
-tasks.withType(JavaCompile) {
-    options.errorprone {
-        check("ParameterComment", CheckSeverity.ERROR)
+subprojects {
+    apply plugin: 'com.github.sherter.google-java-format'
+    apply plugin: 'net.ltgt.errorprone'
+
+    googleJavaFormat {
+        toolVersion = '1.5'
+        options style: 'AOSP'
+    }
+
+    apply plugin: 'com.android.library'
+
+    dependencies {
+        // NOTE: Updating ErrorProne introduces new checks that may cause the build to fail. Pin to a
+        // specific version to control these updates.
+        errorprone("com.google.errorprone:error_prone_core:2.3.2")
+        // ErrorProne requires a JDK 9 compiler, so pull one in as a dependency since we use Java 8:
+        // https://github.com/tbroyer/gradle-errorprone-plugin#jdk-8-support
+        errorproneJavac("com.google.errorprone:javac:9+181-r4173-1")
+    }
+
+    group = 'com.android.volley'
+    version = '1.2.1-SNAPSHOT'
+
+    android {
+        useLibrary 'org.apache.http.legacy'
+
+        compileSdkVersion 28
+        buildToolsVersion = '28.0.3'
+
+        defaultConfig {
+            minSdkVersion 8
+        }
+
+        compileOptions {
+            sourceCompatibility JavaVersion.VERSION_1_7
+            targetCompatibility JavaVersion.VERSION_1_7
+        }
+    }
+
+    tasks.withType(JavaCompile) {
+        options.errorprone {
+            check("ParameterComment", net.ltgt.gradle.errorprone.CheckSeverity.ERROR)
+        }
+        options.compilerArgs << "-Xlint:unchecked" << "-Werror"
+    }
+
+    if (it.name != 'testing') {
+        apply from: '../publish.gradle'
     }
 }
-
-apply from: 'rules.gradle'
-apply from: 'bintray.gradle'
diff --git a/core/build.gradle b/core/build.gradle
new file mode 100644
index 0000000..812968c
--- /dev/null
+++ b/core/build.gradle
@@ -0,0 +1,28 @@
+android {
+    defaultConfig {
+        consumerProguardFiles 'consumer-proguard-rules.pro'
+    }
+}
+
+dependencies {
+    implementation "androidx.annotation:annotation:1.0.1"
+
+    testImplementation project(":testing")
+    testImplementation "junit:junit:4.12"
+    testImplementation "org.hamcrest:hamcrest-library:1.3"
+    testImplementation "org.mockito:mockito-core:2.19.0"
+    testImplementation "org.robolectric:robolectric:3.4.2"
+}
+
+publishing {
+    publications {
+        library(MavenPublication) {
+            artifactId 'volley'
+            pom {
+                name = 'Volley'
+                description = 'An HTTP library that makes networking for Android apps easier and, most importantly, faster.'
+            }
+            artifact "$buildDir/outputs/aar/core-release.aar"
+        }
+    }
+}
diff --git a/consumer-proguard-rules.pro b/core/consumer-proguard-rules.pro
similarity index 100%
rename from consumer-proguard-rules.pro
rename to core/consumer-proguard-rules.pro
diff --git a/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml
similarity index 100%
rename from src/main/AndroidManifest.xml
rename to core/src/main/AndroidManifest.xml
diff --git a/src/main/java/com/android/volley/AsyncCache.java b/core/src/main/java/com/android/volley/AsyncCache.java
similarity index 92%
rename from src/main/java/com/android/volley/AsyncCache.java
rename to core/src/main/java/com/android/volley/AsyncCache.java
index 3cddb4b..8b2dbcc 100644
--- a/src/main/java/com/android/volley/AsyncCache.java
+++ b/core/src/main/java/com/android/volley/AsyncCache.java
@@ -18,7 +18,12 @@
 
 import androidx.annotation.Nullable;
 
-/** Asynchronous equivalent to the {@link Cache} interface. */
+/**
+ * Asynchronous equivalent to the {@link Cache} interface.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
 public abstract class AsyncCache {
 
     public interface OnGetCompleteCallback {
diff --git a/src/main/java/com/android/volley/AsyncNetwork.java b/core/src/main/java/com/android/volley/AsyncNetwork.java
similarity index 94%
rename from src/main/java/com/android/volley/AsyncNetwork.java
rename to core/src/main/java/com/android/volley/AsyncNetwork.java
index ad19c03..47f35ea 100644
--- a/src/main/java/com/android/volley/AsyncNetwork.java
+++ b/core/src/main/java/com/android/volley/AsyncNetwork.java
@@ -22,7 +22,12 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.atomic.AtomicReference;
 
-/** An asynchronous implementation of {@link Network} to perform requests. */
+/**
+ * An asynchronous implementation of {@link Network} to perform requests.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
 public abstract class AsyncNetwork implements Network {
     private ExecutorService mBlockingExecutor;
     private ExecutorService mNonBlockingExecutor;
diff --git a/src/main/java/com/android/volley/AsyncRequestQueue.java b/core/src/main/java/com/android/volley/AsyncRequestQueue.java
similarity index 84%
rename from src/main/java/com/android/volley/AsyncRequestQueue.java
rename to core/src/main/java/com/android/volley/AsyncRequestQueue.java
index 3754866..7bf8c21 100644
--- a/src/main/java/com/android/volley/AsyncRequestQueue.java
+++ b/core/src/main/java/com/android/volley/AsyncRequestQueue.java
@@ -25,9 +25,10 @@
 import com.android.volley.AsyncNetwork.OnRequestComplete;
 import com.android.volley.Cache.Entry;
 import java.net.HttpURLConnection;
+import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.List;
 import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.PriorityBlockingQueue;
@@ -41,7 +42,10 @@
  * An asynchronous request dispatch queue.
  *
  * <p>Add requests to the queue with {@link #add(Request)}. Once completed, responses will be
- * delivered on the main thread (unless a custom {@link ResponseDelivery} has been provided)
+ * delivered on the main thread (unless a custom {@link ResponseDelivery} has been provided).
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
  */
 public class AsyncRequestQueue extends RequestQueue {
     /** Default number of blocking threads to start. */
@@ -83,6 +87,17 @@
     private final WaitingRequestManager mWaitingRequestManager = new WaitingRequestManager(this);
 
     /**
+     * Requests which have been queued before cache initialization has completed.
+     *
+     * <p>These requests are kicked off once cache initialization finishes. We avoid enqueuing them
+     * sooner as the cache may not yet be ready.
+     */
+    private final List<Request<?>> mRequestsAwaitingCacheInitialization = new ArrayList<>();
+
+    private volatile boolean mIsCacheInitialized = false;
+    private final Object mCacheInitializationLock = new Object[0];
+
+    /**
      * Sets all the variables, but processing does not begin until {@link #start()} is called.
      *
      * @param cache to use for persisting responses to disk. If an AsyncCache was provided, then
@@ -119,34 +134,37 @@
         mNetwork.setNonBlockingExecutor(mNonBlockingExecutor);
         mNetwork.setNonBlockingScheduledExecutor(mNonBlockingScheduledExecutor);
 
-        mNonBlockingExecutor.execute(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        // This is intentionally blocking, because we don't want to process any
-                        // requests until the cache is initialized.
-                        if (mAsyncCache != null) {
-                            final CountDownLatch latch = new CountDownLatch(1);
+        // Kick off cache initialization, which must complete before any requests can be processed.
+        if (mAsyncCache != null) {
+            mNonBlockingExecutor.execute(
+                    new Runnable() {
+                        @Override
+                        public void run() {
                             mAsyncCache.initialize(
                                     new AsyncCache.OnWriteCompleteCallback() {
                                         @Override
                                         public void onWriteComplete() {
-                                            latch.countDown();
+                                            onCacheInitializationComplete();
                                         }
                                     });
-                            try {
-                                latch.await();
-                            } catch (InterruptedException e) {
-                                VolleyLog.e(
-                                        e, "Thread was interrupted while initializing the cache.");
-                                Thread.currentThread().interrupt();
-                                throw new RuntimeException(e);
-                            }
-                        } else {
-                            getCache().initialize();
                         }
-                    }
-                });
+                    });
+        } else {
+            mBlockingExecutor.execute(
+                    new Runnable() {
+                        @Override
+                        public void run() {
+                            getCache().initialize();
+                            mNonBlockingExecutor.execute(
+                                    new Runnable() {
+                                        @Override
+                                        public void run() {
+                                            onCacheInitializationComplete();
+                                        }
+                                    });
+                        }
+                    });
+        }
     }
 
     /** Shuts down and nullifies both executors */
@@ -169,6 +187,17 @@
     /** Begins the request by sending it to the Cache or Network. */
     @Override
     <T> void beginRequest(Request<T> request) {
+        // If the cache hasn't been initialized yet, add the request to a temporary queue to be
+        // flushed once initialization completes.
+        if (!mIsCacheInitialized) {
+            synchronized (mCacheInitializationLock) {
+                if (!mIsCacheInitialized) {
+                    mRequestsAwaitingCacheInitialization.add(request);
+                    return;
+                }
+            }
+        }
+
         // If the request is uncacheable, send it over the network.
         if (request.shouldCache()) {
             if (mAsyncCache != null) {
@@ -181,6 +210,20 @@
         }
     }
 
+    private void onCacheInitializationComplete() {
+        List<Request<?>> requestsToDispatch;
+        synchronized (mCacheInitializationLock) {
+            requestsToDispatch = new ArrayList<>(mRequestsAwaitingCacheInitialization);
+            mRequestsAwaitingCacheInitialization.clear();
+            mIsCacheInitialized = true;
+        }
+
+        // Kick off any requests that were queued while waiting for cache initialization.
+        for (Request<?> request : requestsToDispatch) {
+            beginRequest(request);
+        }
+    }
+
     @Override
     <T> void sendRequestOverNetwork(Request<T> request) {
         mNonBlockingExecutor.execute(new NetworkTask<>(request));
@@ -230,8 +273,14 @@
             return;
         }
 
+        // Use a single instant to evaluate cache expiration. Otherwise, a cache entry with
+        // identical soft and hard TTL times may appear to be valid when checking isExpired but
+        // invalid upon checking refreshNeeded(), triggering a soft TTL refresh which should be
+        // impossible.
+        long currentTimeMillis = System.currentTimeMillis();
+
         // If it is completely expired, just send it to the network.
-        if (entry.isExpired()) {
+        if (entry.isExpired(currentTimeMillis)) {
             mRequest.addMarker("cache-hit-expired");
             mRequest.setCacheEntry(entry);
             if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) {
@@ -241,15 +290,17 @@
         }
 
         // We have a cache hit; parse its data for delivery back to the request.
-        mBlockingExecutor.execute(new CacheParseTask<>(mRequest, entry));
+        mBlockingExecutor.execute(new CacheParseTask<>(mRequest, entry, currentTimeMillis));
     }
 
     private class CacheParseTask<T> extends RequestTask<T> {
         Cache.Entry entry;
+        long startTimeMillis;
 
-        CacheParseTask(Request<T> request, Cache.Entry entry) {
+        CacheParseTask(Request<T> request, Cache.Entry entry, long startTimeMillis) {
             super(request);
             this.entry = entry;
+            this.startTimeMillis = startTimeMillis;
         }
 
         @Override
@@ -265,7 +316,7 @@
                                     entry.allResponseHeaders));
             mRequest.addMarker("cache-hit-parsed");
 
-            if (!entry.refreshNeeded()) {
+            if (!entry.refreshNeeded(startTimeMillis)) {
                 // Completely unexpired cache hit. Just deliver the response.
                 getResponseDelivery().postResponse(mRequest, response);
             } else {
@@ -436,17 +487,24 @@
     }
 
     /**
-     * This class may be used by advanced applications to provide custom executors according to
-     * their needs. Apps must create ExecutorServices dynamically given a blocking queue rather than
-     * providing them directly so that Volley can provide a PriorityQueue which will prioritize
-     * requests according to Request#getPriority.
+     * Factory to create/provide the executors which Volley will use.
+     *
+     * <p>This class may be used by advanced applications to provide custom executors according to
+     * their needs.
+     *
+     * <p>For applications which rely on setting request priority via {@link Request#getPriority}, a
+     * task queue is provided which will prioritize requests of higher priority should the thread
+     * pool itself be exhausted. If a shared pool is provided which does not make use of the given
+     * queue, then lower-priority requests may have tasks executed before higher-priority requests
+     * when enough tasks are in flight to fully saturate the shared pool.
      */
     public abstract static class ExecutorFactory {
-        abstract ExecutorService createNonBlockingExecutor(BlockingQueue<Runnable> taskQueue);
+        public abstract ExecutorService createNonBlockingExecutor(
+                BlockingQueue<Runnable> taskQueue);
 
-        abstract ExecutorService createBlockingExecutor(BlockingQueue<Runnable> taskQueue);
+        public abstract ExecutorService createBlockingExecutor(BlockingQueue<Runnable> taskQueue);
 
-        abstract ScheduledExecutorService createNonBlockingScheduledExecutor();
+        public abstract ScheduledExecutorService createNonBlockingScheduledExecutor();
     }
 
     /** Provides a BlockingQueue to be used to create executors. */
diff --git a/src/main/java/com/android/volley/AuthFailureError.java b/core/src/main/java/com/android/volley/AuthFailureError.java
similarity index 100%
rename from src/main/java/com/android/volley/AuthFailureError.java
rename to core/src/main/java/com/android/volley/AuthFailureError.java
diff --git a/src/main/java/com/android/volley/Cache.java b/core/src/main/java/com/android/volley/Cache.java
similarity index 90%
rename from src/main/java/com/android/volley/Cache.java
rename to core/src/main/java/com/android/volley/Cache.java
index b8908ac..7348d0f 100644
--- a/src/main/java/com/android/volley/Cache.java
+++ b/core/src/main/java/com/android/volley/Cache.java
@@ -102,12 +102,20 @@
 
         /** True if the entry is expired. */
         public boolean isExpired() {
-            return this.ttl < System.currentTimeMillis();
+            return isExpired(System.currentTimeMillis());
+        }
+
+        boolean isExpired(long currentTimeMillis) {
+            return this.ttl < currentTimeMillis;
         }
 
         /** True if a refresh is needed from the original data source. */
         public boolean refreshNeeded() {
-            return this.softTtl < System.currentTimeMillis();
+            return refreshNeeded(System.currentTimeMillis());
+        }
+
+        boolean refreshNeeded(long currentTimeMillis) {
+            return this.softTtl < currentTimeMillis;
         }
     }
 }
diff --git a/src/main/java/com/android/volley/CacheDispatcher.java b/core/src/main/java/com/android/volley/CacheDispatcher.java
similarity index 94%
rename from src/main/java/com/android/volley/CacheDispatcher.java
rename to core/src/main/java/com/android/volley/CacheDispatcher.java
index 1bfc0ea..4443143 100644
--- a/src/main/java/com/android/volley/CacheDispatcher.java
+++ b/core/src/main/java/com/android/volley/CacheDispatcher.java
@@ -138,8 +138,14 @@
                 return;
             }
 
+            // Use a single instant to evaluate cache expiration. Otherwise, a cache entry with
+            // identical soft and hard TTL times may appear to be valid when checking isExpired but
+            // invalid upon checking refreshNeeded(), triggering a soft TTL refresh which should be
+            // impossible.
+            long currentTimeMillis = System.currentTimeMillis();
+
             // If it is completely expired, just send it to the network.
-            if (entry.isExpired()) {
+            if (entry.isExpired(currentTimeMillis)) {
                 request.addMarker("cache-hit-expired");
                 request.setCacheEntry(entry);
                 if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
@@ -164,7 +170,7 @@
                 }
                 return;
             }
-            if (!entry.refreshNeeded()) {
+            if (!entry.refreshNeeded(currentTimeMillis)) {
                 // Completely unexpired cache hit. Just deliver the response.
                 mDelivery.postResponse(request, response);
             } else {
diff --git a/src/main/java/com/android/volley/ClientError.java b/core/src/main/java/com/android/volley/ClientError.java
similarity index 100%
rename from src/main/java/com/android/volley/ClientError.java
rename to core/src/main/java/com/android/volley/ClientError.java
diff --git a/src/main/java/com/android/volley/DefaultRetryPolicy.java b/core/src/main/java/com/android/volley/DefaultRetryPolicy.java
similarity index 100%
rename from src/main/java/com/android/volley/DefaultRetryPolicy.java
rename to core/src/main/java/com/android/volley/DefaultRetryPolicy.java
diff --git a/src/main/java/com/android/volley/ExecutorDelivery.java b/core/src/main/java/com/android/volley/ExecutorDelivery.java
similarity index 100%
rename from src/main/java/com/android/volley/ExecutorDelivery.java
rename to core/src/main/java/com/android/volley/ExecutorDelivery.java
diff --git a/src/main/java/com/android/volley/Header.java b/core/src/main/java/com/android/volley/Header.java
similarity index 100%
rename from src/main/java/com/android/volley/Header.java
rename to core/src/main/java/com/android/volley/Header.java
diff --git a/src/main/java/com/android/volley/Network.java b/core/src/main/java/com/android/volley/Network.java
similarity index 100%
rename from src/main/java/com/android/volley/Network.java
rename to core/src/main/java/com/android/volley/Network.java
diff --git a/src/main/java/com/android/volley/NetworkDispatcher.java b/core/src/main/java/com/android/volley/NetworkDispatcher.java
similarity index 100%
rename from src/main/java/com/android/volley/NetworkDispatcher.java
rename to core/src/main/java/com/android/volley/NetworkDispatcher.java
diff --git a/src/main/java/com/android/volley/NetworkError.java b/core/src/main/java/com/android/volley/NetworkError.java
similarity index 100%
rename from src/main/java/com/android/volley/NetworkError.java
rename to core/src/main/java/com/android/volley/NetworkError.java
diff --git a/src/main/java/com/android/volley/NetworkResponse.java b/core/src/main/java/com/android/volley/NetworkResponse.java
similarity index 100%
rename from src/main/java/com/android/volley/NetworkResponse.java
rename to core/src/main/java/com/android/volley/NetworkResponse.java
diff --git a/src/main/java/com/android/volley/NoConnectionError.java b/core/src/main/java/com/android/volley/NoConnectionError.java
similarity index 100%
rename from src/main/java/com/android/volley/NoConnectionError.java
rename to core/src/main/java/com/android/volley/NoConnectionError.java
diff --git a/src/main/java/com/android/volley/ParseError.java b/core/src/main/java/com/android/volley/ParseError.java
similarity index 100%
rename from src/main/java/com/android/volley/ParseError.java
rename to core/src/main/java/com/android/volley/ParseError.java
diff --git a/src/main/java/com/android/volley/Request.java b/core/src/main/java/com/android/volley/Request.java
similarity index 98%
rename from src/main/java/com/android/volley/Request.java
rename to core/src/main/java/com/android/volley/Request.java
index b60dc74..df0d18f 100644
--- a/src/main/java/com/android/volley/Request.java
+++ b/core/src/main/java/com/android/volley/Request.java
@@ -135,8 +135,8 @@
      * @deprecated Use {@link #Request(int, String, com.android.volley.Response.ErrorListener)}.
      */
     @Deprecated
-    public Request(String url, Response.ErrorListener listener) {
-        this(Method.DEPRECATED_GET_OR_POST, url, listener);
+    public Request(String url, Response.ErrorListener errorListener) {
+        this(Method.DEPRECATED_GET_OR_POST, url, errorListener);
     }
 
     /**
@@ -144,11 +144,15 @@
      * error listener. Note that the normal response listener is not provided here as delivery of
      * responses is provided by subclasses, who have a better idea of how to deliver an
      * already-parsed response.
+     *
+     * @param method the HTTP method to use
+     * @param url URL to fetch the response from
+     * @param errorListener Error listener, or null to ignore errors.
      */
-    public Request(int method, String url, @Nullable Response.ErrorListener listener) {
+    public Request(int method, String url, @Nullable Response.ErrorListener errorListener) {
         mMethod = method;
         mUrl = url;
-        mErrorListener = listener;
+        mErrorListener = errorListener;
         setRetryPolicy(new DefaultRetryPolicy());
 
         mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
diff --git a/src/main/java/com/android/volley/RequestQueue.java b/core/src/main/java/com/android/volley/RequestQueue.java
similarity index 100%
rename from src/main/java/com/android/volley/RequestQueue.java
rename to core/src/main/java/com/android/volley/RequestQueue.java
diff --git a/core/src/main/java/com/android/volley/RequestTask.java b/core/src/main/java/com/android/volley/RequestTask.java
new file mode 100644
index 0000000..b429f79
--- /dev/null
+++ b/core/src/main/java/com/android/volley/RequestTask.java
@@ -0,0 +1,20 @@
+package com.android.volley;
+
+/**
+ * Abstract runnable that's a task to be completed by the RequestQueue.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
+public abstract class RequestTask<T> implements Runnable {
+    final Request<T> mRequest;
+
+    public RequestTask(Request<T> request) {
+        mRequest = request;
+    }
+
+    @SuppressWarnings("unchecked")
+    public int compareTo(RequestTask<?> other) {
+        return mRequest.compareTo((Request<T>) other.mRequest);
+    }
+}
diff --git a/src/main/java/com/android/volley/Response.java b/core/src/main/java/com/android/volley/Response.java
similarity index 100%
rename from src/main/java/com/android/volley/Response.java
rename to core/src/main/java/com/android/volley/Response.java
diff --git a/src/main/java/com/android/volley/ResponseDelivery.java b/core/src/main/java/com/android/volley/ResponseDelivery.java
similarity index 100%
rename from src/main/java/com/android/volley/ResponseDelivery.java
rename to core/src/main/java/com/android/volley/ResponseDelivery.java
diff --git a/src/main/java/com/android/volley/RetryPolicy.java b/core/src/main/java/com/android/volley/RetryPolicy.java
similarity index 100%
rename from src/main/java/com/android/volley/RetryPolicy.java
rename to core/src/main/java/com/android/volley/RetryPolicy.java
diff --git a/src/main/java/com/android/volley/ServerError.java b/core/src/main/java/com/android/volley/ServerError.java
similarity index 100%
rename from src/main/java/com/android/volley/ServerError.java
rename to core/src/main/java/com/android/volley/ServerError.java
diff --git a/src/main/java/com/android/volley/TimeoutError.java b/core/src/main/java/com/android/volley/TimeoutError.java
similarity index 100%
rename from src/main/java/com/android/volley/TimeoutError.java
rename to core/src/main/java/com/android/volley/TimeoutError.java
diff --git a/src/main/java/com/android/volley/VolleyError.java b/core/src/main/java/com/android/volley/VolleyError.java
similarity index 100%
rename from src/main/java/com/android/volley/VolleyError.java
rename to core/src/main/java/com/android/volley/VolleyError.java
diff --git a/src/main/java/com/android/volley/VolleyLog.java b/core/src/main/java/com/android/volley/VolleyLog.java
similarity index 100%
rename from src/main/java/com/android/volley/VolleyLog.java
rename to core/src/main/java/com/android/volley/VolleyLog.java
diff --git a/src/main/java/com/android/volley/WaitingRequestManager.java b/core/src/main/java/com/android/volley/WaitingRequestManager.java
similarity index 100%
rename from src/main/java/com/android/volley/WaitingRequestManager.java
rename to core/src/main/java/com/android/volley/WaitingRequestManager.java
diff --git a/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java b/core/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java
rename to core/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java
diff --git a/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java b/core/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java
rename to core/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java
diff --git a/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java b/core/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java
similarity index 96%
rename from src/main/java/com/android/volley/toolbox/AsyncHttpStack.java
rename to core/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java
index bafab8c..4165637 100644
--- a/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java
+++ b/core/src/main/java/com/android/volley/toolbox/AsyncHttpStack.java
@@ -28,7 +28,12 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicReference;
 
-/** Asynchronous extension of the {@link BaseHttpStack} class. */
+/**
+ * Asynchronous extension of the {@link BaseHttpStack} class.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
 public abstract class AsyncHttpStack extends BaseHttpStack {
     private ExecutorService mBlockingExecutor;
     private ExecutorService mNonBlockingExecutor;
diff --git a/src/main/java/com/android/volley/toolbox/Authenticator.java b/core/src/main/java/com/android/volley/toolbox/Authenticator.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/Authenticator.java
rename to core/src/main/java/com/android/volley/toolbox/Authenticator.java
diff --git a/src/main/java/com/android/volley/toolbox/BaseHttpStack.java b/core/src/main/java/com/android/volley/toolbox/BaseHttpStack.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/BaseHttpStack.java
rename to core/src/main/java/com/android/volley/toolbox/BaseHttpStack.java
diff --git a/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java b/core/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java
similarity index 86%
rename from src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java
rename to core/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java
index 55892a0..cdedaff 100644
--- a/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java
+++ b/core/src/main/java/com/android/volley/toolbox/BasicAsyncNetwork.java
@@ -29,6 +29,7 @@
 import com.android.volley.Request;
 import com.android.volley.RequestTask;
 import com.android.volley.VolleyError;
+import com.android.volley.toolbox.NetworkUtility.RetryInfo;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.HttpURLConnection;
@@ -36,7 +37,12 @@
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
 
-/** A network performing Volley requests over an {@link HttpStack}. */
+/**
+ * A network performing Volley requests over an {@link HttpStack}.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
 public class BasicAsyncNetwork extends AsyncNetwork {
 
     private final AsyncHttpStack mAsyncStack;
@@ -126,13 +132,39 @@
             @Nullable HttpResponse httpResponse,
             @Nullable byte[] responseContents) {
         try {
-            NetworkUtility.handleException(
-                    request, exception, requestStartMs, httpResponse, responseContents);
+            RetryInfo retryInfo =
+                    NetworkUtility.shouldRetryException(
+                            request, exception, requestStartMs, httpResponse, responseContents);
+            // RetryPolicy#retry may need a background thread, so invoke in the blocking executor.
+            getBlockingExecutor()
+                    .execute(new InvokeRetryPolicyTask<>(request, retryInfo, callback));
         } catch (VolleyError volleyError) {
             callback.onError(volleyError);
-            return;
         }
-        performRequest(request, callback);
+    }
+
+    private class InvokeRetryPolicyTask<T> extends RequestTask<T> {
+        final Request<T> request;
+        final RetryInfo retryInfo;
+        final OnRequestComplete callback;
+
+        InvokeRetryPolicyTask(Request<T> request, RetryInfo retryInfo, OnRequestComplete callback) {
+            super(request);
+            this.request = request;
+            this.retryInfo = retryInfo;
+            this.callback = callback;
+        }
+
+        @Override
+        public void run() {
+            try {
+                NetworkUtility.attemptRetryOnException(request, retryInfo);
+                // attemptRetryOnException didn't throw, so proceed with the next attempt.
+                performRequest(request, callback);
+            } catch (VolleyError e) {
+                callback.onError(e);
+            }
+        }
     }
 
     @Override
diff --git a/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/core/src/main/java/com/android/volley/toolbox/BasicNetwork.java
similarity index 93%
rename from src/main/java/com/android/volley/toolbox/BasicNetwork.java
rename to core/src/main/java/com/android/volley/toolbox/BasicNetwork.java
index 06427fe..552e628 100644
--- a/src/main/java/com/android/volley/toolbox/BasicNetwork.java
+++ b/core/src/main/java/com/android/volley/toolbox/BasicNetwork.java
@@ -22,6 +22,7 @@
 import com.android.volley.NetworkResponse;
 import com.android.volley.Request;
 import com.android.volley.VolleyError;
+import com.android.volley.toolbox.NetworkUtility.RetryInfo;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.HttpURLConnection;
@@ -140,8 +141,11 @@
             } catch (IOException e) {
                 // This will either throw an exception, breaking us from the loop, or will loop
                 // again and retry the request.
-                NetworkUtility.handleException(
-                        request, e, requestStart, httpResponse, responseContents);
+                RetryInfo retryInfo =
+                        NetworkUtility.shouldRetryException(
+                                request, e, requestStart, httpResponse, responseContents);
+                // We should already be on a background thread, so we can invoke the retry inline.
+                NetworkUtility.attemptRetryOnException(request, retryInfo);
             }
         }
     }
diff --git a/src/main/java/com/android/volley/toolbox/ByteArrayPool.java b/core/src/main/java/com/android/volley/toolbox/ByteArrayPool.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/ByteArrayPool.java
rename to core/src/main/java/com/android/volley/toolbox/ByteArrayPool.java
diff --git a/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java b/core/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/ClearCacheRequest.java
rename to core/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java
diff --git a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/core/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/DiskBasedCache.java
rename to core/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
diff --git a/src/main/java/com/android/volley/toolbox/FileSupplier.java b/core/src/main/java/com/android/volley/toolbox/FileSupplier.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/FileSupplier.java
rename to core/src/main/java/com/android/volley/toolbox/FileSupplier.java
diff --git a/src/main/java/com/android/volley/toolbox/HttpClientStack.java b/core/src/main/java/com/android/volley/toolbox/HttpClientStack.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/HttpClientStack.java
rename to core/src/main/java/com/android/volley/toolbox/HttpClientStack.java
diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
rename to core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
diff --git a/src/main/java/com/android/volley/toolbox/HttpResponse.java b/core/src/main/java/com/android/volley/toolbox/HttpResponse.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/HttpResponse.java
rename to core/src/main/java/com/android/volley/toolbox/HttpResponse.java
diff --git a/src/main/java/com/android/volley/toolbox/HttpStack.java b/core/src/main/java/com/android/volley/toolbox/HttpStack.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/HttpStack.java
rename to core/src/main/java/com/android/volley/toolbox/HttpStack.java
diff --git a/src/main/java/com/android/volley/toolbox/HurlStack.java b/core/src/main/java/com/android/volley/toolbox/HurlStack.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/HurlStack.java
rename to core/src/main/java/com/android/volley/toolbox/HurlStack.java
diff --git a/src/main/java/com/android/volley/toolbox/ImageLoader.java b/core/src/main/java/com/android/volley/toolbox/ImageLoader.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/ImageLoader.java
rename to core/src/main/java/com/android/volley/toolbox/ImageLoader.java
diff --git a/src/main/java/com/android/volley/toolbox/ImageRequest.java b/core/src/main/java/com/android/volley/toolbox/ImageRequest.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/ImageRequest.java
rename to core/src/main/java/com/android/volley/toolbox/ImageRequest.java
diff --git a/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java b/core/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java
similarity index 92%
rename from src/main/java/com/android/volley/toolbox/JsonArrayRequest.java
rename to core/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java
index 86ed9e9..9f56746 100644
--- a/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java
+++ b/core/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java
@@ -26,7 +26,10 @@
 import org.json.JSONArray;
 import org.json.JSONException;
 
-/** A request for retrieving a {@link JSONArray} response body at a given URL. */
+/**
+ * A request for retrieving a {@link JSONArray} response body at a given URL, allowing for an
+ * optional {@link JSONArray} to be passed in as part of the request body.
+ */
 public class JsonArrayRequest extends JsonRequest<JSONArray> {
 
     /**
@@ -60,7 +63,7 @@
         super(
                 method,
                 url,
-                (jsonRequest == null) ? null : jsonRequest.toString(),
+                jsonRequest != null ? jsonRequest.toString() : null,
                 listener,
                 errorListener);
     }
diff --git a/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java b/core/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java
similarity index 82%
rename from src/main/java/com/android/volley/toolbox/JsonObjectRequest.java
rename to core/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java
index 8dca0ec..eccb54b 100644
--- a/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java
+++ b/core/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java
@@ -35,6 +35,38 @@
     /**
      * Creates a new request.
      *
+     * @param url URL to fetch the JSON from
+     * @param listener Listener to receive the JSON response
+     * @param errorListener Error listener, or null to ignore errors.
+     */
+    public JsonObjectRequest(
+            String url, Listener<JSONObject> listener, @Nullable ErrorListener errorListener) {
+        super(Method.GET, url, null, listener, errorListener);
+    }
+
+    /**
+     * Constructor which defaults to <code>GET</code> if <code>jsonRequest</code> is <code>null
+     * </code> , <code>POST</code> otherwise.
+     *
+     * @deprecated Use {@link #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener)}.
+     */
+    @Deprecated
+    public JsonObjectRequest(
+            String url,
+            @Nullable JSONObject jsonRequest,
+            Listener<JSONObject> listener,
+            @Nullable ErrorListener errorListener) {
+        super(
+                jsonRequest == null ? Method.GET : Method.POST,
+                url,
+                jsonRequest != null ? jsonRequest.toString() : null,
+                listener,
+                errorListener);
+    }
+
+    /**
+     * Creates a new request.
+     *
      * @param method the HTTP method to use
      * @param url URL to fetch the JSON from
      * @param jsonRequest A {@link JSONObject} to post with the request. Null indicates no
@@ -51,26 +83,7 @@
         super(
                 method,
                 url,
-                (jsonRequest == null) ? null : jsonRequest.toString(),
-                listener,
-                errorListener);
-    }
-
-    /**
-     * Constructor which defaults to <code>GET</code> if <code>jsonRequest</code> is <code>null
-     * </code> , <code>POST</code> otherwise.
-     *
-     * @see #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener)
-     */
-    public JsonObjectRequest(
-            String url,
-            @Nullable JSONObject jsonRequest,
-            Listener<JSONObject> listener,
-            @Nullable ErrorListener errorListener) {
-        this(
-                jsonRequest == null ? Method.GET : Method.POST,
-                url,
-                jsonRequest,
+                jsonRequest != null ? jsonRequest.toString() : null,
                 listener,
                 errorListener);
     }
diff --git a/src/main/java/com/android/volley/toolbox/JsonRequest.java b/core/src/main/java/com/android/volley/toolbox/JsonRequest.java
similarity index 90%
rename from src/main/java/com/android/volley/toolbox/JsonRequest.java
rename to core/src/main/java/com/android/volley/toolbox/JsonRequest.java
index bc035ae..c2d1fad 100644
--- a/src/main/java/com/android/volley/toolbox/JsonRequest.java
+++ b/core/src/main/java/com/android/volley/toolbox/JsonRequest.java
@@ -61,6 +61,16 @@
         this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener);
     }
 
+    /**
+     * Creates a new request.
+     *
+     * @param method the HTTP method to use
+     * @param url URL to fetch the JSON from
+     * @param requestBody The content to post as the body of the request. Null indicates no
+     *     parameters will be posted along with request.
+     * @param listener Listener to receive the JSON response
+     * @param errorListener Error listener, or null to ignore errors.
+     */
     public JsonRequest(
             int method,
             String url,
diff --git a/src/main/java/com/android/volley/toolbox/NetworkImageView.java b/core/src/main/java/com/android/volley/toolbox/NetworkImageView.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/NetworkImageView.java
rename to core/src/main/java/com/android/volley/toolbox/NetworkImageView.java
diff --git a/src/main/java/com/android/volley/toolbox/NetworkUtility.java b/core/src/main/java/com/android/volley/toolbox/NetworkUtility.java
similarity index 78%
rename from src/main/java/com/android/volley/toolbox/NetworkUtility.java
rename to core/src/main/java/com/android/volley/toolbox/NetworkUtility.java
index 44d5904..58a3bb3 100644
--- a/src/main/java/com/android/volley/toolbox/NetworkUtility.java
+++ b/core/src/main/java/com/android/volley/toolbox/NetworkUtility.java
@@ -42,7 +42,7 @@
  * Utility class for methods that are shared between {@link BasicNetwork} and {@link
  * BasicAsyncNetwork}
  */
-public final class NetworkUtility {
+final class NetworkUtility {
     private static final int SLOW_REQUEST_THRESHOLD_MS = 3000;
 
     private NetworkUtility() {}
@@ -113,30 +113,45 @@
 
     /**
      * Attempts to prepare the request for a retry. If there are no more attempts remaining in the
-     * request's retry policy, a timeout exception is thrown.
+     * request's retry policy, the provided exception is thrown.
+     *
+     * <p>Must be invoked from a background thread, as client implementations of RetryPolicy#retry
+     * may make blocking calls.
      *
      * @param request The request to use.
      */
-    private static void attemptRetryOnException(
-            final String logPrefix, final Request<?> request, final VolleyError exception)
+    static void attemptRetryOnException(final Request<?> request, final RetryInfo retryInfo)
             throws VolleyError {
         final RetryPolicy retryPolicy = request.getRetryPolicy();
         final int oldTimeout = request.getTimeoutMs();
         try {
-            retryPolicy.retry(exception);
+            retryPolicy.retry(retryInfo.errorToRetry);
         } catch (VolleyError e) {
             request.addMarker(
-                    String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
+                    String.format(
+                            "%s-timeout-giveup [timeout=%s]", retryInfo.logPrefix, oldTimeout));
             throw e;
         }
-        request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
+        request.addMarker(String.format("%s-retry [timeout=%s]", retryInfo.logPrefix, oldTimeout));
+    }
+
+    static class RetryInfo {
+        private final String logPrefix;
+        private final VolleyError errorToRetry;
+
+        private RetryInfo(String logPrefix, VolleyError errorToRetry) {
+            this.logPrefix = logPrefix;
+            this.errorToRetry = errorToRetry;
+        }
     }
 
     /**
      * Based on the exception thrown, decides whether to attempt to retry, or to throw the error.
-     * Also handles logging.
+     *
+     * <p>If this method returns without throwing, {@link #attemptRetryOnException} should be called
+     * with the provided {@link RetryInfo} to consult the client's retry policy.
      */
-    static void handleException(
+    static RetryInfo shouldRetryException(
             Request<?> request,
             IOException exception,
             long requestStartMs,
@@ -144,7 +159,7 @@
             @Nullable byte[] responseContents)
             throws VolleyError {
         if (exception instanceof SocketTimeoutException) {
-            attemptRetryOnException("socket", request, new TimeoutError());
+            return new RetryInfo("socket", new TimeoutError());
         } else if (exception instanceof MalformedURLException) {
             throw new RuntimeException("Bad URL " + request.getUrl(), exception);
         } else {
@@ -153,11 +168,9 @@
                 statusCode = httpResponse.getStatusCode();
             } else {
                 if (request.shouldRetryConnectionErrors()) {
-                    attemptRetryOnException("connection", request, new NoConnectionError());
-                    return;
-                } else {
-                    throw new NoConnectionError(exception);
+                    return new RetryInfo("connection", new NoConnectionError());
                 }
+                throw new NoConnectionError(exception);
             }
             VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
             NetworkResponse networkResponse;
@@ -173,24 +186,21 @@
                                 responseHeaders);
                 if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED
                         || statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
-                    attemptRetryOnException("auth", request, new AuthFailureError(networkResponse));
-                } else if (statusCode >= 400 && statusCode <= 499) {
+                    return new RetryInfo("auth", new AuthFailureError(networkResponse));
+                }
+                if (statusCode >= 400 && statusCode <= 499) {
                     // Don't retry other client errors.
                     throw new ClientError(networkResponse);
-                } else if (statusCode >= 500 && statusCode <= 599) {
-                    if (request.shouldRetryServerErrors()) {
-                        attemptRetryOnException(
-                                "server", request, new ServerError(networkResponse));
-                    } else {
-                        throw new ServerError(networkResponse);
-                    }
-                } else {
-                    // 3xx? No reason to retry.
-                    throw new ServerError(networkResponse);
                 }
-            } else {
-                attemptRetryOnException("network", request, new NetworkError());
+                if (statusCode >= 500 && statusCode <= 599) {
+                    if (request.shouldRetryServerErrors()) {
+                        return new RetryInfo("server", new ServerError(networkResponse));
+                    }
+                }
+                // Server error and client has opted out of retries, or 3xx. No reason to retry.
+                throw new ServerError(networkResponse);
             }
+            return new RetryInfo("network", new NetworkError());
         }
     }
 }
diff --git a/src/main/java/com/android/volley/toolbox/NoAsyncCache.java b/core/src/main/java/com/android/volley/toolbox/NoAsyncCache.java
similarity index 80%
rename from src/main/java/com/android/volley/toolbox/NoAsyncCache.java
rename to core/src/main/java/com/android/volley/toolbox/NoAsyncCache.java
index aa4aeea..1fda58f 100644
--- a/src/main/java/com/android/volley/toolbox/NoAsyncCache.java
+++ b/core/src/main/java/com/android/volley/toolbox/NoAsyncCache.java
@@ -3,7 +3,12 @@
 import com.android.volley.AsyncCache;
 import com.android.volley.Cache;
 
-/** An AsyncCache that doesn't cache anything. */
+/**
+ * An AsyncCache that doesn't cache anything.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
+ */
 public class NoAsyncCache extends AsyncCache {
     @Override
     public void get(String key, OnGetCompleteCallback callback) {
diff --git a/src/main/java/com/android/volley/toolbox/NoCache.java b/core/src/main/java/com/android/volley/toolbox/NoCache.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/NoCache.java
rename to core/src/main/java/com/android/volley/toolbox/NoCache.java
diff --git a/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java b/core/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java
rename to core/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java
diff --git a/src/main/java/com/android/volley/toolbox/RequestFuture.java b/core/src/main/java/com/android/volley/toolbox/RequestFuture.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/RequestFuture.java
rename to core/src/main/java/com/android/volley/toolbox/RequestFuture.java
diff --git a/src/main/java/com/android/volley/toolbox/StringRequest.java b/core/src/main/java/com/android/volley/toolbox/StringRequest.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/StringRequest.java
rename to core/src/main/java/com/android/volley/toolbox/StringRequest.java
diff --git a/src/main/java/com/android/volley/toolbox/Threads.java b/core/src/main/java/com/android/volley/toolbox/Threads.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/Threads.java
rename to core/src/main/java/com/android/volley/toolbox/Threads.java
diff --git a/src/main/java/com/android/volley/toolbox/UrlRewriter.java b/core/src/main/java/com/android/volley/toolbox/UrlRewriter.java
similarity index 100%
rename from src/main/java/com/android/volley/toolbox/UrlRewriter.java
rename to core/src/main/java/com/android/volley/toolbox/UrlRewriter.java
diff --git a/src/main/java/com/android/volley/toolbox/Volley.java b/core/src/main/java/com/android/volley/toolbox/Volley.java
similarity index 97%
rename from src/main/java/com/android/volley/toolbox/Volley.java
rename to core/src/main/java/com/android/volley/toolbox/Volley.java
index bc65c9c..6ab34bb 100644
--- a/src/main/java/com/android/volley/toolbox/Volley.java
+++ b/core/src/main/java/com/android/volley/toolbox/Volley.java
@@ -21,6 +21,7 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.net.http.AndroidHttpClient;
 import android.os.Build;
+import androidx.annotation.NonNull;
 import com.android.volley.Network;
 import com.android.volley.RequestQueue;
 import java.io.File;
@@ -37,6 +38,7 @@
      * @param stack A {@link BaseHttpStack} to use for the network, or null for default.
      * @return A started {@link RequestQueue} instance.
      */
+    @NonNull
     public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) {
         BasicNetwork network;
         if (stack == null) {
@@ -78,6 +80,7 @@
      */
     @Deprecated
     @SuppressWarnings("deprecation")
+    @NonNull
     public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
         if (stack == null) {
             return newRequestQueue(context, (BaseHttpStack) null);
@@ -85,6 +88,7 @@
         return newRequestQueue(context, new BasicNetwork(stack));
     }
 
+    @NonNull
     private static RequestQueue newRequestQueue(Context context, Network network) {
         final Context appContext = context.getApplicationContext();
         // Use a lazy supplier for the cache directory so that newRequestQueue() can be called on
@@ -112,6 +116,7 @@
      * @param context A {@link Context} to use for creating the cache dir.
      * @return A started {@link RequestQueue} instance.
      */
+    @NonNull
     public static RequestQueue newRequestQueue(Context context) {
         return newRequestQueue(context, (BaseHttpStack) null);
     }
diff --git a/src/test/java/com/android/volley/AsyncRequestQueueTest.java b/core/src/test/java/com/android/volley/AsyncRequestQueueTest.java
similarity index 64%
rename from src/test/java/com/android/volley/AsyncRequestQueueTest.java
rename to core/src/test/java/com/android/volley/AsyncRequestQueueTest.java
index 54ff0a1..aef4f01 100644
--- a/src/test/java/com/android/volley/AsyncRequestQueueTest.java
+++ b/core/src/test/java/com/android/volley/AsyncRequestQueueTest.java
@@ -16,6 +16,9 @@
 
 package com.android.volley;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -23,6 +26,8 @@
 import static org.mockito.Mockito.when;
 import static org.mockito.MockitoAnnotations.initMocks;
 
+import com.android.volley.AsyncCache.OnGetCompleteCallback;
+import com.android.volley.AsyncCache.OnWriteCompleteCallback;
 import com.android.volley.mock.ShadowSystemClock;
 import com.android.volley.toolbox.NoAsyncCache;
 import com.android.volley.toolbox.StringRequest;
@@ -34,6 +39,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.Config;
@@ -45,37 +51,13 @@
 
     @Mock private AsyncNetwork mMockNetwork;
     @Mock private ScheduledExecutorService mMockScheduledExecutor;
+    private final ResponseDelivery mDelivery = new ImmediateResponseDelivery();
     private AsyncRequestQueue queue;
 
     @Before
     public void setUp() throws Exception {
-        ResponseDelivery mDelivery = new ImmediateResponseDelivery();
         initMocks(this);
-        queue =
-                new AsyncRequestQueue.Builder(mMockNetwork)
-                        .setAsyncCache(new NoAsyncCache())
-                        .setResponseDelivery(mDelivery)
-                        .setExecutorFactory(
-                                new AsyncRequestQueue.ExecutorFactory() {
-                                    @Override
-                                    public ExecutorService createNonBlockingExecutor(
-                                            BlockingQueue<Runnable> taskQueue) {
-                                        return MoreExecutors.newDirectExecutorService();
-                                    }
-
-                                    @Override
-                                    public ExecutorService createBlockingExecutor(
-                                            BlockingQueue<Runnable> taskQueue) {
-                                        return MoreExecutors.newDirectExecutorService();
-                                    }
-
-                                    @Override
-                                    public ScheduledExecutorService
-                                            createNonBlockingScheduledExecutor() {
-                                        return mMockScheduledExecutor;
-                                    }
-                                })
-                        .build();
+        queue = createRequestQueue(new NoAsyncCache());
     }
 
     @Test
@@ -161,4 +143,58 @@
         verifyNoMoreInteractions(listener);
         queue.stop();
     }
+
+    @Test
+    public void requestsQueuedBeforeCacheInitialization_asyncCache() {
+        // Create a new queue with a mock cache in order to verify the initialization.
+        AsyncCache mockAsyncCache = mock(AsyncCache.class);
+        AsyncRequestQueue queue = createRequestQueue(mockAsyncCache);
+        queue.start();
+
+        ArgumentCaptor<OnWriteCompleteCallback> callbackCaptor =
+                ArgumentCaptor.forClass(OnWriteCompleteCallback.class);
+        verify(mockAsyncCache).initialize(callbackCaptor.capture());
+
+        StringRequest req = mock(StringRequest.class);
+        req.setShouldCache(true);
+        when(req.getCacheKey()).thenReturn("cache-key");
+        queue.add(req);
+
+        // Cache should not be read before initialization completes.
+        verify(mockAsyncCache, never()).get(anyString(), any(OnGetCompleteCallback.class));
+
+        callbackCaptor.getValue().onWriteComplete();
+
+        // Once the write completes, the request should be kicked off (in the form of a cache
+        // lookup).
+        verify(mockAsyncCache).get(eq("cache-key"), any(OnGetCompleteCallback.class));
+
+        queue.stop();
+    }
+
+    private AsyncRequestQueue createRequestQueue(AsyncCache asyncCache) {
+        return new AsyncRequestQueue.Builder(mMockNetwork)
+                .setResponseDelivery(mDelivery)
+                .setAsyncCache(asyncCache)
+                .setExecutorFactory(
+                        new AsyncRequestQueue.ExecutorFactory() {
+                            @Override
+                            public ExecutorService createNonBlockingExecutor(
+                                    BlockingQueue<Runnable> taskQueue) {
+                                return MoreExecutors.newDirectExecutorService();
+                            }
+
+                            @Override
+                            public ExecutorService createBlockingExecutor(
+                                    BlockingQueue<Runnable> taskQueue) {
+                                return MoreExecutors.newDirectExecutorService();
+                            }
+
+                            @Override
+                            public ScheduledExecutorService createNonBlockingScheduledExecutor() {
+                                return mMockScheduledExecutor;
+                            }
+                        })
+                .build();
+    }
 }
diff --git a/src/test/java/com/android/volley/CacheDispatcherTest.java b/core/src/test/java/com/android/volley/CacheDispatcherTest.java
similarity index 100%
rename from src/test/java/com/android/volley/CacheDispatcherTest.java
rename to core/src/test/java/com/android/volley/CacheDispatcherTest.java
diff --git a/src/test/java/com/android/volley/NetworkDispatcherTest.java b/core/src/test/java/com/android/volley/NetworkDispatcherTest.java
similarity index 100%
rename from src/test/java/com/android/volley/NetworkDispatcherTest.java
rename to core/src/test/java/com/android/volley/NetworkDispatcherTest.java
diff --git a/src/test/java/com/android/volley/NetworkResponseTest.java b/core/src/test/java/com/android/volley/NetworkResponseTest.java
similarity index 100%
rename from src/test/java/com/android/volley/NetworkResponseTest.java
rename to core/src/test/java/com/android/volley/NetworkResponseTest.java
diff --git a/src/test/java/com/android/volley/RequestQueueIntegrationTest.java b/core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java
similarity index 100%
rename from src/test/java/com/android/volley/RequestQueueIntegrationTest.java
rename to core/src/test/java/com/android/volley/RequestQueueIntegrationTest.java
diff --git a/src/test/java/com/android/volley/RequestQueueTest.java b/core/src/test/java/com/android/volley/RequestQueueTest.java
similarity index 100%
rename from src/test/java/com/android/volley/RequestQueueTest.java
rename to core/src/test/java/com/android/volley/RequestQueueTest.java
diff --git a/src/test/java/com/android/volley/RequestTest.java b/core/src/test/java/com/android/volley/RequestTest.java
similarity index 100%
rename from src/test/java/com/android/volley/RequestTest.java
rename to core/src/test/java/com/android/volley/RequestTest.java
diff --git a/src/test/java/com/android/volley/ResponseDeliveryTest.java b/core/src/test/java/com/android/volley/ResponseDeliveryTest.java
similarity index 100%
rename from src/test/java/com/android/volley/ResponseDeliveryTest.java
rename to core/src/test/java/com/android/volley/ResponseDeliveryTest.java
diff --git a/src/test/java/com/android/volley/mock/MockAsyncStack.java b/core/src/test/java/com/android/volley/mock/MockAsyncStack.java
similarity index 100%
rename from src/test/java/com/android/volley/mock/MockAsyncStack.java
rename to core/src/test/java/com/android/volley/mock/MockAsyncStack.java
diff --git a/src/test/java/com/android/volley/mock/MockHttpStack.java b/core/src/test/java/com/android/volley/mock/MockHttpStack.java
similarity index 100%
rename from src/test/java/com/android/volley/mock/MockHttpStack.java
rename to core/src/test/java/com/android/volley/mock/MockHttpStack.java
diff --git a/src/test/java/com/android/volley/mock/MockRequest.java b/core/src/test/java/com/android/volley/mock/MockRequest.java
similarity index 100%
rename from src/test/java/com/android/volley/mock/MockRequest.java
rename to core/src/test/java/com/android/volley/mock/MockRequest.java
diff --git a/src/test/java/com/android/volley/mock/ShadowSystemClock.java b/core/src/test/java/com/android/volley/mock/ShadowSystemClock.java
similarity index 100%
rename from src/test/java/com/android/volley/mock/ShadowSystemClock.java
rename to core/src/test/java/com/android/volley/mock/ShadowSystemClock.java
diff --git a/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java b/core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java
rename to core/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java
diff --git a/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java b/core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java
rename to core/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java
diff --git a/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java b/core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java
rename to core/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java
diff --git a/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java b/core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java
rename to core/src/test/java/com/android/volley/toolbox/BasicAsyncNetworkTest.java
diff --git a/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java b/core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
rename to core/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
diff --git a/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java b/core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java
rename to core/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java
diff --git a/src/test/java/com/android/volley/toolbox/CacheTest.java b/core/src/test/java/com/android/volley/toolbox/CacheTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/CacheTest.java
rename to core/src/test/java/com/android/volley/toolbox/CacheTest.java
diff --git a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
rename to core/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
diff --git a/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java b/core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/HttpClientStackTest.java
rename to core/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java
diff --git a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
rename to core/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
diff --git a/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java b/core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java
rename to core/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java
diff --git a/src/test/java/com/android/volley/toolbox/HurlStackTest.java b/core/src/test/java/com/android/volley/toolbox/HurlStackTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/HurlStackTest.java
rename to core/src/test/java/com/android/volley/toolbox/HurlStackTest.java
diff --git a/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java b/core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/ImageLoaderTest.java
rename to core/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java
diff --git a/src/test/java/com/android/volley/toolbox/ImageRequestTest.java b/core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/ImageRequestTest.java
rename to core/src/test/java/com/android/volley/toolbox/ImageRequestTest.java
diff --git a/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java b/core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java
rename to core/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java
diff --git a/src/test/java/com/android/volley/toolbox/JsonRequestTest.java b/core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/JsonRequestTest.java
rename to core/src/test/java/com/android/volley/toolbox/JsonRequestTest.java
diff --git a/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java b/core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java
rename to core/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java
diff --git a/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java b/core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java
rename to core/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java
diff --git a/src/test/java/com/android/volley/toolbox/RequestFutureTest.java b/core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/RequestFutureTest.java
rename to core/src/test/java/com/android/volley/toolbox/RequestFutureTest.java
diff --git a/src/test/java/com/android/volley/toolbox/RequestQueueTest.java b/core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/RequestQueueTest.java
rename to core/src/test/java/com/android/volley/toolbox/RequestQueueTest.java
diff --git a/src/test/java/com/android/volley/toolbox/RequestTest.java b/core/src/test/java/com/android/volley/toolbox/RequestTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/RequestTest.java
rename to core/src/test/java/com/android/volley/toolbox/RequestTest.java
diff --git a/src/test/java/com/android/volley/toolbox/ResponseTest.java b/core/src/test/java/com/android/volley/toolbox/ResponseTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/ResponseTest.java
rename to core/src/test/java/com/android/volley/toolbox/ResponseTest.java
diff --git a/src/test/java/com/android/volley/toolbox/StringRequestTest.java b/core/src/test/java/com/android/volley/toolbox/StringRequestTest.java
similarity index 100%
rename from src/test/java/com/android/volley/toolbox/StringRequestTest.java
rename to core/src/test/java/com/android/volley/toolbox/StringRequestTest.java
diff --git a/src/test/java/com/android/volley/utils/CacheTestUtils.java b/core/src/test/java/com/android/volley/utils/CacheTestUtils.java
similarity index 100%
rename from src/test/java/com/android/volley/utils/CacheTestUtils.java
rename to core/src/test/java/com/android/volley/utils/CacheTestUtils.java
diff --git a/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java b/core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java
similarity index 100%
rename from src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java
rename to core/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java
diff --git a/src/test/resources/org.robolectric.Config.properties b/core/src/test/resources/org.robolectric.Config.properties
similarity index 100%
rename from src/test/resources/org.robolectric.Config.properties
rename to core/src/test/resources/org.robolectric.Config.properties
diff --git a/cronet/build.gradle b/cronet/build.gradle
new file mode 100644
index 0000000..5ee53d6
--- /dev/null
+++ b/cronet/build.gradle
@@ -0,0 +1,24 @@
+dependencies {
+    implementation project(":core")
+    implementation "androidx.annotation:annotation:1.0.1"
+    compileOnly "org.chromium.net:cronet-embedded:76.3809.111"
+
+    testImplementation project(":testing")
+    testImplementation "org.chromium.net:cronet-embedded:76.3809.111"
+    testImplementation "junit:junit:4.12"
+    testImplementation "org.mockito:mockito-core:2.19.0"
+    testImplementation "org.robolectric:robolectric:3.4.2"
+}
+
+publishing {
+    publications {
+        library(MavenPublication) {
+            artifactId 'volley-cronet'
+            pom {
+                name = 'Volley Cronet'
+                description = 'Cronet support for Volley.'
+            }
+            artifact "$buildDir/outputs/aar/cronet-release.aar"
+        }
+    }
+}
diff --git a/cronet/src/main/AndroidManifest.xml b/cronet/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0dec093
--- /dev/null
+++ b/cronet/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest package="com.android.volley.cronet" />
diff --git a/src/main/java/com/android/volley/cronet/CronetHttpStack.java b/cronet/src/main/java/com/android/volley/cronet/CronetHttpStack.java
similarity index 99%
rename from src/main/java/com/android/volley/cronet/CronetHttpStack.java
rename to cronet/src/main/java/com/android/volley/cronet/CronetHttpStack.java
index f3baace..874029b 100644
--- a/src/main/java/com/android/volley/cronet/CronetHttpStack.java
+++ b/cronet/src/main/java/com/android/volley/cronet/CronetHttpStack.java
@@ -53,6 +53,9 @@
 
 /**
  * A {@link AsyncHttpStack} that's based on Cronet's fully asynchronous API for network requests.
+ *
+ * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
+ * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
  */
 public class CronetHttpStack extends AsyncHttpStack {
 
diff --git a/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java b/cronet/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java
similarity index 100%
rename from src/test/java/com/android/volley/cronet/CronetHttpStackTest.java
rename to cronet/src/test/java/com/android/volley/cronet/CronetHttpStackTest.java
diff --git a/publish-snapshot-on-commit.sh b/publish-snapshot-on-commit.sh
index 0d0e034..ba02b79 100755
--- a/publish-snapshot-on-commit.sh
+++ b/publish-snapshot-on-commit.sh
@@ -1,13 +1,15 @@
 set -eu
 
-if [ "$TRAVIS_REPO_SLUG" == "google/volley" ] && \
-   [ "$TRAVIS_PULL_REQUEST" == "false" ] && \
-   [ "$TRAVIS_BRANCH" == "master" ]; then
-  echo -e "Publishing snapshot build to OJO...\n"
+GITHUB_BRANCH=${GITHUB_REF#refs/heads/}
 
-  ./gradlew artifactoryPublish
+if [ "$GITHUB_REPOSITORY" == "google/volley" ] && \
+   [ "$GITHUB_EVENT_NAME" == "push" ] && \
+   [ "$GITHUB_BRANCH" == "master" ]; then
+  echo -e "Publishing snapshot build...\n"
 
-  echo -e "Published snapshot build to OJO"
+  ./gradlew publish
+
+  echo -e "Published snapshot build"
 else
   echo -e "Not publishing snapshot"
-fi
\ No newline at end of file
+fi
diff --git a/publish.gradle b/publish.gradle
new file mode 100644
index 0000000..429df4d
--- /dev/null
+++ b/publish.gradle
@@ -0,0 +1,72 @@
+apply plugin: 'maven-publish'
+
+task sourcesJar(type: Jar) {
+    classifier = 'sources'
+    from android.sourceSets.main.java.srcDirs
+}
+
+task javadoc(type: Javadoc) {
+    source = android.sourceSets.main.java.srcDirs
+    classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+}
+
+afterEvaluate {
+    javadoc.classpath += files(android.libraryVariants.collect { variant ->
+        variant.getJavaCompile().classpath.files
+    })
+}
+
+task javadocJar(type: Jar, dependsOn: javadoc) {
+    classifier = 'javadoc'
+    from javadoc.destinationDir
+}
+
+artifacts {
+    archives javadocJar
+    archives sourcesJar
+}
+
+publishing {
+    publications {
+        library(MavenPublication) {
+            groupId 'com.android.volley'
+            version project.version
+            pom {
+                name = 'Volley'
+                url = 'https://github.com/google/volley'
+                packaging 'aar'
+                licenses {
+                    license {
+                      name = "The Apache License, Version 2.0"
+                      url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
+                    }
+                }
+                scm {
+                    connection = 'scm:git:git://github.com/google/volley.git'
+                    developerConnection = 'scm:git:ssh://git@github.com/google/volley.git'
+                    url = 'https://github.com/google/volley'
+                }
+                developers {
+                    developer {
+                        name = 'The Volley Team'
+                        email = 'noreply+volley@google.com'
+                    }
+                }
+            }
+
+            // Release AAR, Sources, and JavaDoc
+            artifact sourcesJar
+            artifact javadocJar
+        }
+    }
+
+    repositories {
+        maven {
+            url = "https://oss.sonatype.org/content/repositories/snapshots/"
+            credentials {
+                username = System.env.OSSRH_DEPLOY_USERNAME
+                password = System.env.OSSRH_DEPLOY_PASSWORD
+            }
+        }
+    }
+}
diff --git a/rules.gradle b/rules.gradle
deleted file mode 100644
index e0aef80..0000000
--- a/rules.gradle
+++ /dev/null
@@ -1,36 +0,0 @@
-// See build.gradle for an explanation of what this file is.
-
-apply plugin: 'com.android.library'
-
-android {
-  useLibrary 'org.apache.http.legacy'
-
-  compileOptions {
-    sourceCompatibility JavaVersion.VERSION_1_7
-    targetCompatibility JavaVersion.VERSION_1_7
-  }
-
-  defaultConfig {
-    consumerProguardFiles 'consumer-proguard-rules.pro'
-  }
-}
-
-tasks.withType(JavaCompile) {
-  options.compilerArgs << "-Xlint:unchecked" << "-Werror"
-}
-
-dependencies {
-    implementation "androidx.annotation:annotation:1.0.1"
-    compileOnly "org.chromium.net:cronet-embedded:76.3809.111"
-}
-
-// Check if the android plugin version supports unit testing.
-if (configurations.findByName("testImplementation")) {
-  dependencies {
-      testImplementation "org.chromium.net:cronet-embedded:76.3809.111"
-      testImplementation "junit:junit:4.12"
-      testImplementation "org.hamcrest:hamcrest-library:1.3"
-      testImplementation "org.mockito:mockito-core:2.19.0"
-      testImplementation "org.robolectric:robolectric:3.4.2"
-  }
-}
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..ace4000
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,4 @@
+rootProject.name = 'volley'
+include 'core'
+include 'cronet'
+include 'testing'
diff --git a/src/main/java/com/android/volley/RequestTask.java b/src/main/java/com/android/volley/RequestTask.java
deleted file mode 100644
index 8eeaf2c..0000000
--- a/src/main/java/com/android/volley/RequestTask.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.android.volley;
-
-/** Abstract runnable that's a task to be completed by the RequestQueue. */
-public abstract class RequestTask<T> implements Runnable {
-    final Request<T> mRequest;
-
-    public RequestTask(Request<T> request) {
-        mRequest = request;
-    }
-
-    @SuppressWarnings("unchecked")
-    public int compareTo(RequestTask<?> other) {
-        return mRequest.compareTo((Request<T>) other.mRequest);
-    }
-}
diff --git a/testing/build.gradle b/testing/build.gradle
new file mode 100644
index 0000000..b374088
--- /dev/null
+++ b/testing/build.gradle
@@ -0,0 +1,4 @@
+dependencies {
+    implementation project(":core")
+}
+
diff --git a/testing/src/main/AndroidManifest.xml b/testing/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..24b1376
--- /dev/null
+++ b/testing/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest package="com.android.volley.testing" />
diff --git a/src/test/java/com/android/volley/mock/TestRequest.java b/testing/src/main/java/com/android/volley/mock/TestRequest.java
similarity index 100%
rename from src/test/java/com/android/volley/mock/TestRequest.java
rename to testing/src/main/java/com/android/volley/mock/TestRequest.java