Modify header parser to handle must-revalidate.

This flag indicates that the response must NOT be returned after the
cache TTL has expired, but it does not mandate that the response
should not be cached at all (which the code was doing previously).

Change-Id: I61532f3aa8144c50dcee442dc30215bb81ada868
diff --git a/local.properties b/local.properties
new file mode 100644
index 0000000..4de71c4
--- /dev/null
+++ b/local.properties
@@ -0,0 +1,11 @@
+## This file is automatically generated by Android Studio.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+#Wed Jan 28 16:21:52 PST 2015
+sdk.dir=/usr/local/google/clients/android/gcore-orla/prebuilts/fullsdk/linux
diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
index 7306052..c3b48d8 100644
--- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
+++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
@@ -49,6 +49,7 @@
         long maxAge = 0;
         long staleWhileRevalidate = 0;
         boolean hasCacheControl = false;
+        boolean mustRevalidate = false;
 
         String serverEtag = null;
         String headerValue;
@@ -77,7 +78,7 @@
                     } catch (Exception e) {
                     }
                 } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
-                    maxAge = 0;
+                    mustRevalidate = true;
                 }
             }
         }
@@ -98,7 +99,9 @@
         // is more restrictive.
         if (hasCacheControl) {
             softExpire = now + maxAge * 1000;
-            finalExpire = softExpire + staleWhileRevalidate * 1000;
+            finalExpire = mustRevalidate
+                    ? softExpire
+                    : softExpire + staleWhileRevalidate * 1000;
         } else if (serverDate > 0 && serverExpires >= serverDate) {
             // Default semantic for Expire header in HTTP specification is softExpire.
             softExpire = now + (serverExpires - serverDate);
diff --git a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
index f9230c6..fd8cf51 100644
--- a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
+++ b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
@@ -41,6 +41,7 @@
     private static long ONE_MINUTE_MILLIS = 1000L * 60;
     private static long ONE_HOUR_MILLIS = 1000L * 60 * 60;
     private static long ONE_DAY_MILLIS = ONE_HOUR_MILLIS * 24;
+    private static long ONE_WEEK_MILLIS = ONE_DAY_MILLIS * 7;
 
     private NetworkResponse response;
     private Map<String, String> headers;
@@ -135,7 +136,7 @@
 
         assertNotNull(entry);
         assertNull(entry.etag);
-        assertEqualsWithin(now + 24 * ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
+        assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
         assertEquals(entry.softTtl, entry.ttl);
     }
 
@@ -153,8 +154,8 @@
 
         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);
+        assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS);
+        assertEqualsWithin(now + ONE_DAY_MILLIS + ONE_WEEK_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
     }
 
     @Test public void parseCacheHeaders_cacheControlNoCache() {
@@ -168,20 +169,51 @@
         assertNull(entry);
     }
 
-    @Test public void parseCacheHeaders_cacheControlMustRevalidate() {
+    @Test public void parseCacheHeaders_cacheControlMustRevalidateNoMaxAge() {
         long now = System.currentTimeMillis();
         headers.put("Date", rfc1123Date(now));
         headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS));
         headers.put("Cache-Control", "must-revalidate");
 
         Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
-
         assertNotNull(entry);
         assertNull(entry.etag);
         assertEqualsWithin(now, entry.ttl, ONE_MINUTE_MILLIS);
         assertEquals(entry.softTtl, entry.ttl);
     }
 
+    @Test public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAge() {
+        long now = System.currentTimeMillis();
+        headers.put("Date", rfc1123Date(now));
+        headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS));
+        headers.put("Cache-Control", "must-revalidate, max-age=3600");
+
+        Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+        assertNotNull(entry);
+        assertNull(entry.etag);
+        assertEqualsWithin(now + ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
+        assertEquals(entry.softTtl, entry.ttl);
+    }
+
+    @Test public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAgeAndStale() {
+        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, but this is
+        // ignored in this case because of the must-revalidate header.
+        headers.put("Cache-Control",
+                "must-revalidate, max-age=86400, stale-while-revalidate=604800");
+
+        Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+        assertNotNull(entry);
+        assertNull(entry.etag);
+        assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS);
+        assertEquals(entry.softTtl, entry.ttl);
+    }
+
     private void assertEqualsWithin(long expected, long value, long fudgeFactor) {
         long diff = Math.abs(expected - value);
         assertTrue(diff < fudgeFactor);
@@ -253,7 +285,7 @@
 
         assertNotNull(entry);
         assertEquals("Yow!", entry.etag);
-        assertEqualsWithin(now + 24 * ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
+        assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
         assertEquals(entry.softTtl, entry.ttl);
         assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers));
     }