Adds Cache-Control "stale-while-revalidate"

Adds Cache-Control "stale-while-revalidate" header parsing for better
server side cache entry expire setup.

see:
https://www.mnot.net/blog/2007/12/12/stale
http://tools.ietf.org/html/rfc5861
https://groups.google.com/a/chromium.org/forum/m/#!msg/chromium-dev/zchogDvIYrY/ZqWSdt3LJdMJ
Signed-off-by: Ralph Bergmann <ralph@the4thfloor.eu>

Change-Id: I1b7baf9997d3a8a251d21631a11deb503e3a7461
diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
index e342c9e..da04490 100644
--- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
+++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
@@ -45,7 +45,9 @@
         long lastModified = 0;
         long serverExpires = 0;
         long softExpire = 0;
+        long finalExpire = 0;
         long maxAge = 0;
+        long staleWhileRevalidate = 0;
         boolean hasCacheControl = false;
 
         String serverEtag = null;
@@ -69,6 +71,11 @@
                         maxAge = Long.parseLong(token.substring(8));
                     } catch (Exception e) {
                     }
+                } else if (token.startsWith("stale-while-revalidate=")) {
+                    try {
+                        staleWhileRevalidate = Long.parseLong(token.substring(23));
+                    } catch (Exception e) {
+                    }
                 } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
                     maxAge = 0;
                 }
@@ -91,16 +98,18 @@
         // is more restrictive.
         if (hasCacheControl) {
             softExpire = now + maxAge * 1000;
+            finalExpire = softExpire + staleWhileRevalidate * 1000;
         } else if (serverDate > 0 && serverExpires >= serverDate) {
             // Default semantic for Expire header in HTTP specification is softExpire.
             softExpire = now + (serverExpires - serverDate);
+            finalExpire = softExpire;
         }
 
         Cache.Entry entry = new Cache.Entry();
         entry.data = response.data;
         entry.etag = serverEtag;
         entry.softTtl = softExpire;
-        entry.ttl = entry.softTtl;
+        entry.ttl = finalExpire;
         entry.serverDate = serverDate;
         entry.lastModified = lastModified;
         entry.responseHeaders = headers;
diff --git a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
index 01ff2c2..60c2727 100644
--- a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
+++ b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
@@ -139,6 +139,24 @@
         assertEquals(entry.softTtl, entry.ttl);
     }
 
+    @Test public void testParseCacheHeaders_staleWhileRevalidate() {
+        long now = System.currentTimeMillis();
+        headers.put("Date", rfc1123Date(now));
+        headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS));
+
+        // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day
+        // - stale-while-revalidate (entry.ttl) indicates that the asset may
+        // continue to be served stale for up to additional 7 days
+        headers.put("Cache-Control", "max-age=86400, stale-while-revalidate=604800");
+
+        Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+
+        assertNotNull(entry);
+        assertNull(entry.etag);
+        assertEqualsWithin(now + 24 * ONE_HOUR_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS);
+        assertEqualsWithin(now + 8 * 24 * ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
+    }
+
     @Test public void parseCacheHeaders_cacheControlNoCache() {
         long now = System.currentTimeMillis();
         headers.put("Date", rfc1123Date(now));