Merge commit '949b3428fcd4bdae1cff4763bbc8727472fd3ccb' into HEAD

Change-Id: I06f54572cdc15eba9aef520a69a20b04051b0742
diff --git a/bintray.gradle b/bintray.gradle
index df0e49b..d7b23d0 100644
--- a/bintray.gradle
+++ b/bintray.gradle
@@ -17,7 +17,7 @@
 
 def bintrayInfoFilePath = "$buildDir/outputs/bintray-descriptor.bintray-info.json"
 
-project.ext.version = '1.0.1-SNAPSHOT'
+project.ext.version = '1.1.0-SNAPSHOT'
 
 task sourcesJar(type: Jar) {
     classifier = 'sources'
diff --git a/consumer-proguard-rules.pro b/consumer-proguard-rules.pro
new file mode 100644
index 0000000..38d2cf1
--- /dev/null
+++ b/consumer-proguard-rules.pro
@@ -0,0 +1,9 @@
+# Prevent Proguard from inlining methods that are intentionally extracted to ensure locals have a
+# constrained liveness scope by the GC. This is needed to avoid keeping previous request references
+# alive for an indeterminate amount of time. See also https://github.com/google/volley/issues/114
+-keepclassmembers,allowshrinking,allowobfuscation class com.android.volley.NetworkDispatcher {
+    void processRequest();
+}
+-keepclassmembers,allowshrinking,allowobfuscation class com.android.volley.CacheDispatcher {
+    void processRequest();
+}
diff --git a/rules.gradle b/rules.gradle
index af81ac2..21df898 100644
--- a/rules.gradle
+++ b/rules.gradle
@@ -9,6 +9,10 @@
     sourceCompatibility JavaVersion.VERSION_1_7
     targetCompatibility JavaVersion.VERSION_1_7
   }
+
+  defaultConfig {
+    consumerProguardFiles 'consumer-proguard-rules.pro'
+  }
 }
 
 // Check if the android plugin version supports unit testing.
diff --git a/src/main/java/com/android/volley/CacheDispatcher.java b/src/main/java/com/android/volley/CacheDispatcher.java
index b0432f3..cd3635d 100644
--- a/src/main/java/com/android/volley/CacheDispatcher.java
+++ b/src/main/java/com/android/volley/CacheDispatcher.java
@@ -93,77 +93,7 @@
 
         while (true) {
             try {
-                // Get a request from the cache triage queue, blocking until
-                // at least one is available.
-                final Request<?> request = mCacheQueue.take();
-                request.addMarker("cache-queue-take");
-
-                // If the request has been canceled, don't bother dispatching it.
-                if (request.isCanceled()) {
-                    request.finish("cache-discard-canceled");
-                    continue;
-                }
-
-                // Attempt to retrieve this item from cache.
-                Cache.Entry entry = mCache.get(request.getCacheKey());
-                if (entry == null) {
-                    request.addMarker("cache-miss");
-                    // Cache miss; send off to the network dispatcher.
-                    if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
-                        mNetworkQueue.put(request);
-                    }
-                    continue;
-                }
-
-                // If it is completely expired, just send it to the network.
-                if (entry.isExpired()) {
-                    request.addMarker("cache-hit-expired");
-                    request.setCacheEntry(entry);
-                    if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
-                        mNetworkQueue.put(request);
-                    }
-                    continue;
-                }
-
-                // We have a cache hit; parse its data for delivery back to the request.
-                request.addMarker("cache-hit");
-                Response<?> response = request.parseNetworkResponse(
-                        new NetworkResponse(entry.data, entry.responseHeaders));
-                request.addMarker("cache-hit-parsed");
-
-                if (!entry.refreshNeeded()) {
-                    // Completely unexpired cache hit. Just deliver the response.
-                    mDelivery.postResponse(request, response);
-                } else {
-                    // Soft-expired cache hit. We can deliver the cached response,
-                    // but we need to also send the request to the network for
-                    // refreshing.
-                    request.addMarker("cache-hit-refresh-needed");
-                    request.setCacheEntry(entry);
-                    // Mark the response as intermediate.
-                    response.intermediate = true;
-
-                    if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
-                        // Post the intermediate response back to the user and have
-                        // the delivery then forward the request along to the network.
-                        mDelivery.postResponse(request, response, new Runnable() {
-                            @Override
-                            public void run() {
-                                try {
-                                    mNetworkQueue.put(request);
-                                } catch (InterruptedException e) {
-                                    // Restore the interrupted status
-                                    Thread.currentThread().interrupt();
-                                }
-                            }
-                        });
-                    } else {
-                        // request has been added to list of waiting requests
-                        // to receive the network response from the first request once it returns.
-                        mDelivery.postResponse(request, response);
-                    }
-                }
-
+                processRequest();
             } catch (InterruptedException e) {
                 // We may have been interrupted because it was time to quit.
                 if (mQuit) {
@@ -173,6 +103,83 @@
         }
     }
 
+    // Extracted to its own method to ensure locals have a constrained liveness scope by the GC.
+    // This is needed to avoid keeping previous request references alive for an indeterminate amount
+    // of time. Update consumer-proguard-rules.pro when modifying this. See also
+    // https://github.com/google/volley/issues/114
+    private void processRequest() throws InterruptedException {
+        // Get a request from the cache triage queue, blocking until
+        // at least one is available.
+        final Request<?> request = mCacheQueue.take();
+        request.addMarker("cache-queue-take");
+
+        // If the request has been canceled, don't bother dispatching it.
+        if (request.isCanceled()) {
+            request.finish("cache-discard-canceled");
+            return;
+        }
+
+        // Attempt to retrieve this item from cache.
+        Cache.Entry entry = mCache.get(request.getCacheKey());
+        if (entry == null) {
+            request.addMarker("cache-miss");
+            // Cache miss; send off to the network dispatcher.
+            if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+                mNetworkQueue.put(request);
+            }
+            return;
+        }
+
+        // If it is completely expired, just send it to the network.
+        if (entry.isExpired()) {
+            request.addMarker("cache-hit-expired");
+            request.setCacheEntry(entry);
+            if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+                mNetworkQueue.put(request);
+            }
+            return;
+        }
+
+        // We have a cache hit; parse its data for delivery back to the request.
+        request.addMarker("cache-hit");
+        Response<?> response = request.parseNetworkResponse(
+                new NetworkResponse(entry.data, entry.responseHeaders));
+        request.addMarker("cache-hit-parsed");
+
+        if (!entry.refreshNeeded()) {
+            // Completely unexpired cache hit. Just deliver the response.
+            mDelivery.postResponse(request, response);
+        } else {
+            // Soft-expired cache hit. We can deliver the cached response,
+            // but we need to also send the request to the network for
+            // refreshing.
+            request.addMarker("cache-hit-refresh-needed");
+            request.setCacheEntry(entry);
+            // Mark the response as intermediate.
+            response.intermediate = true;
+
+            if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+                // Post the intermediate response back to the user and have
+                // the delivery then forward the request along to the network.
+                mDelivery.postResponse(request, response, new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            mNetworkQueue.put(request);
+                        } catch (InterruptedException e) {
+                            // Restore the interrupted status
+                            Thread.currentThread().interrupt();
+                        }
+                    }
+                });
+            } else {
+                // request has been added to list of waiting requests
+                // to receive the network response from the first request once it returns.
+                mDelivery.postResponse(request, response);
+            }
+        }
+    }
+
     private static class WaitingRequestManager implements Request.NetworkRequestCompleteListener {
 
         /**
diff --git a/src/main/java/com/android/volley/NetworkDispatcher.java b/src/main/java/com/android/volley/NetworkDispatcher.java
index 0384429..2c04ce0 100644
--- a/src/main/java/com/android/volley/NetworkDispatcher.java
+++ b/src/main/java/com/android/volley/NetworkDispatcher.java
@@ -83,70 +83,76 @@
     public void run() {
         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
         while (true) {
-            long startTimeMs = SystemClock.elapsedRealtime();
-            Request<?> request;
             try {
-                // Take a request from the queue.
-                request = mQueue.take();
+                processRequest();
             } catch (InterruptedException e) {
                 // We may have been interrupted because it was time to quit.
                 if (mQuit) {
                     return;
                 }
-                continue;
+            }
+        }
+    }
+
+    // Extracted to its own method to ensure locals have a constrained liveness scope by the GC.
+    // This is needed to avoid keeping previous request references alive for an indeterminate amount
+    // of time. Update consumer-proguard-rules.pro when modifying this. See also
+    // https://github.com/google/volley/issues/114
+    private void processRequest() throws InterruptedException {
+        long startTimeMs = SystemClock.elapsedRealtime();
+        // Take a request from the queue.
+        Request<?> request = mQueue.take();
+
+        try {
+            request.addMarker("network-queue-take");
+
+            // If the request was cancelled already, do not perform the
+            // network request.
+            if (request.isCanceled()) {
+                request.finish("network-discard-cancelled");
+                request.notifyListenerResponseNotUsable();
+                return;
             }
 
-            try {
-                request.addMarker("network-queue-take");
+            addTrafficStatsTag(request);
 
-                // If the request was cancelled already, do not perform the
-                // network request.
-                if (request.isCanceled()) {
-                    request.finish("network-discard-cancelled");
-                    request.notifyListenerResponseNotUsable();
-                    continue;
-                }
+            // Perform the network request.
+            NetworkResponse networkResponse = mNetwork.performRequest(request);
+            request.addMarker("network-http-complete");
 
-                addTrafficStatsTag(request);
-
-                // Perform the network request.
-                NetworkResponse networkResponse = mNetwork.performRequest(request);
-                request.addMarker("network-http-complete");
-
-                // If the server returned 304 AND we delivered a response already,
-                // we're done -- don't deliver a second identical response.
-                if (networkResponse.notModified && request.hasHadResponseDelivered()) {
-                    request.finish("not-modified");
-                    request.notifyListenerResponseNotUsable();
-                    continue;
-                }
-
-                // Parse the response here on the worker thread.
-                Response<?> response = request.parseNetworkResponse(networkResponse);
-                request.addMarker("network-parse-complete");
-
-                // Write to cache if applicable.
-                // TODO: Only update cache metadata instead of entire record for 304s.
-                if (request.shouldCache() && response.cacheEntry != null) {
-                    mCache.put(request.getCacheKey(), response.cacheEntry);
-                    request.addMarker("network-cache-written");
-                }
-
-                // Post the response back.
-                request.markDelivered();
-                mDelivery.postResponse(request, response);
-                request.notifyListenerResponseReceived(response);
-            } catch (VolleyError volleyError) {
-                volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
-                parseAndDeliverNetworkError(request, volleyError);
+            // If the server returned 304 AND we delivered a response already,
+            // we're done -- don't deliver a second identical response.
+            if (networkResponse.notModified && request.hasHadResponseDelivered()) {
+                request.finish("not-modified");
                 request.notifyListenerResponseNotUsable();
-            } catch (Exception e) {
-                VolleyLog.e(e, "Unhandled exception %s", e.toString());
-                VolleyError volleyError = new VolleyError(e);
-                volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
-                mDelivery.postError(request, volleyError);
-                request.notifyListenerResponseNotUsable();
+                return;
             }
+
+            // Parse the response here on the worker thread.
+            Response<?> response = request.parseNetworkResponse(networkResponse);
+            request.addMarker("network-parse-complete");
+
+            // Write to cache if applicable.
+            // TODO: Only update cache metadata instead of entire record for 304s.
+            if (request.shouldCache() && response.cacheEntry != null) {
+                mCache.put(request.getCacheKey(), response.cacheEntry);
+                request.addMarker("network-cache-written");
+            }
+
+            // Post the response back.
+            request.markDelivered();
+            mDelivery.postResponse(request, response);
+            request.notifyListenerResponseReceived(response);
+        } catch (VolleyError volleyError) {
+            volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
+            parseAndDeliverNetworkError(request, volleyError);
+            request.notifyListenerResponseNotUsable();
+        } catch (Exception e) {
+            VolleyLog.e(e, "Unhandled exception %s", e.toString());
+            VolleyError volleyError = new VolleyError(e);
+            volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
+            mDelivery.postError(request, volleyError);
+            request.notifyListenerResponseNotUsable();
         }
     }