jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/340906 )

Change subject: Update: add second OkHttp cache for saved content
......................................................................


Update: add second OkHttp cache for saved content

Bug: T156917
Change-Id: I45825777d18cbbc686d757a715002464cf94ae3f
---
M app/build.gradle
A app/src/main/java/okhttp3/CacheDelegate.java
A app/src/main/java/okhttp3/internal/cache/CacheDelegateInterceptor.java
M app/src/main/java/org/wikipedia/dataclient/okhttp/CacheIfErrorInterceptor.java
M app/src/main/java/org/wikipedia/dataclient/okhttp/OkHttpConnectionFactory.java
A 
app/src/main/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategy.java
A app/src/main/java/org/wikipedia/dataclient/okhttp/cache/SaveHeader.java
A app/src/test/java/okhttp3/internal/cache/CacheDelegateInterceptorTest.java
M app/src/test/java/org/wikipedia/dataclient/mwapi/page/MwPageClientTest.java
A 
app/src/test/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategyTest.java
A app/src/test/java/org/wikipedia/dataclient/okhttp/cache/SaveHeaderTest.java
M app/src/test/java/org/wikipedia/dataclient/restbase/page/RbPageClientTest.java
M app/src/test/java/org/wikipedia/test/MockWebServerTest.java
M gradle/src/checkstyle.gradle
14 files changed, 733 insertions(+), 31 deletions(-)

Approvals:
  Dbrant: Looks good to me, approved
  jenkins-bot: Verified
  Mholloway: Looks good to me, but someone else must approve



diff --git a/app/build.gradle b/app/build.gradle
index f81c165..99bb773 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -157,12 +157,14 @@
     // use http://gradleplease.appspot.com/ or http://search.maven.org/.
     // Debug with ./gradlew -q app:dependencies --configuration compile
 
-    String okHttpVersion = '3.6.0'
+    String okHttpVersion = '3.6.0' // When updating this version, resync file 
copies under
+                                   // app/src/main/java/okhttp3
     String retrofitVersion = '2.2.0'
     String supportVersion = '25.3.1'
     String espressoVersion = '2.2.2'
     String butterKnifeVersion = '8.4.0'
-    String frescoVersion = '1.1.0'
+    String frescoVersion = '1.1.0' // When updating this version, resync file 
copies under
+                                   // app/src/main/java/com/facebook
     String testingSupportVersion = '0.5'
     String mockitoCore = 'org.mockito:mockito-core:1.9.5'
     String leakCanaryVersion = '1.5'
@@ -192,6 +194,7 @@
     compile 'com.mobsandgeeks:android-saripaar:2.0.3'
     compile 'com.github.ryanjohn1:onboarding:1.0.3'
     compile "com.jakewharton:butterknife:$butterKnifeVersion"
+    // When upgrading the Mapbox version, remove the superfluous ProGuard rule 
and delete this comment
     compile('com.mapbox.mapboxsdk:mapbox-android-sdk:5.0.0@aar') {
         transitive = true
     }
diff --git a/app/src/main/java/okhttp3/CacheDelegate.java 
b/app/src/main/java/okhttp3/CacheDelegate.java
new file mode 100644
index 0000000..f696b21
--- /dev/null
+++ b/app/src/main/java/okhttp3/CacheDelegate.java
@@ -0,0 +1,53 @@
+package okhttp3;
+
+import android.support.annotation.NonNull;
+
+import java.io.IOException;
+
+import okhttp3.internal.Util;
+import okhttp3.internal.cache.DiskLruCache;
+import okhttp3.internal.cache.InternalCache;
+import okio.ByteString;
+
+public class CacheDelegate {
+    @NonNull public static InternalCache internalCache(@NonNull Cache cache) {
+        return cache.internalCache;
+    }
+
+    @NonNull private final Cache cache;
+
+    public CacheDelegate(@NonNull Cache cache) {
+        this.cache = cache;
+    }
+
+    // Copy of Cache.get(). Calling this method modifies the Cache. If the URL 
is present, it's
+    // cache entry is moved to the head of the LRU queue. This method performs 
file I/O
+    public boolean isCached(@NonNull String url) {
+        String key = key(url);
+        DiskLruCache.Snapshot snapshot;
+        try {
+            snapshot = cache.cache.get(key);
+            if (snapshot == null) {
+                return false;
+            }
+        } catch (IOException e) {
+            // Give up because the cache cannot be read.
+            return false;
+        }
+
+        Util.closeQuietly(snapshot);
+        return true;
+    }
+
+    // Copy of Cache.remove(). This method performs file I/O
+    public void remove(@NonNull Request req) {
+        try {
+            cache.remove(req);
+        } catch (IOException ignore) { }
+    }
+
+    // Copy of Cache.key()
+    @NonNull private String key(@NonNull String url) {
+        return ByteString.encodeUtf8(url).md5().hex();
+    }
+}
diff --git 
a/app/src/main/java/okhttp3/internal/cache/CacheDelegateInterceptor.java 
b/app/src/main/java/okhttp3/internal/cache/CacheDelegateInterceptor.java
new file mode 100644
index 0000000..2e70b70
--- /dev/null
+++ b/app/src/main/java/okhttp3/internal/cache/CacheDelegateInterceptor.java
@@ -0,0 +1,301 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package okhttp3.internal.cache;
+
+import org.wikipedia.dataclient.okhttp.cache.CacheDelegateStrategy;
+
+import java.io.IOException;
+
+import okhttp3.Headers;
+import okhttp3.Interceptor;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.internal.Internal;
+import okhttp3.internal.Util;
+import okhttp3.internal.http.HttpCodec;
+import okhttp3.internal.http.HttpHeaders;
+import okhttp3.internal.http.HttpMethod;
+import okhttp3.internal.http.RealResponseBody;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
+import okio.Timeout;
+
+import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static okhttp3.internal.Util.closeQuietly;
+import static okhttp3.internal.Util.discard;
+
+/** Serves requests from the cache and writes responses to the cache. Copied 
from
+ 
https://github.com/square/okhttp/blob/a27afaf/okhttp/src/main/java/okhttp3/internal/cache/CacheInterceptor.java
+ to allow a custom cache strategy. Deviations are marked with "Change:". */
+public final class CacheDelegateInterceptor implements Interceptor {
+  final InternalCache cache;
+  // Change: add secondaryCache member variable
+  final InternalCache secondaryCache;
+
+  // Change: add secondaryCache param
+  public CacheDelegateInterceptor(InternalCache cache, InternalCache 
secondaryCache) {
+    this.cache = cache;
+
+    // Change: initialize secondaryCache param
+    this.secondaryCache = secondaryCache;
+  }
+
+  @Override public Response intercept(Chain chain) throws IOException {
+    Response cacheCandidate = cache != null
+        ? cache.get(chain.request())
+        : null;
+
+    long now = System.currentTimeMillis();
+
+    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), 
cacheCandidate).get();
+    Request networkRequest = strategy.networkRequest;
+    Response cacheResponse = strategy.cacheResponse;
+
+    if (cache != null) {
+      cache.trackResponse(strategy);
+    }
+
+    // Change: the true cache further down the chain must be used directly to 
retrieve non-network
+    // responses
+    if (networkRequest == null && cacheResponse == null) {
+      cacheResponse = secondaryCache.get(chain.request());
+    }
+
+    if (cacheCandidate != null && cacheResponse == null) {
+      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't 
applicable. Close it.
+    }
+
+    // If we're forbidden from using the network and the cache is 
insufficient, fail.
+    if (networkRequest == null && cacheResponse == null) {
+      return new Response.Builder()
+          .request(chain.request())
+          .protocol(Protocol.HTTP_1_1)
+          .code(504)
+          .message("Unsatisfiable Request (only-if-cached)")
+          .body(Util.EMPTY_RESPONSE)
+          .sentRequestAtMillis(-1L)
+          .receivedResponseAtMillis(System.currentTimeMillis())
+          .build();
+    }
+
+    // If we don't need the network, we're done.
+    if (networkRequest == null) {
+      return cacheResponse.newBuilder()
+          .cacheResponse(stripBody(cacheResponse))
+          .build();
+    }
+
+    Response networkResponse = null;
+    try {
+      networkResponse = chain.proceed(networkRequest);
+    } finally {
+      // If we're crashing on I/O or otherwise, don't leak the cache body.
+      if (networkResponse == null && cacheCandidate != null) {
+        closeQuietly(cacheCandidate.body());
+      }
+    }
+
+    // Change: the original CacheInterceptor sets Response.Builder.*Response() 
but Response.Builder,
+    // invoked further down for the final Response, forbids a Response's cache 
/ network / prior
+    // Response to itself contain a cache / network / prior Response or a 
body. The body is already
+    // stripped by stripBody()
+    networkResponse = networkResponse.newBuilder()
+        .cacheResponse(null)
+        .networkResponse(null)
+        .priorResponse(null)
+        .build();
+
+    // If we have a cache response too, then we're doing a conditional get.
+    if (cacheResponse != null) {
+      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
+        Response response = cacheResponse.newBuilder()
+            .headers(combine(cacheResponse.headers(), 
networkResponse.headers()))
+            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
+            
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
+            .cacheResponse(stripBody(cacheResponse))
+            .networkResponse(stripBody(networkResponse))
+            .build();
+        networkResponse.body().close();
+
+        // Update the cache after combining headers but before stripping the
+        // Content-Encoding header (as performed by initContentStream()).
+        cache.trackConditionalCacheHit();
+        cache.update(cacheResponse, response);
+        return response;
+      } else {
+        closeQuietly(cacheResponse.body());
+      }
+    }
+
+    Response response = networkResponse.newBuilder()
+        .cacheResponse(stripBody(cacheResponse))
+        .networkResponse(stripBody(networkResponse))
+        .build();
+
+    if (HttpHeaders.hasBody(response)) {
+      // Change: add cacheCandidate parameter
+      CacheRequest cacheRequest = maybeCache(cacheCandidate, response, 
networkResponse.request(), cache);
+      response = cacheWritingResponse(cacheRequest, response);
+    }
+
+    return response;
+  }
+
+  private static Response stripBody(Response response) {
+    return response != null && response.body() != null
+        ? response.newBuilder().body(null).build()
+        : response;
+  }
+
+  // Change: add cacheCandidate parameter
+  private CacheRequest maybeCache(Response cacheCandidate, Response 
userResponse, Request networkRequest,
+      InternalCache responseCache) throws IOException {
+    if (responseCache == null) return null;
+
+    // Should we cache this response for this request?
+    if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
+      if (HttpMethod.invalidatesCache(networkRequest.method())) {
+        try {
+          responseCache.remove(networkRequest);
+        } catch (IOException ignored) {
+          // The cache cannot be written.
+        }
+      }
+      return null;
+    }
+
+    // Change: do not permit cache writes unless the page has been cached 
previously or the request
+    // is cacheable
+    if (cacheCandidate == null && 
!CacheDelegateStrategy.isCacheable(networkRequest)) {
+      return null;
+    }
+
+    // Offer this request to the cache.
+    return responseCache.put(userResponse);
+  }
+
+  /**
+   * Returns a new source that writes bytes to {@code cacheRequest} as they 
are read by the source
+   * consumer. This is careful to discard bytes left over when the stream is 
closed; otherwise we
+   * may never exhaust the source stream and therefore not complete the cached 
response.
+   */
+  private Response cacheWritingResponse(final CacheRequest cacheRequest, 
Response response)
+      throws IOException {
+    // Some apps return a null body; for compatibility we treat that like a 
null cache request.
+    if (cacheRequest == null) return response;
+    Sink cacheBodyUnbuffered = cacheRequest.body();
+    if (cacheBodyUnbuffered == null) return response;
+
+    final BufferedSource source = response.body().source();
+    final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
+
+    Source cacheWritingSource = new Source() {
+      boolean cacheRequestClosed;
+
+      @Override public long read(Buffer sink, long byteCount) throws 
IOException {
+        long bytesRead;
+        try {
+          bytesRead = source.read(sink, byteCount);
+        } catch (IOException e) {
+          if (!cacheRequestClosed) {
+            cacheRequestClosed = true;
+            cacheRequest.abort(); // Failed to write a complete cache response.
+          }
+          throw e;
+        }
+
+        if (bytesRead == -1) {
+          if (!cacheRequestClosed) {
+            cacheRequestClosed = true;
+            cacheBody.close(); // The cache response is complete!
+          }
+          return -1;
+        }
+
+        sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
+        cacheBody.emitCompleteSegments();
+        return bytesRead;
+      }
+
+      @Override public Timeout timeout() {
+        return source.timeout();
+      }
+
+      @Override public void close() throws IOException {
+        if (!cacheRequestClosed
+            && !discard(this, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS, 
MILLISECONDS)) {
+          cacheRequestClosed = true;
+          cacheRequest.abort();
+        }
+        source.close();
+      }
+    };
+
+    return response.newBuilder()
+        .body(new RealResponseBody(response.headers(), 
Okio.buffer(cacheWritingSource)))
+        .build();
+  }
+
+  /** Combines cached headers with a network headers as defined by RFC 2616, 
13.5.3. */
+  private static Headers combine(Headers cachedHeaders, Headers 
networkHeaders) {
+    Headers.Builder result = new Headers.Builder();
+
+    for (int i = 0, size = cachedHeaders.size(); i < size; i++) {
+      String fieldName = cachedHeaders.name(i);
+      String value = cachedHeaders.value(i);
+      if ("Warning".equalsIgnoreCase(fieldName) && value.startsWith("1")) {
+        continue; // Drop 100-level freshness warnings.
+      }
+      if (!isEndToEnd(fieldName) || networkHeaders.get(fieldName) == null) {
+        Internal.instance.addLenient(result, fieldName, value);
+      }
+    }
+
+    for (int i = 0, size = networkHeaders.size(); i < size; i++) {
+      String fieldName = networkHeaders.name(i);
+      if ("Content-Length".equalsIgnoreCase(fieldName)) {
+        continue; // Ignore content-length headers of validating responses.
+      }
+      if (isEndToEnd(fieldName)) {
+        Internal.instance.addLenient(result, fieldName, 
networkHeaders.value(i));
+      }
+    }
+
+    return result.build();
+  }
+
+  /**
+   * Returns true if {@code fieldName} is an end-to-end HTTP header, as 
defined by RFC 2616,
+   * 13.5.1.
+   */
+  static boolean isEndToEnd(String fieldName) {
+    return !"Connection".equalsIgnoreCase(fieldName)
+        && !"Keep-Alive".equalsIgnoreCase(fieldName)
+        && !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
+        && !"Proxy-Authorization".equalsIgnoreCase(fieldName)
+        && !"TE".equalsIgnoreCase(fieldName)
+        && !"Trailers".equalsIgnoreCase(fieldName)
+        && !"Transfer-Encoding".equalsIgnoreCase(fieldName)
+        && !"Upgrade".equalsIgnoreCase(fieldName);
+  }
+}
diff --git 
a/app/src/main/java/org/wikipedia/dataclient/okhttp/CacheIfErrorInterceptor.java
 
b/app/src/main/java/org/wikipedia/dataclient/okhttp/CacheIfErrorInterceptor.java
index 317e9f1..dee97fa 100644
--- 
a/app/src/main/java/org/wikipedia/dataclient/okhttp/CacheIfErrorInterceptor.java
+++ 
b/app/src/main/java/org/wikipedia/dataclient/okhttp/CacheIfErrorInterceptor.java
@@ -4,25 +4,32 @@
 
 import java.io.IOException;
 
+import okhttp3.CacheControl;
 import okhttp3.Interceptor;
 import okhttp3.Request;
 import okhttp3.Response;
 
 class CacheIfErrorInterceptor implements Interceptor {
     @Override public Response intercept(Chain chain) throws IOException {
-        Request req = chain.request();
-        Response rsp = chain.proceed(req);
+        Response rsp = null;
+        try {
+            rsp = 
chain.proceed(chain.request().newBuilder().cacheControl(CacheControl.FORCE_CACHE).build());
+        } catch (IOException ignore) { }
 
         // when max-stale is set, CacheStrategy forbids a conditional network 
request in
         // CacheInterceptor
-        if (rsp == null || stale(rsp)) {
-            Request netReq = forceNetRequest(req);
+        if (!chain.request().cacheControl().onlyIfCached()) {
+            Request netReq = forceNetRequest(chain.request());
             Response netRsp = null;
             try {
                 netRsp = chain.proceed(netReq);
-            } catch (IOException ignore) { }
+            } catch (IOException ignore) {
+                if (rsp == null) {
+                    throw ignore;
+                }
+            }
 
-            if (netRsp != null) {
+            if (netRsp != null && (netRsp.isSuccessful() || rsp == null || 
!rsp.isSuccessful())) {
                 return netRsp;
             }
         }
@@ -30,18 +37,8 @@
         return rsp;
     }
 
-    @NonNull static Request forceNetRequest(@NonNull Request req) {
+    @NonNull private Request forceNetRequest(@NonNull Request req) {
         String cacheControl = 
CacheControlUtil.forceNetRequest(req.cacheControl().toString());
         return req.newBuilder().header("Cache-Control", cacheControl).build();
-    }
-
-    private boolean stale(@NonNull Response response) {
-        final String staleRspCode = "110";
-        for (String header : response.headers("Warning")) {
-            if (header.startsWith(staleRspCode)) {
-                return true;
-            }
-        }
-        return false;
     }
 }
diff --git 
a/app/src/main/java/org/wikipedia/dataclient/okhttp/OkHttpConnectionFactory.java
 
b/app/src/main/java/org/wikipedia/dataclient/okhttp/OkHttpConnectionFactory.java
index 3063dc2..b5ac3cd 100644
--- 
a/app/src/main/java/org/wikipedia/dataclient/okhttp/OkHttpConnectionFactory.java
+++ 
b/app/src/main/java/org/wikipedia/dataclient/okhttp/OkHttpConnectionFactory.java
@@ -1,6 +1,7 @@
 package org.wikipedia.dataclient.okhttp;
 
 import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
 
 import com.github.kevinsawicki.http.HttpRequest;
 
@@ -9,22 +10,30 @@
 import org.wikipedia.settings.Prefs;
 import org.wikipedia.settings.RbSwitch;
 
+import java.io.File;
 import java.io.IOException;
 import java.net.HttpURLConnection;
 import java.net.Proxy;
 import java.net.URL;
 
 import okhttp3.Cache;
+import okhttp3.CacheDelegate;
 import okhttp3.CookieJar;
 import okhttp3.JavaNetCookieJar;
 import okhttp3.OkHttpClient;
 import okhttp3.OkUrlFactory;
+import okhttp3.internal.cache.CacheDelegateInterceptor;
 import okhttp3.logging.HttpLoggingInterceptor;
 
 public class OkHttpConnectionFactory implements HttpRequest.ConnectionFactory {
-    private static final long HTTP_CACHE_SIZE = 64 * 1024 * 1024;
-    @NonNull private static final Cache HTTP_CACHE = new 
Cache(WikipediaApp.getInstance().getCacheDir(),
-            HTTP_CACHE_SIZE);
+    private static final String CACHE_DIR_NAME = "okhttp-cache";
+    private static final long NET_CACHE_SIZE = 64 * 1024 * 1024;
+    @VisibleForTesting @NonNull public static final Cache NET_CACHE = new 
Cache(new File(WikipediaApp.getInstance().getCacheDir(),
+            CACHE_DIR_NAME), NET_CACHE_SIZE);
+    private static final long SAVED_PAGE_CACHE_SIZE = NET_CACHE_SIZE * 1024;
+    @NonNull public static final Cache SAVE_CACHE = new Cache(new 
File(WikipediaApp.getInstance().getFilesDir(),
+            CACHE_DIR_NAME), SAVED_PAGE_CACHE_SIZE);
+
     @NonNull private static final OkHttpClient CLIENT = createClient();
 
     @NonNull public static OkHttpClient getClient() {
@@ -50,7 +59,7 @@
 
         return new OkHttpClient.Builder()
                 .cookieJar(cookieJar)
-                .cache(HTTP_CACHE)
+                .cache(NET_CACHE)
                 .addInterceptor(new 
HttpLoggingInterceptor().setLevel(Prefs.getRetrofitLogLevel()))
                 .addInterceptor(new UnsuccessfulResponseInterceptor())
                 .addInterceptor(new 
StatusResponseInterceptor(RbSwitch.INSTANCE))
@@ -58,6 +67,7 @@
                 .addInterceptor(new CommonHeaderRequestInterceptor())
                 .addInterceptor(new DefaultMaxStaleRequestInterceptor())
                 .addInterceptor(new CacheIfErrorInterceptor())
+                .addInterceptor(new 
CacheDelegateInterceptor(CacheDelegate.internalCache(SAVE_CACHE), 
CacheDelegate.internalCache(NET_CACHE)))
                 .addInterceptor(new 
WikipediaZeroResponseInterceptor(WikipediaApp.getInstance().getWikipediaZeroHandler()))
                 // this interceptor should appear last since it examines the 
final cache and network responses
                 .addInterceptor(new 
ResponseLoggingInterceptor().setLevel(Prefs.getRetrofitLogLevel()))
diff --git 
a/app/src/main/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategy.java
 
b/app/src/main/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategy.java
new file mode 100644
index 0000000..33fbab9
--- /dev/null
+++ 
b/app/src/main/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategy.java
@@ -0,0 +1,13 @@
+package org.wikipedia.dataclient.okhttp.cache;
+
+import android.support.annotation.NonNull;
+
+import okhttp3.Request;
+
+public final class CacheDelegateStrategy {
+    public static boolean isCacheable(@NonNull Request request) {
+        return SaveHeader.isSaveEnabled(request.header(SaveHeader.FIELD));
+    }
+
+    private CacheDelegateStrategy() { }
+}
diff --git 
a/app/src/main/java/org/wikipedia/dataclient/okhttp/cache/SaveHeader.java 
b/app/src/main/java/org/wikipedia/dataclient/okhttp/cache/SaveHeader.java
new file mode 100644
index 0000000..0ccc004
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/dataclient/okhttp/cache/SaveHeader.java
@@ -0,0 +1,15 @@
+package org.wikipedia.dataclient.okhttp.cache;
+
+import android.support.annotation.Nullable;
+
+public final class SaveHeader {
+    public static final String FIELD = "X-Save";
+    public static final String VAL_ENABLED = "true";
+    public static final String VAL_DISABLED = "false";
+
+    public static boolean isSaveEnabled(@Nullable String val) {
+        return VAL_ENABLED.equalsIgnoreCase(val);
+    }
+
+    private SaveHeader() { }
+}
diff --git 
a/app/src/test/java/okhttp3/internal/cache/CacheDelegateInterceptorTest.java 
b/app/src/test/java/okhttp3/internal/cache/CacheDelegateInterceptorTest.java
new file mode 100644
index 0000000..ae835e0
--- /dev/null
+++ b/app/src/test/java/okhttp3/internal/cache/CacheDelegateInterceptorTest.java
@@ -0,0 +1,252 @@
+package okhttp3.internal.cache;
+
+import android.support.annotation.NonNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.wikipedia.dataclient.okhttp.HttpStatusException;
+import org.wikipedia.dataclient.okhttp.OkHttpConnectionFactory;
+import org.wikipedia.dataclient.okhttp.cache.SaveHeader;
+import org.wikipedia.test.MockWebServerTest;
+
+import okhttp3.CacheControl;
+import okhttp3.CacheDelegate;
+import okhttp3.Request;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class CacheDelegateInterceptorTest extends MockWebServerTest {
+    private static final String URL = "url";
+
+    @NonNull private final CacheDelegate netCache = new 
CacheDelegate(OkHttpConnectionFactory.NET_CACHE);
+    @NonNull private final CacheDelegate saveCache = new 
CacheDelegate(OkHttpConnectionFactory.SAVE_CACHE);
+
+    @Before public void setUp() throws Throwable {
+        super.setUp();
+
+        Request req = newRequest();
+        netCache.remove(req);
+        saveCache.remove(req);
+    }
+
+    // Sanity check that both caches are truly empty
+    @Test(expected = HttpStatusException.class) public void testAssumptions() 
throws Throwable {
+        Request req = newOnlyIfCachedRequest();
+
+        assertCached(netCache, req, false);
+        assertCached(saveCache, req, false);
+
+        executeRequest(req);
+    }
+
+    @Test public void testInterceptWriteNetCacheNoHeader() throws Throwable {
+        Request req = newRequest();
+        requestResponse("0", req);
+        assertCached(netCache, req, true);
+    }
+
+    @Test public void testInterceptWriteSaveCacheNoHeader() throws Throwable {
+        Request req = newRequest();
+        requestResponse("0", req);
+        assertCached(saveCache, req, false);
+    }
+
+    @Test public void testInterceptWriteNetCacheSaveHeaderEnabled() throws 
Throwable {
+        Request req = newSaveEnabledRequest();
+        requestResponse("0", req);
+        assertCached(netCache, req, true);
+    }
+
+    @Test public void testInterceptWriteSaveCacheSaveHeaderEnabled() throws 
Throwable {
+        Request req = newSaveEnabledRequest();
+        requestResponse("0", req);
+        assertCached(saveCache, req, true);
+    }
+
+    @Test public void testInterceptWriteNetCacheSaveHeaderDisabled() throws 
Throwable {
+        Request req = newSaveDisabledRequest();
+        requestResponse("0", req);
+        assertCached(netCache, req, true);
+    }
+
+    @Test public void testInterceptWriteSaveCacheSaveHeaderDisabled() throws 
Throwable {
+        Request req = newSaveDisabledRequest();
+        requestResponse("0", req);
+        assertCached(saveCache, req, false);
+    }
+
+    @Test public void testInterceptReadNetCacheNoHeader() throws Throwable {
+        Request req = newRequest();
+        requestResponse("0", req);
+
+        saveCache.remove(req);
+        requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
+    }
+
+    @Test public void testInterceptReadSaveCacheNoHeader() throws Throwable {
+        Request req = newRequest();
+        requestResponse("0", newSaveEnabledRequest());
+
+        netCache.remove(req);
+        requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
+    }
+
+    @Test public void testInterceptReadNetCacheSaveHeaderEnabled() throws 
Throwable {
+        Request req = newSaveEnabledRequest();
+        requestResponse("0", req);
+
+        saveCache.remove(req);
+        requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
+    }
+
+    @Test public void testInterceptReadSaveCacheSaveHeaderEnabled() throws 
Throwable {
+        Request req = newSaveEnabledRequest();
+        requestResponse("0", req);
+
+        netCache.remove(req);
+        requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
+    }
+
+    @Test public void testInterceptReadNetCacheSaveHeaderDisabled() throws 
Throwable {
+        Request req = newSaveDisabledRequest();
+        requestResponse("0", req);
+
+        saveCache.remove(req);
+        requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
+    }
+
+    @Test public void testInterceptReadSaveCacheSaveHeaderDisabled() throws 
Throwable {
+        Request req = newSaveDisabledRequest();
+        requestResponse("0", newSaveEnabledRequest());
+
+        netCache.remove(req);
+        requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
+    }
+
+    @Test public void testInterceptUpdateNetCacheNoHeader() throws Throwable {
+        Request req = newRequest();
+        requestResponse("0", req);
+        requestResponse("1", req);
+
+        saveCache.remove(req);
+        requestOnlyIfCachedResponse("1", newOnlyIfCachedRequest());
+    }
+
+    @Test public void testInterceptUpdateSaveCacheNoHeader() throws Throwable {
+        Request req = newRequest();
+        requestResponse("0", newSaveEnabledRequest());
+        requestResponse("1", req);
+
+        netCache.remove(req);
+        requestOnlyIfCachedResponse("1", newOnlyIfCachedRequest());
+    }
+
+    @Test public void testInterceptUpdateNetCacheSaveHeaderEnabled() throws 
Throwable {
+        Request req = newRequest();
+        requestResponse("0", req);
+        requestResponse("1", newSaveEnabledRequest());
+
+        saveCache.remove(req);
+        requestOnlyIfCachedResponse("1", newOnlyIfCachedRequest());
+    }
+
+    @Test public void testInterceptUpdateSaveCacheSaveHeaderEnabled() throws 
Throwable {
+        Request req = newRequest();
+        requestResponse("0", req);
+        requestResponse("1", newSaveEnabledRequest());
+
+        netCache.remove(req);
+        requestOnlyIfCachedResponse("1", newOnlyIfCachedRequest());
+    }
+
+    @Test public void 
testInterceptUpdateNetCacheSaveHeaderEnabledThenNoHeader() throws Throwable {
+        Request req = newRequest();
+        requestResponse("0", req);
+        requestResponse("1", newSaveEnabledRequest());
+
+        saveCache.remove(req);
+
+        requestResponse("2", req);
+
+        saveCache.remove(req);
+        requestOnlyIfCachedResponse("2", newOnlyIfCachedRequest());
+    }
+
+    @Test public void 
testInterceptUpdateSaveCacheSaveHeaderEnabledThenNoHeader() throws Throwable {
+        Request req = newRequest();
+        requestResponse("0", req);
+        requestResponse("1", newSaveEnabledRequest());
+
+        netCache.remove(req);
+
+        requestResponse("2", req);
+
+        netCache.remove(req);
+        requestOnlyIfCachedResponse("2", newOnlyIfCachedRequest());
+    }
+
+    @Test public void testInterceptErrorNetCache() throws Throwable {
+        Request req = newRequest();
+        requestResponse("0", req);
+
+        saveCache.remove(req);
+
+        requestResponseError("0", req);
+    }
+
+    @Test public void testInterceptErrorSaveCache() throws Throwable {
+        Request req = newSaveEnabledRequest();
+        requestResponse("0", req);
+
+        netCache.remove(req);
+
+        requestResponseError("0", req);
+    }
+
+    private void requestResponseError(@NonNull String body, @NonNull Request 
req) throws Throwable {
+        enqueue404();
+        String rsp = executeRequest(req);
+        server().takeRequest();
+        assertThat(rsp, is(body));
+    }
+
+    private void requestResponse(@NonNull String body, @NonNull Request req) 
throws Throwable {
+        server().enqueue(body);
+        String rsp = executeRequest(req);
+        server().takeRequest();
+        assertThat(rsp, is(body));
+    }
+
+    private void requestOnlyIfCachedResponse(@NonNull String body, @NonNull 
Request req) throws Throwable {
+        String rsp = executeRequest(req);
+        assertThat(rsp, is(body));
+    }
+
+    private String executeRequest(@NonNull Request req) throws Throwable {
+        // Note: raw non-Retrofit usage of OkHttp Requests requires that the 
Response body is read
+        // for the cache to be written.
+        return okHttpClient().newCall(req).execute().body().string();
+    }
+
+    @NonNull private Request newOnlyIfCachedRequest() {
+        return 
newRequest().newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
+    }
+
+    @NonNull private Request newSaveEnabledRequest() {
+        return newRequest().newBuilder().header(SaveHeader.FIELD, 
SaveHeader.VAL_ENABLED).build();
+    }
+
+    @NonNull private Request newSaveDisabledRequest() {
+        return newRequest().newBuilder().header(SaveHeader.FIELD, 
SaveHeader.VAL_DISABLED).build();
+    }
+
+    @NonNull private Request newRequest() {
+        return new Request.Builder().url(server().getUrl(URL)).build();
+    }
+
+    private void assertCached(@NonNull CacheDelegate cacheDelegate, @NonNull 
Request req,
+                              boolean cached) {
+        assertThat(cacheDelegate.isCached(req.url().toString()), is(cached));
+    }
+}
diff --git 
a/app/src/test/java/org/wikipedia/dataclient/mwapi/page/MwPageClientTest.java 
b/app/src/test/java/org/wikipedia/dataclient/mwapi/page/MwPageClientTest.java
index 1993765..1ef1445 100644
--- 
a/app/src/test/java/org/wikipedia/dataclient/mwapi/page/MwPageClientTest.java
+++ 
b/app/src/test/java/org/wikipedia/dataclient/mwapi/page/MwPageClientTest.java
@@ -15,7 +15,8 @@
 public class MwPageClientTest extends MockWebServerTest {
     private PageClient subject;
 
-    @Before public void setUp() {
+    @Before public void setUp() throws Throwable {
+        super.setUp();
         subject = new MwPageClient(service(MwPageService.class));
     }
 
diff --git 
a/app/src/test/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategyTest.java
 
b/app/src/test/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategyTest.java
new file mode 100644
index 0000000..76a0713
--- /dev/null
+++ 
b/app/src/test/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategyTest.java
@@ -0,0 +1,26 @@
+package org.wikipedia.dataclient.okhttp.cache;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.wikipedia.Constants;
+import org.wikipedia.test.TestRunner;
+
+import okhttp3.Request;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+@RunWith(TestRunner.class) public class CacheDelegateStrategyTest {
+    @Test public void testIsCacheableTrue() {
+        Request req = new Request.Builder()
+                .url(Constants.WIKIPEDIA_URL)
+                .addHeader(SaveHeader.FIELD, SaveHeader.VAL_ENABLED)
+                .build();
+        assertThat(CacheDelegateStrategy.isCacheable(req), is(true));
+    }
+
+    @Test public void testIsCacheableFalse() {
+        Request req = new 
Request.Builder().url(Constants.WIKIPEDIA_URL).build();
+        assertThat(CacheDelegateStrategy.isCacheable(req), is(false));
+    }
+}
diff --git 
a/app/src/test/java/org/wikipedia/dataclient/okhttp/cache/SaveHeaderTest.java 
b/app/src/test/java/org/wikipedia/dataclient/okhttp/cache/SaveHeaderTest.java
new file mode 100644
index 0000000..d36867c
--- /dev/null
+++ 
b/app/src/test/java/org/wikipedia/dataclient/okhttp/cache/SaveHeaderTest.java
@@ -0,0 +1,22 @@
+package org.wikipedia.dataclient.okhttp.cache;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.wikipedia.test.TestRunner;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+@RunWith(TestRunner.class) public class SaveHeaderTest {
+    @Test public void testIsSaveEnabledTrue() {
+        assertThat(SaveHeader.isSaveEnabled("true"), is(true));
+    }
+
+    @Test public void testIsSaveEnabledFalse() {
+        assertThat(SaveHeader.isSaveEnabled("false"), is(false));
+    }
+
+    @Test public void testIsSaveEnabledCaseInsensitive() {
+        assertThat(SaveHeader.isSaveEnabled("TRUE"), is(true));
+    }
+}
diff --git 
a/app/src/test/java/org/wikipedia/dataclient/restbase/page/RbPageClientTest.java
 
b/app/src/test/java/org/wikipedia/dataclient/restbase/page/RbPageClientTest.java
index b326379..0eee3e6 100644
--- 
a/app/src/test/java/org/wikipedia/dataclient/restbase/page/RbPageClientTest.java
+++ 
b/app/src/test/java/org/wikipedia/dataclient/restbase/page/RbPageClientTest.java
@@ -15,7 +15,8 @@
 public class RbPageClientTest extends MockWebServerTest {
     private PageClient subject;
 
-    @Before public void setUp() {
+    @Before public void setUp() throws Throwable {
+        super.setUp();
         subject = new RbPageClient(service(RbPageService.class));
     }
 
diff --git a/app/src/test/java/org/wikipedia/test/MockWebServerTest.java 
b/app/src/test/java/org/wikipedia/test/MockWebServerTest.java
index 0cbf765..0df67d4 100644
--- a/app/src/test/java/org/wikipedia/test/MockWebServerTest.java
+++ b/app/src/test/java/org/wikipedia/test/MockWebServerTest.java
@@ -16,9 +16,15 @@
 
 @RunWith(TestRunner.class)
 public abstract class MockWebServerTest {
+    private OkHttpClient okHttpClient;
     private final TestWebServer server = new TestWebServer();
 
     @Before public void setUp() throws Throwable {
+        okHttpClient = OkHttpConnectionFactory
+                .getClient()
+                .newBuilder()
+                .dispatcher(new Dispatcher(new ImmediateExecutorService()))
+                .build();
         server.setUp();
     }
 
@@ -44,18 +50,19 @@
         server.enqueue(new MockResponse().setBody("{}"));
     }
 
-    @NonNull public <T> T service(Class<T> clazz) {
+    @NonNull protected OkHttpClient okHttpClient() {
+        return okHttpClient;
+    }
+
+    @NonNull protected <T> T service(Class<T> clazz) {
         return service(clazz, server().getUrl());
     }
 
-    @NonNull public <T> T service(Class<T> clazz, @NonNull String url) {
-        OkHttpClient okHttp = OkHttpConnectionFactory.getClient().newBuilder()
-                .dispatcher(new Dispatcher(new ImmediateExecutorService()))
-                .build();
+    @NonNull protected <T> T service(Class<T> clazz, @NonNull String url) {
         return new Retrofit.Builder()
                 .baseUrl(url)
                 .callbackExecutor(new ImmediateExecutor())
-                .client(okHttp)
+                .client(okHttpClient)
                 
.addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson()))
                 .build()
                 .create(clazz);
diff --git a/gradle/src/checkstyle.gradle b/gradle/src/checkstyle.gradle
index 325e80b..80f4b07 100644
--- a/gradle/src/checkstyle.gradle
+++ b/gradle/src/checkstyle.gradle
@@ -16,6 +16,7 @@
     include '**/*.java'
     exclude '**/gen/**'
     exclude '**/com/facebook/samples/**/*.java'
+    exclude '**/okhttp3/**/*.java'
 
     classpath = configurations.compile
 }
\ No newline at end of file

-- 
To view, visit https://gerrit.wikimedia.org/r/340906
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I45825777d18cbbc686d757a715002464cf94ae3f
Gerrit-PatchSet: 7
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Niedzielski <[email protected]>
Gerrit-Reviewer: Brion VIBBER <[email protected]>
Gerrit-Reviewer: Dbrant <[email protected]>
Gerrit-Reviewer: Mholloway <[email protected]>
Gerrit-Reviewer: Niedzielski <[email protected]>
Gerrit-Reviewer: Sniedzielski <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to