You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ia...@apache.org on 2013/04/29 15:39:56 UTC

[7/7] android commit: [CB-2431] Update to okhttp to include jwilson's recovery fixes [f38fec5b]

[CB-2431] Update to okhttp to include jwilson's recovery fixes [f38fec5b]


Project: http://git-wip-us.apache.org/repos/asf/cordova-android/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-android/commit/cbb0bd5e
Tree: http://git-wip-us.apache.org/repos/asf/cordova-android/tree/cbb0bd5e
Diff: http://git-wip-us.apache.org/repos/asf/cordova-android/diff/cbb0bd5e

Branch: refs/heads/master
Commit: cbb0bd5ee79639ef6ef5a9dda6326a3f424863bb
Parents: 553a25c
Author: Ian Clelland <ic...@chromium.org>
Authored: Fri Apr 26 00:27:43 2013 -0400
Committer: Ian Clelland <ic...@chromium.org>
Committed: Mon Apr 29 09:37:53 2013 -0400

----------------------------------------------------------------------
 framework/src/com/squareup/okhttp/Connection.java  |   86 +--
 .../src/com/squareup/okhttp/ConnectionPool.java    |   18 +-
 .../src/com/squareup/okhttp/HttpResponseCache.java |  693 +++++++++++++++
 .../src/com/squareup/okhttp/OkHttpClient.java      |   24 +-
 framework/src/com/squareup/okhttp/Route.java       |   91 ++
 .../okhttp/internal/AbstractOutputStream.java      |   45 +
 .../com/squareup/okhttp/internal/DiskLruCache.java |  362 +++++---
 .../internal/FaultRecoveringOutputStream.java      |  163 ++++
 .../src/com/squareup/okhttp/internal/Platform.java |   74 ++-
 .../squareup/okhttp/internal/StrictLineReader.java |   71 +-
 .../src/com/squareup/okhttp/internal/Util.java     |    8 +-
 .../squareup/okhttp/internal/http/HttpEngine.java  |   24 +-
 .../okhttp/internal/http/HttpTransport.java        |   22 +-
 .../internal/http/HttpURLConnectionImpl.java       |  106 ++-
 .../internal/http/HttpsURLConnectionImpl.java      |   17 +-
 .../okhttp/internal/http/OkResponseCache.java      |   55 ++
 .../internal/http/OkResponseCacheAdapter.java      |   53 ++
 .../squareup/okhttp/internal/http/RawHeaders.java  |   39 +-
 .../okhttp/internal/http/RequestHeaders.java       |    2 +-
 .../okhttp/internal/http/ResponseHeaders.java      |    5 +-
 .../internal/http/RetryableOutputStream.java       |    3 +-
 .../okhttp/internal/http/RouteSelector.java        |   53 +-
 .../okhttp/internal/spdy/SpdyConnection.java       |    6 +-
 .../squareup/okhttp/internal/spdy/SpdyReader.java  |   76 +-
 24 files changed, 1705 insertions(+), 391 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-android/blob/cbb0bd5e/framework/src/com/squareup/okhttp/Connection.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/Connection.java b/framework/src/com/squareup/okhttp/Connection.java
index 90424ff..6a6c84d 100644
--- a/framework/src/com/squareup/okhttp/Connection.java
+++ b/framework/src/com/squareup/okhttp/Connection.java
@@ -24,11 +24,11 @@ import com.squareup.okhttp.internal.http.RawHeaders;
 import com.squareup.okhttp.internal.http.SpdyTransport;
 import com.squareup.okhttp.internal.spdy.SpdyConnection;
 import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.net.InetSocketAddress;
 import java.net.Proxy;
 import java.net.Socket;
 import java.net.URL;
@@ -76,10 +76,7 @@ public final class Connection implements Closeable {
       'h', 't', 't', 'p', '/', '1', '.', '1'
   };
 
-  private final Address address;
-  private final Proxy proxy;
-  private final InetSocketAddress inetSocketAddress;
-  private final boolean modernTls;
+  private final Route route;
 
   private Socket socket;
   private InputStream in;
@@ -89,15 +86,8 @@ public final class Connection implements Closeable {
   private int httpMinorVersion = 1; // Assume HTTP/1.1
   private long idleStartTimeNs;
 
-  public Connection(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
-      boolean modernTls) {
-    if (address == null) throw new NullPointerException("address == null");
-    if (proxy == null) throw new NullPointerException("proxy == null");
-    if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null");
-    this.address = address;
-    this.proxy = proxy;
-    this.inetSocketAddress = inetSocketAddress;
-    this.modernTls = modernTls;
+  public Connection(Route route) {
+    this.route = route;
   }
 
   public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
@@ -106,21 +96,20 @@ public final class Connection implements Closeable {
       throw new IllegalStateException("already connected");
     }
     connected = true;
-    socket = (proxy.type() != Proxy.Type.HTTP) ? new Socket(proxy) : new Socket();
-    socket.connect(inetSocketAddress, connectTimeout);
+    socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket();
+    socket.connect(route.inetSocketAddress, connectTimeout);
     socket.setSoTimeout(readTimeout);
     in = socket.getInputStream();
     out = socket.getOutputStream();
 
-    if (address.sslSocketFactory != null) {
+    if (route.address.sslSocketFactory != null) {
       upgradeToTls(tunnelRequest);
     }
 
-    // Buffer the socket stream to permit efficient parsing of HTTP headers and chunk sizes.
-    if (!isSpdy()) {
-      int bufferSize = 128;
-      in = new BufferedInputStream(in, bufferSize);
-    }
+    // Use MTU-sized buffers to send fewer packets.
+    int mtu = Platform.get().getMtu(socket);
+    in = new BufferedInputStream(in, mtu);
+    out = new BufferedOutputStream(out, mtu);
   }
 
   /**
@@ -136,16 +125,16 @@ public final class Connection implements Closeable {
     }
 
     // Create the wrapper over connected socket.
-    socket = address.sslSocketFactory
-        .createSocket(socket, address.uriHost, address.uriPort, true /* autoClose */);
+    socket = route.address.sslSocketFactory
+        .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */);
     SSLSocket sslSocket = (SSLSocket) socket;
-    if (modernTls) {
-      platform.enableTlsExtensions(sslSocket, address.uriHost);
+    if (route.modernTls) {
+      platform.enableTlsExtensions(sslSocket, route.address.uriHost);
     } else {
       platform.supportTlsIntolerantServer(sslSocket);
     }
 
-    if (modernTls) {
+    if (route.modernTls) {
       platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
     }
 
@@ -153,18 +142,20 @@ public final class Connection implements Closeable {
     sslSocket.startHandshake();
 
     // Verify that the socket's certificates are acceptable for the target host.
-    if (!address.hostnameVerifier.verify(address.uriHost, sslSocket.getSession())) {
-      throw new IOException("Hostname '" + address.uriHost + "' was not verified");
+    if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) {
+      throw new IOException("Hostname '" + route.address.uriHost + "' was not verified");
     }
 
     out = sslSocket.getOutputStream();
     in = sslSocket.getInputStream();
 
     byte[] selectedProtocol;
-    if (modernTls && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
+    if (route.modernTls
+        && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
       if (Arrays.equals(selectedProtocol, SPDY3)) {
         sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
-        spdyConnection = new SpdyConnection.Builder(address.getUriHost(), true, in, out).build();
+        spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out)
+            .build();
       } else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
         throw new IOException(
             "Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1"));
@@ -181,29 +172,9 @@ public final class Connection implements Closeable {
     socket.close();
   }
 
-  /**
-   * Returns the proxy that this connection is using.
-   *
-   * <strong>Warning:</strong> This may be different than the proxy returned
-   * by {@link #getAddress}! That is the proxy that the user asked to be
-   * connected to; this returns the proxy that they were actually connected
-   * to. The two may disagree when a proxy selector selects a different proxy
-   * for a connection.
-   */
-  public Proxy getProxy() {
-    return proxy;
-  }
-
-  public Address getAddress() {
-    return address;
-  }
-
-  public InetSocketAddress getSocketAddress() {
-    return inetSocketAddress;
-  }
-
-  public boolean isModernTls() {
-    return modernTls;
+  /** Returns the route used by this connection. */
+  public Route getRoute() {
+    return route;
   }
 
   /**
@@ -284,7 +255,7 @@ public final class Connection implements Closeable {
    * we must avoid buffering bytes intended for the higher-level protocol.
    */
   public boolean requiresTunnel() {
-    return address.sslSocketFactory != null && proxy != null && proxy.type() == Proxy.Type.HTTP;
+    return route.address.sslSocketFactory != null && route.proxy.type() == Proxy.Type.HTTP;
   }
 
   /**
@@ -304,9 +275,8 @@ public final class Connection implements Closeable {
         case HTTP_PROXY_AUTH:
           requestHeaders = new RawHeaders(requestHeaders);
           URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/");
-          boolean credentialsFound =
-              HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH, responseHeaders, requestHeaders,
-                  proxy, url);
+          boolean credentialsFound = HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH,
+              responseHeaders, requestHeaders, route.proxy, url);
           if (credentialsFound) {
             continue;
           } else {

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/cbb0bd5e/framework/src/com/squareup/okhttp/ConnectionPool.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/ConnectionPool.java b/framework/src/com/squareup/okhttp/ConnectionPool.java
index 4eff4ec..933bd73 100644
--- a/framework/src/com/squareup/okhttp/ConnectionPool.java
+++ b/framework/src/com/squareup/okhttp/ConnectionPool.java
@@ -1,3 +1,19 @@
+/*
+ *  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 com.squareup.okhttp;
 
 import com.squareup.okhttp.internal.Platform;
@@ -164,7 +180,7 @@ public class ConnectionPool {
     for (ListIterator<Connection> i = connections.listIterator(connections.size());
         i.hasPrevious(); ) {
       Connection connection = i.previous();
-      if (!connection.getAddress().equals(address)
+      if (!connection.getRoute().getAddress().equals(address)
           || !connection.isAlive()
           || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
         continue;

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/cbb0bd5e/framework/src/com/squareup/okhttp/HttpResponseCache.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/HttpResponseCache.java b/framework/src/com/squareup/okhttp/HttpResponseCache.java
new file mode 100644
index 0000000..a6d380a
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/HttpResponseCache.java
@@ -0,0 +1,693 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed 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 com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.Base64;
+import com.squareup.okhttp.internal.DiskLruCache;
+import com.squareup.okhttp.internal.StrictLineReader;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.HttpEngine;
+import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
+import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
+import com.squareup.okhttp.internal.http.OkResponseCache;
+import com.squareup.okhttp.internal.http.RawHeaders;
+import com.squareup.okhttp.internal.http.ResponseHeaders;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FilterInputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.ResponseCache;
+import java.net.SecureCacheResponse;
+import java.net.URI;
+import java.net.URLConnection;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+import static com.squareup.okhttp.internal.Util.US_ASCII;
+import static com.squareup.okhttp.internal.Util.UTF_8;
+
+/**
+ * Caches HTTP and HTTPS responses to the filesystem so they may be reused,
+ * saving time and bandwidth.
+ *
+ * <h3>Cache Optimization</h3>
+ * To measure cache effectiveness, this class tracks three statistics:
+ * <ul>
+ *     <li><strong>{@link #getRequestCount() Request Count:}</strong> the number
+ *         of HTTP requests issued since this cache was created.
+ *     <li><strong>{@link #getNetworkCount() Network Count:}</strong> the
+ *         number of those requests that required network use.
+ *     <li><strong>{@link #getHitCount() Hit Count:}</strong> the number of
+ *         those requests whose responses were served by the cache.
+ * </ul>
+ * Sometimes a request will result in a conditional cache hit. If the cache
+ * contains a stale copy of the response, the client will issue a conditional
+ * {@code GET}. The server will then send either the updated response if it has
+ * changed, or a short 'not modified' response if the client's copy is still
+ * valid. Such responses increment both the network count and hit count.
+ *
+ * <p>The best way to improve the cache hit rate is by configuring the web
+ * server to return cacheable responses. Although this client honors all <a
+ * href="http://www.ietf.org/rfc/rfc2616.txt">HTTP/1.1 (RFC 2068)</a> cache
+ * headers, it doesn't cache partial responses.
+ *
+ * <h3>Force a Network Response</h3>
+ * In some situations, such as after a user clicks a 'refresh' button, it may be
+ * necessary to skip the cache, and fetch data directly from the server. To force
+ * a full refresh, add the {@code no-cache} directive: <pre>   {@code
+ *         connection.addRequestProperty("Cache-Control", "no-cache");
+ * }</pre>
+ * If it is only necessary to force a cached response to be validated by the
+ * server, use the more efficient {@code max-age=0} instead: <pre>   {@code
+ *         connection.addRequestProperty("Cache-Control", "max-age=0");
+ * }</pre>
+ *
+ * <h3>Force a Cache Response</h3>
+ * Sometimes you'll want to show resources if they are available immediately,
+ * but not otherwise. This can be used so your application can show
+ * <i>something</i> while waiting for the latest data to be downloaded. To
+ * restrict a request to locally-cached resources, add the {@code
+ * only-if-cached} directive: <pre>   {@code
+ *     try {
+ *         connection.addRequestProperty("Cache-Control", "only-if-cached");
+ *         InputStream cached = connection.getInputStream();
+ *         // the resource was cached! show it
+ *     } catch (FileNotFoundException e) {
+ *         // the resource was not cached
+ *     }
+ * }</pre>
+ * This technique works even better in situations where a stale response is
+ * better than no response. To permit stale cached responses, use the {@code
+ * max-stale} directive with the maximum staleness in seconds: <pre>   {@code
+ *         int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
+ *         connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);
+ * }</pre>
+ */
+public final class HttpResponseCache extends ResponseCache {
+  private static final char[] DIGITS =
+      { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+  // TODO: add APIs to iterate the cache?
+  private static final int VERSION = 201105;
+  private static final int ENTRY_METADATA = 0;
+  private static final int ENTRY_BODY = 1;
+  private static final int ENTRY_COUNT = 2;
+
+  private final DiskLruCache cache;
+
+  /* read and write statistics, all guarded by 'this' */
+  private int writeSuccessCount;
+  private int writeAbortCount;
+  private int networkCount;
+  private int hitCount;
+  private int requestCount;
+
+  /**
+   * Although this class only exposes the limited ResponseCache API, it
+   * implements the full OkResponseCache interface. This field is used as a
+   * package private handle to the complete implementation. It delegates to
+   * public and private members of this type.
+   */
+  final OkResponseCache okResponseCache = new OkResponseCache() {
+    @Override public CacheResponse get(URI uri, String requestMethod,
+        Map<String, List<String>> requestHeaders) throws IOException {
+      return HttpResponseCache.this.get(uri, requestMethod, requestHeaders);
+    }
+
+    @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+      return HttpResponseCache.this.put(uri, connection);
+    }
+
+    @Override public void update(
+        CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException {
+      HttpResponseCache.this.update(conditionalCacheHit, connection);
+    }
+
+    @Override public void trackConditionalCacheHit() {
+      HttpResponseCache.this.trackConditionalCacheHit();
+    }
+
+    @Override public void trackResponse(ResponseSource source) {
+      HttpResponseCache.this.trackResponse(source);
+    }
+  };
+
+  public HttpResponseCache(File directory, long maxSize) throws IOException {
+    cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize);
+  }
+
+  private String uriToKey(URI uri) {
+    try {
+      MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+      byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8"));
+      return bytesToHexString(md5bytes);
+    } catch (NoSuchAlgorithmException e) {
+      throw new AssertionError(e);
+    } catch (UnsupportedEncodingException e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  private static String bytesToHexString(byte[] bytes) {
+    char[] digits = DIGITS;
+    char[] buf = new char[bytes.length * 2];
+    int c = 0;
+    for (byte b : bytes) {
+      buf[c++] = digits[(b >> 4) & 0xf];
+      buf[c++] = digits[b & 0xf];
+    }
+    return new String(buf);
+  }
+
+  @Override public CacheResponse get(URI uri, String requestMethod,
+      Map<String, List<String>> requestHeaders) {
+    String key = uriToKey(uri);
+    DiskLruCache.Snapshot snapshot;
+    Entry entry;
+    try {
+      snapshot = cache.get(key);
+      if (snapshot == null) {
+        return null;
+      }
+      entry = new Entry(snapshot.getInputStream(ENTRY_METADATA));
+    } catch (IOException e) {
+      // Give up because the cache cannot be read.
+      return null;
+    }
+
+    if (!entry.matches(uri, requestMethod, requestHeaders)) {
+      snapshot.close();
+      return null;
+    }
+
+    return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot)
+        : new EntryCacheResponse(entry, snapshot);
+  }
+
+  @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+    if (!(urlConnection instanceof HttpURLConnection)) {
+      return null;
+    }
+
+    HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
+    String requestMethod = httpConnection.getRequestMethod();
+    String key = uriToKey(uri);
+
+    if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
+        "DELETE")) {
+      try {
+        cache.remove(key);
+      } catch (IOException ignored) {
+        // The cache cannot be written.
+      }
+      return null;
+    } else if (!requestMethod.equals("GET")) {
+      // Don't cache non-GET responses. We're technically allowed to cache
+      // HEAD requests and some POST requests, but the complexity of doing
+      // so is high and the benefit is low.
+      return null;
+    }
+
+    HttpEngine httpEngine = getHttpEngine(httpConnection);
+    if (httpEngine == null) {
+      // Don't cache unless the HTTP implementation is ours.
+      return null;
+    }
+
+    ResponseHeaders response = httpEngine.getResponseHeaders();
+    if (response.hasVaryAll()) {
+      return null;
+    }
+
+    RawHeaders varyHeaders =
+        httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
+    Entry entry = new Entry(uri, varyHeaders, httpConnection);
+    DiskLruCache.Editor editor = null;
+    try {
+      editor = cache.edit(key);
+      if (editor == null) {
+        return null;
+      }
+      entry.writeTo(editor);
+      return new CacheRequestImpl(editor);
+    } catch (IOException e) {
+      abortQuietly(editor);
+      return null;
+    }
+  }
+
+  private void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
+      throws IOException {
+    HttpEngine httpEngine = getHttpEngine(httpConnection);
+    URI uri = httpEngine.getUri();
+    ResponseHeaders response = httpEngine.getResponseHeaders();
+    RawHeaders varyHeaders =
+        httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
+    Entry entry = new Entry(uri, varyHeaders, httpConnection);
+    DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse)
+        ? ((EntryCacheResponse) conditionalCacheHit).snapshot
+        : ((EntrySecureCacheResponse) conditionalCacheHit).snapshot;
+    DiskLruCache.Editor editor = null;
+    try {
+      editor = snapshot.edit(); // returns null if snapshot is not current
+      if (editor != null) {
+        entry.writeTo(editor);
+        editor.commit();
+      }
+    } catch (IOException e) {
+      abortQuietly(editor);
+    }
+  }
+
+  private void abortQuietly(DiskLruCache.Editor editor) {
+    // Give up because the cache cannot be written.
+    try {
+      if (editor != null) {
+        editor.abort();
+      }
+    } catch (IOException ignored) {
+    }
+  }
+
+  private HttpEngine getHttpEngine(URLConnection httpConnection) {
+    if (httpConnection instanceof HttpURLConnectionImpl) {
+      return ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
+    } else if (httpConnection instanceof HttpsURLConnectionImpl) {
+      return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine();
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   * Closes the cache and deletes all of its stored values. This will delete
+   * all files in the cache directory including files that weren't created by
+   * the cache.
+   */
+  public void delete() throws IOException {
+    cache.delete();
+  }
+
+  public synchronized int getWriteAbortCount() {
+    return writeAbortCount;
+  }
+
+  public synchronized int getWriteSuccessCount() {
+    return writeSuccessCount;
+  }
+
+  private synchronized void trackResponse(ResponseSource source) {
+    requestCount++;
+
+    switch (source) {
+      case CACHE:
+        hitCount++;
+        break;
+      case CONDITIONAL_CACHE:
+      case NETWORK:
+        networkCount++;
+        break;
+    }
+  }
+
+  private synchronized void trackConditionalCacheHit() {
+    hitCount++;
+  }
+
+  public synchronized int getNetworkCount() {
+    return networkCount;
+  }
+
+  public synchronized int getHitCount() {
+    return hitCount;
+  }
+
+  public synchronized int getRequestCount() {
+    return requestCount;
+  }
+
+  private final class CacheRequestImpl extends CacheRequest {
+    private final DiskLruCache.Editor editor;
+    private OutputStream cacheOut;
+    private boolean done;
+    private OutputStream body;
+
+    public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
+      this.editor = editor;
+      this.cacheOut = editor.newOutputStream(ENTRY_BODY);
+      this.body = new FilterOutputStream(cacheOut) {
+        @Override public void close() throws IOException {
+          synchronized (HttpResponseCache.this) {
+            if (done) {
+              return;
+            }
+            done = true;
+            writeSuccessCount++;
+          }
+          super.close();
+          editor.commit();
+        }
+
+        @Override
+        public void write(byte[] buffer, int offset, int length) throws IOException {
+          // Since we don't override "write(int oneByte)", we can write directly to "out"
+          // and avoid the inefficient implementation from the FilterOutputStream.
+          out.write(buffer, offset, length);
+        }
+      };
+    }
+
+    @Override public void abort() {
+      synchronized (HttpResponseCache.this) {
+        if (done) {
+          return;
+        }
+        done = true;
+        writeAbortCount++;
+      }
+      Util.closeQuietly(cacheOut);
+      try {
+        editor.abort();
+      } catch (IOException ignored) {
+      }
+    }
+
+    @Override public OutputStream getBody() throws IOException {
+      return body;
+    }
+  }
+
+  private static final class Entry {
+    private final String uri;
+    private final RawHeaders varyHeaders;
+    private final String requestMethod;
+    private final RawHeaders responseHeaders;
+    private final String cipherSuite;
+    private final Certificate[] peerCertificates;
+    private final Certificate[] localCertificates;
+
+    /**
+     * Reads an entry from an input stream. A typical entry looks like this:
+     * <pre>{@code
+     *   http://google.com/foo
+     *   GET
+     *   2
+     *   Accept-Language: fr-CA
+     *   Accept-Charset: UTF-8
+     *   HTTP/1.1 200 OK
+     *   3
+     *   Content-Type: image/png
+     *   Content-Length: 100
+     *   Cache-Control: max-age=600
+     * }</pre>
+     *
+     * <p>A typical HTTPS file looks like this:
+     * <pre>{@code
+     *   https://google.com/foo
+     *   GET
+     *   2
+     *   Accept-Language: fr-CA
+     *   Accept-Charset: UTF-8
+     *   HTTP/1.1 200 OK
+     *   3
+     *   Content-Type: image/png
+     *   Content-Length: 100
+     *   Cache-Control: max-age=600
+     *
+     *   AES_256_WITH_MD5
+     *   2
+     *   base64-encoded peerCertificate[0]
+     *   base64-encoded peerCertificate[1]
+     *   -1
+     * }</pre>
+     * The file is newline separated. The first two lines are the URL and
+     * the request method. Next is the number of HTTP Vary request header
+     * lines, followed by those lines.
+     *
+     * <p>Next is the response status line, followed by the number of HTTP
+     * response header lines, followed by those lines.
+     *
+     * <p>HTTPS responses also contain SSL session information. This begins
+     * with a blank line, and then a line containing the cipher suite. Next
+     * is the length of the peer certificate chain. These certificates are
+     * base64-encoded and appear each on their own line. The next line
+     * contains the length of the local certificate chain. These
+     * certificates are also base64-encoded and appear each on their own
+     * line. A length of -1 is used to encode a null array.
+     */
+    public Entry(InputStream in) throws IOException {
+      try {
+        StrictLineReader reader = new StrictLineReader(in, US_ASCII);
+        uri = reader.readLine();
+        requestMethod = reader.readLine();
+        varyHeaders = new RawHeaders();
+        int varyRequestHeaderLineCount = reader.readInt();
+        for (int i = 0; i < varyRequestHeaderLineCount; i++) {
+          varyHeaders.addLine(reader.readLine());
+        }
+
+        responseHeaders = new RawHeaders();
+        responseHeaders.setStatusLine(reader.readLine());
+        int responseHeaderLineCount = reader.readInt();
+        for (int i = 0; i < responseHeaderLineCount; i++) {
+          responseHeaders.addLine(reader.readLine());
+        }
+
+        if (isHttps()) {
+          String blank = reader.readLine();
+          if (blank.length() > 0) {
+            throw new IOException("expected \"\" but was \"" + blank + "\"");
+          }
+          cipherSuite = reader.readLine();
+          peerCertificates = readCertArray(reader);
+          localCertificates = readCertArray(reader);
+        } else {
+          cipherSuite = null;
+          peerCertificates = null;
+          localCertificates = null;
+        }
+      } finally {
+        in.close();
+      }
+    }
+
+    public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection)
+        throws IOException {
+      this.uri = uri.toString();
+      this.varyHeaders = varyHeaders;
+      this.requestMethod = httpConnection.getRequestMethod();
+      this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true);
+
+      if (isHttps()) {
+        HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
+        cipherSuite = httpsConnection.getCipherSuite();
+        Certificate[] peerCertificatesNonFinal = null;
+        try {
+          peerCertificatesNonFinal = httpsConnection.getServerCertificates();
+        } catch (SSLPeerUnverifiedException ignored) {
+        }
+        peerCertificates = peerCertificatesNonFinal;
+        localCertificates = httpsConnection.getLocalCertificates();
+      } else {
+        cipherSuite = null;
+        peerCertificates = null;
+        localCertificates = null;
+      }
+    }
+
+    public void writeTo(DiskLruCache.Editor editor) throws IOException {
+      OutputStream out = editor.newOutputStream(ENTRY_METADATA);
+      Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+
+      writer.write(uri + '\n');
+      writer.write(requestMethod + '\n');
+      writer.write(Integer.toString(varyHeaders.length()) + '\n');
+      for (int i = 0; i < varyHeaders.length(); i++) {
+        writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n');
+      }
+
+      writer.write(responseHeaders.getStatusLine() + '\n');
+      writer.write(Integer.toString(responseHeaders.length()) + '\n');
+      for (int i = 0; i < responseHeaders.length(); i++) {
+        writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n');
+      }
+
+      if (isHttps()) {
+        writer.write('\n');
+        writer.write(cipherSuite + '\n');
+        writeCertArray(writer, peerCertificates);
+        writeCertArray(writer, localCertificates);
+      }
+      writer.close();
+    }
+
+    private boolean isHttps() {
+      return uri.startsWith("https://");
+    }
+
+    private Certificate[] readCertArray(StrictLineReader reader) throws IOException {
+      int length = reader.readInt();
+      if (length == -1) {
+        return null;
+      }
+      try {
+        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+        Certificate[] result = new Certificate[length];
+        for (int i = 0; i < result.length; i++) {
+          String line = reader.readLine();
+          byte[] bytes = Base64.decode(line.getBytes("US-ASCII"));
+          result[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(bytes));
+        }
+        return result;
+      } catch (CertificateException e) {
+        throw new IOException(e.getMessage());
+      }
+    }
+
+    private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {
+      if (certificates == null) {
+        writer.write("-1\n");
+        return;
+      }
+      try {
+        writer.write(Integer.toString(certificates.length) + '\n');
+        for (Certificate certificate : certificates) {
+          byte[] bytes = certificate.getEncoded();
+          String line = Base64.encode(bytes);
+          writer.write(line + '\n');
+        }
+      } catch (CertificateEncodingException e) {
+        throw new IOException(e.getMessage());
+      }
+    }
+
+    public boolean matches(URI uri, String requestMethod,
+        Map<String, List<String>> requestHeaders) {
+      return this.uri.equals(uri.toString())
+          && this.requestMethod.equals(requestMethod)
+          && new ResponseHeaders(uri, responseHeaders).varyMatches(varyHeaders.toMultimap(false),
+          requestHeaders);
+    }
+  }
+
+  /**
+   * Returns an input stream that reads the body of a snapshot, closing the
+   * snapshot when the stream is closed.
+   */
+  private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) {
+    return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) {
+      @Override public void close() throws IOException {
+        snapshot.close();
+        super.close();
+      }
+    };
+  }
+
+  static class EntryCacheResponse extends CacheResponse {
+    private final Entry entry;
+    private final DiskLruCache.Snapshot snapshot;
+    private final InputStream in;
+
+    public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
+      this.entry = entry;
+      this.snapshot = snapshot;
+      this.in = newBodyInputStream(snapshot);
+    }
+
+    @Override public Map<String, List<String>> getHeaders() {
+      return entry.responseHeaders.toMultimap(true);
+    }
+
+    @Override public InputStream getBody() {
+      return in;
+    }
+  }
+
+  static class EntrySecureCacheResponse extends SecureCacheResponse {
+    private final Entry entry;
+    private final DiskLruCache.Snapshot snapshot;
+    private final InputStream in;
+
+    public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
+      this.entry = entry;
+      this.snapshot = snapshot;
+      this.in = newBodyInputStream(snapshot);
+    }
+
+    @Override public Map<String, List<String>> getHeaders() {
+      return entry.responseHeaders.toMultimap(true);
+    }
+
+    @Override public InputStream getBody() {
+      return in;
+    }
+
+    @Override public String getCipherSuite() {
+      return entry.cipherSuite;
+    }
+
+    @Override public List<Certificate> getServerCertificateChain()
+        throws SSLPeerUnverifiedException {
+      if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
+        throw new SSLPeerUnverifiedException(null);
+      }
+      return Arrays.asList(entry.peerCertificates.clone());
+    }
+
+    @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+      if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
+        throw new SSLPeerUnverifiedException(null);
+      }
+      return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal();
+    }
+
+    @Override public List<Certificate> getLocalCertificateChain() {
+      if (entry.localCertificates == null || entry.localCertificates.length == 0) {
+        return null;
+      }
+      return Arrays.asList(entry.localCertificates.clone());
+    }
+
+    @Override public Principal getLocalPrincipal() {
+      if (entry.localCertificates == null || entry.localCertificates.length == 0) {
+        return null;
+      }
+      return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal();
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/cbb0bd5e/framework/src/com/squareup/okhttp/OkHttpClient.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/OkHttpClient.java b/framework/src/com/squareup/okhttp/OkHttpClient.java
index d21cdb7..7834bd6 100644
--- a/framework/src/com/squareup/okhttp/OkHttpClient.java
+++ b/framework/src/com/squareup/okhttp/OkHttpClient.java
@@ -17,12 +17,17 @@ package com.squareup.okhttp;
 
 import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
 import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
+import com.squareup.okhttp.internal.http.OkResponseCache;
+import com.squareup.okhttp.internal.http.OkResponseCacheAdapter;
 import java.net.CookieHandler;
 import java.net.HttpURLConnection;
 import java.net.Proxy;
 import java.net.ProxySelector;
 import java.net.ResponseCache;
 import java.net.URL;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLSocketFactory;
@@ -30,6 +35,7 @@ import javax.net.ssl.SSLSocketFactory;
 /** Configures and creates HTTP connections. */
 public final class OkHttpClient {
   private Proxy proxy;
+  private Set<Route> failedRoutes = Collections.synchronizedSet(new LinkedHashSet<Route>());
   private ProxySelector proxySelector;
   private CookieHandler cookieHandler;
   private ResponseCache responseCache;
@@ -102,6 +108,16 @@ public final class OkHttpClient {
     return responseCache;
   }
 
+  private OkResponseCache okResponseCache() {
+    if (responseCache instanceof HttpResponseCache) {
+      return ((HttpResponseCache) responseCache).okResponseCache;
+    } else if (responseCache != null) {
+      return new OkResponseCacheAdapter(responseCache);
+    } else {
+      return null;
+    }
+  }
+
   /**
    * Sets the socket factory used to secure HTTPS connections.
    *
@@ -166,22 +182,24 @@ public final class OkHttpClient {
 
   public HttpURLConnection open(URL url) {
     String protocol = url.getProtocol();
+    OkHttpClient copy = copyWithDefaults();
     if (protocol.equals("http")) {
-      return new HttpURLConnectionImpl(url, copyWithDefaults());
+      return new HttpURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
     } else if (protocol.equals("https")) {
-      return new HttpsURLConnectionImpl(url, copyWithDefaults());
+      return new HttpsURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
     } else {
       throw new IllegalArgumentException("Unexpected protocol: " + protocol);
     }
   }
 
   /**
-   * Returns a copy of this OkHttpClient that uses the system-wide default for
+   * Returns a shallow copy of this OkHttpClient that uses the system-wide default for
    * each field that hasn't been explicitly configured.
    */
   private OkHttpClient copyWithDefaults() {
     OkHttpClient result = new OkHttpClient();
     result.proxy = proxy;
+    result.failedRoutes = failedRoutes;
     result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault();
     result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault();
     result.responseCache = responseCache != null ? responseCache : ResponseCache.getDefault();

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/cbb0bd5e/framework/src/com/squareup/okhttp/Route.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/Route.java b/framework/src/com/squareup/okhttp/Route.java
new file mode 100644
index 0000000..6968c60
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/Route.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed 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 com.squareup.okhttp;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+/** Represents the route used by a connection to reach an endpoint. */
+public class Route {
+  final Address address;
+  final Proxy proxy;
+  final InetSocketAddress inetSocketAddress;
+  final boolean modernTls;
+
+  public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
+      boolean modernTls) {
+    if (address == null) throw new NullPointerException("address == null");
+    if (proxy == null) throw new NullPointerException("proxy == null");
+    if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null");
+    this.address = address;
+    this.proxy = proxy;
+    this.inetSocketAddress = inetSocketAddress;
+    this.modernTls = modernTls;
+  }
+
+  /** Returns the {@link Address} of this route. */
+  public Address getAddress() {
+    return address;
+  }
+
+  /**
+   * Returns the {@link Proxy} of this route.
+   *
+   * <strong>Warning:</strong> This may be different than the proxy returned
+   * by {@link #getAddress}! That is the proxy that the user asked to be
+   * connected to; this returns the proxy that they were actually connected
+   * to. The two may disagree when a proxy selector selects a different proxy
+   * for a connection.
+   */
+  public Proxy getProxy() {
+    return proxy;
+  }
+
+  /** Returns the {@link InetSocketAddress} of this route. */
+  public InetSocketAddress getSocketAddress() {
+    return inetSocketAddress;
+  }
+
+  /** Returns true if this route uses modern tls. */
+  public boolean isModernTls() {
+    return modernTls;
+  }
+
+  /** Returns a copy of this route with flipped tls mode. */
+  public Route flipTlsMode() {
+    return new Route(address, proxy, inetSocketAddress, !modernTls);
+  }
+
+  @Override public boolean equals(Object obj) {
+    if (obj instanceof Route) {
+      Route other = (Route) obj;
+      return (address.equals(other.address)
+          && proxy.equals(other.proxy)
+          && inetSocketAddress.equals(other.inetSocketAddress)
+          && modernTls == other.modernTls);
+    }
+    return false;
+  }
+
+  @Override public int hashCode() {
+    int result = 17;
+    result = 31 * result + address.hashCode();
+    result = 31 * result + proxy.hashCode();
+    result = 31 * result + inetSocketAddress.hashCode();
+    result = result + (modernTls ? (31 * result) : 0);
+    return result;
+  }
+}

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/cbb0bd5e/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java b/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java
new file mode 100644
index 0000000..78c9691
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed 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 com.squareup.okhttp.internal;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An output stream for an HTTP request body.
+ *
+ * <p>Since a single socket's output stream may be used to write multiple HTTP
+ * requests to the same server, subclasses should not close the socket stream.
+ */
+public abstract class AbstractOutputStream extends OutputStream {
+  protected boolean closed;
+
+  @Override public final void write(int data) throws IOException {
+    write(new byte[] { (byte) data });
+  }
+
+  protected final void checkNotClosed() throws IOException {
+    if (closed) {
+      throw new IOException("stream closed");
+    }
+  }
+
+  /** Returns true if this stream was closed locally. */
+  public boolean isClosed() {
+    return closed;
+  }
+}

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/cbb0bd5e/framework/src/com/squareup/okhttp/internal/DiskLruCache.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/internal/DiskLruCache.java b/framework/src/com/squareup/okhttp/internal/DiskLruCache.java
index 00fe2f1..f7fcb1e 100644
--- a/framework/src/com/squareup/okhttp/internal/DiskLruCache.java
+++ b/framework/src/com/squareup/okhttp/internal/DiskLruCache.java
@@ -23,7 +23,6 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
-import java.io.FileWriter;
 import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -32,23 +31,22 @@ import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.Writer;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
-
-import static com.squareup.okhttp.internal.Util.UTF_8;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * A cache that uses a bounded amount of space on a filesystem. Each cache
- * entry has a string key and a fixed number of values. Values are byte
- * sequences, accessible as streams or files. Each value must be between {@code
- * 0} and {@code Integer.MAX_VALUE} bytes in length.
+ * entry has a string key and a fixed number of values. Each key must match
+ * the regex <strong>[a-z0-9_-]{1,64}</strong>. Values are byte sequences,
+ * accessible as streams or files. Each value must be between {@code 0} and
+ * {@code Integer.MAX_VALUE} bytes in length.
  *
  * <p>The cache stores its data in a directory on the filesystem. This
  * directory must be exclusive to the cache; the cache may delete or overwrite
@@ -66,12 +64,12 @@ import static com.squareup.okhttp.internal.Util.UTF_8;
  * entry may have only one editor at one time; if a value is not available to be
  * edited then {@link #edit} will return null.
  * <ul>
- * <li>When an entry is being <strong>created</strong> it is necessary to
- * supply a full set of values; the empty value should be used as a
- * placeholder if necessary.
- * <li>When an entry is being <strong>edited</strong>, it is not necessary
- * to supply data for every value; values default to their previous
- * value.
+ *     <li>When an entry is being <strong>created</strong> it is necessary to
+ *         supply a full set of values; the empty value should be used as a
+ *         placeholder if necessary.
+ *     <li>When an entry is being <strong>edited</strong>, it is not necessary
+ *         to supply data for every value; values default to their previous
+ *         value.
  * </ul>
  * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
  * or {@link Editor#abort}. Committing is atomic: a read observes the full set
@@ -89,58 +87,63 @@ import static com.squareup.okhttp.internal.Util.UTF_8;
  */
 public final class DiskLruCache implements Closeable {
   static final String JOURNAL_FILE = "journal";
-  static final String JOURNAL_FILE_TMP = "journal.tmp";
+  static final String JOURNAL_FILE_TEMP = "journal.tmp";
+  static final String JOURNAL_FILE_BACKUP = "journal.bkp";
   static final String MAGIC = "libcore.io.DiskLruCache";
   static final String VERSION_1 = "1";
   static final long ANY_SEQUENCE_NUMBER = -1;
+  static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}");
   private static final String CLEAN = "CLEAN";
   private static final String DIRTY = "DIRTY";
   private static final String REMOVE = "REMOVE";
   private static final String READ = "READ";
 
-  // This cache uses a journal file named "journal". A typical journal file
-  // looks like this:
-  //     libcore.io.DiskLruCache
-  //     1
-  //     100
-  //     2
-  //
-  //     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
-  //     DIRTY 335c4c6028171cfddfbaae1a9c313c52
-  //     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
-  //     REMOVE 335c4c6028171cfddfbaae1a9c313c52
-  //     DIRTY 1ab96a171faeeee38496d8b330771a7a
-  //     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
-  //     READ 335c4c6028171cfddfbaae1a9c313c52
-  //     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
-  //
-  // The first five lines of the journal form its header. They are the
-  // constant string "libcore.io.DiskLruCache", the disk cache's version,
-  // the application's version, the value count, and a blank line.
-  //
-  // Each of the subsequent lines in the file is a record of the state of a
-  // cache entry. Each line contains space-separated values: a state, a key,
-  // and optional state-specific values.
-  //   o DIRTY lines track that an entry is actively being created or updated.
-  //     Every successful DIRTY action should be followed by a CLEAN or REMOVE
-  //     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
-  //     temporary files may need to be deleted.
-  //   o CLEAN lines track a cache entry that has been successfully published
-  //     and may be read. A publish line is followed by the lengths of each of
-  //     its values.
-  //   o READ lines track accesses for LRU.
-  //   o REMOVE lines track entries that have been deleted.
-  //
-  // The journal file is appended to as cache operations occur. The journal may
-  // occasionally be compacted by dropping redundant lines. A temporary file named
-  // "journal.tmp" will be used during compaction; that file should be deleted if
-  // it exists when the cache is opened.
+    /*
+     * This cache uses a journal file named "journal". A typical journal file
+     * looks like this:
+     *     libcore.io.DiskLruCache
+     *     1
+     *     100
+     *     2
+     *
+     *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
+     *     DIRTY 335c4c6028171cfddfbaae1a9c313c52
+     *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
+     *     REMOVE 335c4c6028171cfddfbaae1a9c313c52
+     *     DIRTY 1ab96a171faeeee38496d8b330771a7a
+     *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
+     *     READ 335c4c6028171cfddfbaae1a9c313c52
+     *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
+     *
+     * The first five lines of the journal form its header. They are the
+     * constant string "libcore.io.DiskLruCache", the disk cache's version,
+     * the application's version, the value count, and a blank line.
+     *
+     * Each of the subsequent lines in the file is a record of the state of a
+     * cache entry. Each line contains space-separated values: a state, a key,
+     * and optional state-specific values.
+     *   o DIRTY lines track that an entry is actively being created or updated.
+     *     Every successful DIRTY action should be followed by a CLEAN or REMOVE
+     *     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
+     *     temporary files may need to be deleted.
+     *   o CLEAN lines track a cache entry that has been successfully published
+     *     and may be read. A publish line is followed by the lengths of each of
+     *     its values.
+     *   o READ lines track accesses for LRU.
+     *   o REMOVE lines track entries that have been deleted.
+     *
+     * The journal file is appended to as cache operations occur. The journal may
+     * occasionally be compacted by dropping redundant lines. A temporary file named
+     * "journal.tmp" will be used during compaction; that file should be deleted if
+     * it exists when the cache is opened.
+     */
 
   private final File directory;
   private final File journalFile;
   private final File journalFileTmp;
+  private final File journalFileBackup;
   private final int appVersion;
-  private final long maxSize;
+  private long maxSize;
   private final int valueCount;
   private long size = 0;
   private Writer journalWriter;
@@ -156,13 +159,13 @@ public final class DiskLruCache implements Closeable {
   private long nextSequenceNumber = 0;
 
   /** This cache uses a single background thread to evict entries. */
-  private final ExecutorService executorService =
+  final ThreadPoolExecutor executorService =
       new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
   private final Callable<Void> cleanupCallable = new Callable<Void>() {
-    @Override public Void call() throws Exception {
+    public Void call() throws Exception {
       synchronized (DiskLruCache.this) {
         if (journalWriter == null) {
-          return null; // closed
+          return null; // Closed.
         }
         trimToSize();
         if (journalRebuildRequired()) {
@@ -178,7 +181,8 @@ public final class DiskLruCache implements Closeable {
     this.directory = directory;
     this.appVersion = appVersion;
     this.journalFile = new File(directory, JOURNAL_FILE);
-    this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
+    this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
+    this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
     this.valueCount = valueCount;
     this.maxSize = maxSize;
   }
@@ -201,26 +205,35 @@ public final class DiskLruCache implements Closeable {
       throw new IllegalArgumentException("valueCount <= 0");
     }
 
-    // prefer to pick up where we left off
+    // If a bkp file exists, use it instead.
+    File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
+    if (backupFile.exists()) {
+      File journalFile = new File(directory, JOURNAL_FILE);
+      // If journal file also exists just delete backup file.
+      if (journalFile.exists()) {
+        backupFile.delete();
+      } else {
+        renameTo(backupFile, journalFile, false);
+      }
+    }
+
+    // Prefer to pick up where we left off.
     DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
     if (cache.journalFile.exists()) {
       try {
         cache.readJournal();
         cache.processJournal();
-        cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true));
+        cache.journalWriter = new BufferedWriter(
+            new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
         return cache;
       } catch (IOException journalIsCorrupt) {
-        Platform.get()
-            .logW("DiskLruCache "
-                + directory
-                + " is corrupt: "
-                + journalIsCorrupt.getMessage()
-                + ", removing");
+        Platform.get().logW("DiskLruCache " + directory + " is corrupt: "
+            + journalIsCorrupt.getMessage() + ", removing");
         cache.delete();
       }
     }
 
-    // create a new empty cache
+    // Create a new empty cache.
     directory.mkdirs();
     cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
     cache.rebuildJournal();
@@ -235,42 +248,47 @@ public final class DiskLruCache implements Closeable {
       String appVersionString = reader.readLine();
       String valueCountString = reader.readLine();
       String blank = reader.readLine();
-      if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(appVersion)
-          .equals(appVersionString) || !Integer.toString(valueCount).equals(valueCountString) || !""
-          .equals(blank)) {
-        throw new IOException("unexpected journal header: ["
-            + magic
-            + ", "
-            + version
-            + ", "
-            + valueCountString
-            + ", "
-            + blank
-            + "]");
+      if (!MAGIC.equals(magic)
+          || !VERSION_1.equals(version)
+          || !Integer.toString(appVersion).equals(appVersionString)
+          || !Integer.toString(valueCount).equals(valueCountString)
+          || !"".equals(blank)) {
+        throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+            + valueCountString + ", " + blank + "]");
       }
 
+      int lineCount = 0;
       while (true) {
         try {
           readJournalLine(reader.readLine());
+          lineCount++;
         } catch (EOFException endOfJournal) {
           break;
         }
       }
+      redundantOpCount = lineCount - lruEntries.size();
     } finally {
       Util.closeQuietly(reader);
     }
   }
 
   private void readJournalLine(String line) throws IOException {
-    String[] parts = line.split(" ");
-    if (parts.length < 2) {
+    int firstSpace = line.indexOf(' ');
+    if (firstSpace == -1) {
       throw new IOException("unexpected journal line: " + line);
     }
 
-    String key = parts[1];
-    if (parts[0].equals(REMOVE) && parts.length == 2) {
-      lruEntries.remove(key);
-      return;
+    int keyBegin = firstSpace + 1;
+    int secondSpace = line.indexOf(' ', keyBegin);
+    final String key;
+    if (secondSpace == -1) {
+      key = line.substring(keyBegin);
+      if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
+        lruEntries.remove(key);
+        return;
+      }
+    } else {
+      key = line.substring(keyBegin, secondSpace);
     }
 
     Entry entry = lruEntries.get(key);
@@ -279,14 +297,15 @@ public final class DiskLruCache implements Closeable {
       lruEntries.put(key, entry);
     }
 
-    if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
+    if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
+      String[] parts = line.substring(secondSpace + 1).split(" ");
       entry.readable = true;
       entry.currentEditor = null;
-      entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length));
-    } else if (parts[0].equals(DIRTY) && parts.length == 2) {
+      entry.setLengths(parts);
+    } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
       entry.currentEditor = new Editor(entry);
-    } else if (parts[0].equals(READ) && parts.length == 2) {
-      // this work was already done by calling lruEntries.get()
+    } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
+      // This work was already done by calling lruEntries.get().
     } else {
       throw new IOException("unexpected journal line: " + line);
     }
@@ -324,32 +343,53 @@ public final class DiskLruCache implements Closeable {
       journalWriter.close();
     }
 
-    Writer writer = new BufferedWriter(new FileWriter(journalFileTmp));
-    writer.write(MAGIC);
-    writer.write("\n");
-    writer.write(VERSION_1);
-    writer.write("\n");
-    writer.write(Integer.toString(appVersion));
-    writer.write("\n");
-    writer.write(Integer.toString(valueCount));
-    writer.write("\n");
-    writer.write("\n");
-
-    for (Entry entry : lruEntries.values()) {
-      if (entry.currentEditor != null) {
-        writer.write(DIRTY + ' ' + entry.key + '\n');
-      } else {
-        writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+    Writer writer = new BufferedWriter(
+        new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
+    try {
+      writer.write(MAGIC);
+      writer.write("\n");
+      writer.write(VERSION_1);
+      writer.write("\n");
+      writer.write(Integer.toString(appVersion));
+      writer.write("\n");
+      writer.write(Integer.toString(valueCount));
+      writer.write("\n");
+      writer.write("\n");
+
+      for (Entry entry : lruEntries.values()) {
+        if (entry.currentEditor != null) {
+          writer.write(DIRTY + ' ' + entry.key + '\n');
+        } else {
+          writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+        }
       }
+    } finally {
+      writer.close();
+    }
+
+    if (journalFile.exists()) {
+      renameTo(journalFile, journalFileBackup, true);
     }
+    renameTo(journalFileTmp, journalFile, false);
+    journalFileBackup.delete();
 
-    writer.close();
-    journalFileTmp.renameTo(journalFile);
-    journalWriter = new BufferedWriter(new FileWriter(journalFile, true));
+    journalWriter = new BufferedWriter(
+        new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
   }
 
   private static void deleteIfExists(File file) throws IOException {
-    file.delete();
+    if (file.exists() && !file.delete()) {
+      throw new IOException();
+    }
+  }
+
+  private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
+    if (deleteDestination) {
+      deleteIfExists(to);
+    }
+    if (!from.renameTo(to)) {
+      throw new IOException();
+    }
   }
 
   /**
@@ -378,7 +418,14 @@ public final class DiskLruCache implements Closeable {
         ins[i] = new FileInputStream(entry.getCleanFile(i));
       }
     } catch (FileNotFoundException e) {
-      // a file must have been deleted manually!
+      // A file must have been deleted manually!
+      for (int i = 0; i < valueCount; i++) {
+        if (ins[i] != null) {
+          Util.closeQuietly(ins[i]);
+        } else {
+          break;
+        }
+      }
       return null;
     }
 
@@ -388,7 +435,7 @@ public final class DiskLruCache implements Closeable {
       executorService.submit(cleanupCallable);
     }
 
-    return new Snapshot(key, entry.sequenceNumber, ins);
+    return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
   }
 
   /**
@@ -405,19 +452,19 @@ public final class DiskLruCache implements Closeable {
     Entry entry = lruEntries.get(key);
     if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
         || entry.sequenceNumber != expectedSequenceNumber)) {
-      return null; // snapshot is stale
+      return null; // Snapshot is stale.
     }
     if (entry == null) {
       entry = new Entry(key);
       lruEntries.put(key, entry);
     } else if (entry.currentEditor != null) {
-      return null; // another edit is in progress
+      return null; // Another edit is in progress.
     }
 
     Editor editor = new Editor(entry);
     entry.currentEditor = editor;
 
-    // flush the journal before creating files to prevent file leaks
+    // Flush the journal before creating files to prevent file leaks.
     journalWriter.write(DIRTY + ' ' + key + '\n');
     journalWriter.flush();
     return editor;
@@ -432,11 +479,20 @@ public final class DiskLruCache implements Closeable {
    * Returns the maximum number of bytes that this cache should use to store
    * its data.
    */
-  public long maxSize() {
+  public long getMaxSize() {
     return maxSize;
   }
 
   /**
+   * Changes the maximum number of bytes the cache can store and queues a job
+   * to trim the existing store, if necessary.
+   */
+  public synchronized void setMaxSize(long maxSize) {
+    this.maxSize = maxSize;
+    executorService.submit(cleanupCallable);
+  }
+
+  /**
    * Returns the number of bytes currently being used to store the values in
    * this cache. This may be greater than the max size if a background
    * deletion is pending.
@@ -451,7 +507,7 @@ public final class DiskLruCache implements Closeable {
       throw new IllegalStateException();
     }
 
-    // if this edit is creating the entry for the first time, every index must have a value
+    // If this edit is creating the entry for the first time, every index must have a value.
     if (success && !entry.readable) {
       for (int i = 0; i < valueCount; i++) {
         if (!editor.written[i]) {
@@ -460,7 +516,6 @@ public final class DiskLruCache implements Closeable {
         }
         if (!entry.getDirtyFile(i).exists()) {
           editor.abort();
-          Platform.get().logW("DiskLruCache: Newly created entry doesn't have file for index " + i);
           return;
         }
       }
@@ -494,6 +549,7 @@ public final class DiskLruCache implements Closeable {
       lruEntries.remove(entry.key);
       journalWriter.write(REMOVE + ' ' + entry.key + '\n');
     }
+    journalWriter.flush();
 
     if (size > maxSize || journalRebuildRequired()) {
       executorService.submit(cleanupCallable);
@@ -506,7 +562,8 @@ public final class DiskLruCache implements Closeable {
    */
   private boolean journalRebuildRequired() {
     final int redundantOpCompactThreshold = 2000;
-    return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size();
+    return redundantOpCount >= redundantOpCompactThreshold //
+        && redundantOpCount >= lruEntries.size();
   }
 
   /**
@@ -564,7 +621,7 @@ public final class DiskLruCache implements Closeable {
   /** Closes this cache. Stored values will remain on the filesystem. */
   public synchronized void close() throws IOException {
     if (journalWriter == null) {
-      return; // already closed
+      return; // Already closed.
     }
     for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
       if (entry.currentEditor != null) {
@@ -594,14 +651,14 @@ public final class DiskLruCache implements Closeable {
   }
 
   private void validateKey(String key) {
-    if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
-      throw new IllegalArgumentException(
-          "keys must not contain spaces or newlines: \"" + key + "\"");
+    Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
+    if (!matcher.matches()) {
+      throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\"");
     }
   }
 
   private static String inputStreamToString(InputStream in) throws IOException {
-    return Util.readFully(new InputStreamReader(in, UTF_8));
+    return Util.readFully(new InputStreamReader(in, Util.UTF_8));
   }
 
   /** A snapshot of the values for an entry. */
@@ -609,11 +666,13 @@ public final class DiskLruCache implements Closeable {
     private final String key;
     private final long sequenceNumber;
     private final InputStream[] ins;
+    private final long[] lengths;
 
-    private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
+    private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
       this.key = key;
       this.sequenceNumber = sequenceNumber;
       this.ins = ins;
+      this.lengths = lengths;
     }
 
     /**
@@ -635,18 +694,31 @@ public final class DiskLruCache implements Closeable {
       return inputStreamToString(getInputStream(index));
     }
 
-    @Override public void close() {
+    /** Returns the byte length of the value for {@code index}. */
+    public long getLength(int index) {
+      return lengths[index];
+    }
+
+    public void close() {
       for (InputStream in : ins) {
         Util.closeQuietly(in);
       }
     }
   }
 
+  private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
+    @Override
+    public void write(int b) throws IOException {
+      // Eat all writes silently. Nom nom.
+    }
+  };
+
   /** Edits the values for an entry. */
   public final class Editor {
     private final Entry entry;
     private final boolean[] written;
     private boolean hasErrors;
+    private boolean committed;
 
     private Editor(Entry entry) {
       this.entry = entry;
@@ -665,7 +737,11 @@ public final class DiskLruCache implements Closeable {
         if (!entry.readable) {
           return null;
         }
-        return new FileInputStream(entry.getCleanFile(index));
+        try {
+          return new FileInputStream(entry.getCleanFile(index));
+        } catch (FileNotFoundException e) {
+          return null;
+        }
       }
     }
 
@@ -693,7 +769,21 @@ public final class DiskLruCache implements Closeable {
         if (!entry.readable) {
           written[index] = true;
         }
-        return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
+        File dirtyFile = entry.getDirtyFile(index);
+        FileOutputStream outputStream;
+        try {
+          outputStream = new FileOutputStream(dirtyFile);
+        } catch (FileNotFoundException e) {
+          // Attempt to recreate the cache directory.
+          directory.mkdirs();
+          try {
+            outputStream = new FileOutputStream(dirtyFile);
+          } catch (FileNotFoundException e2) {
+            // We are unable to recover. Silently eat the writes.
+            return NULL_OUTPUT_STREAM;
+          }
+        }
+        return new FaultHidingOutputStream(outputStream);
       }
     }
 
@@ -701,7 +791,7 @@ public final class DiskLruCache implements Closeable {
     public void set(int index, String value) throws IOException {
       Writer writer = null;
       try {
-        writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
+        writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
         writer.write(value);
       } finally {
         Util.closeQuietly(writer);
@@ -715,10 +805,11 @@ public final class DiskLruCache implements Closeable {
     public void commit() throws IOException {
       if (hasErrors) {
         completeEdit(this, false);
-        remove(entry.key); // the previous entry is stale
+        remove(entry.key); // The previous entry is stale.
       } else {
         completeEdit(this, true);
       }
+      committed = true;
     }
 
     /**
@@ -729,7 +820,16 @@ public final class DiskLruCache implements Closeable {
       completeEdit(this, false);
     }
 
-    private final class FaultHidingOutputStream extends FilterOutputStream {
+    public void abortUnlessCommitted() {
+      if (!committed) {
+        try {
+          abort();
+        } catch (IOException ignored) {
+        }
+      }
+    }
+
+    private class FaultHidingOutputStream extends FilterOutputStream {
       private FaultHidingOutputStream(OutputStream out) {
         super(out);
       }
@@ -812,7 +912,7 @@ public final class DiskLruCache implements Closeable {
     }
 
     private IOException invalidLengths(String[] strings) throws IOException {
-      throw new IOException("unexpected journal line: " + Arrays.toString(strings));
+      throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
     }
 
     public File getCleanFile(int i) {

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/cbb0bd5e/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java b/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java
new file mode 100644
index 0000000..c32b27a
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed 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 com.squareup.okhttp.internal;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+
+/**
+ * An output stream wrapper that recovers from failures in the underlying stream
+ * by replacing it with another stream. This class buffers a fixed amount of
+ * data under the assumption that failures occur early in a stream's life.
+ * If a failure occurs after the buffer has been exhausted, no recovery is
+ * attempted.
+ *
+ * <p>Subclasses must override {@link #replacementStream} which will request a
+ * replacement stream each time an {@link IOException} is encountered on the
+ * current stream.
+ */
+public abstract class FaultRecoveringOutputStream extends AbstractOutputStream {
+  private final int maxReplayBufferLength;
+
+  /** Bytes to transmit on the replacement stream, or null if no recovery is possible. */
+  private ByteArrayOutputStream replayBuffer;
+  private OutputStream out;
+
+  /**
+   * @param maxReplayBufferLength the maximum number of successfully written
+   *     bytes to buffer so they can be replayed in the event of an error.
+   *     Failure recoveries are not possible once this limit has been exceeded.
+   */
+  public FaultRecoveringOutputStream(int maxReplayBufferLength, OutputStream out) {
+    if (maxReplayBufferLength < 0) throw new IllegalArgumentException();
+    this.maxReplayBufferLength = maxReplayBufferLength;
+    this.replayBuffer = new ByteArrayOutputStream(maxReplayBufferLength);
+    this.out = out;
+  }
+
+  @Override public final void write(byte[] buffer, int offset, int count) throws IOException {
+    if (closed) throw new IOException("stream closed");
+    checkOffsetAndCount(buffer.length, offset, count);
+
+    while (true) {
+      try {
+        out.write(buffer, offset, count);
+
+        if (replayBuffer != null) {
+          if (count + replayBuffer.size() > maxReplayBufferLength) {
+            // Failure recovery is no longer possible once we overflow the replay buffer.
+            replayBuffer = null;
+          } else {
+            // Remember the written bytes to the replay buffer.
+            replayBuffer.write(buffer, offset, count);
+          }
+        }
+        return;
+      } catch (IOException e) {
+        if (!recover(e)) throw e;
+      }
+    }
+  }
+
+  @Override public final void flush() throws IOException {
+    if (closed) {
+      return; // don't throw; this stream might have been closed on the caller's behalf
+    }
+    while (true) {
+      try {
+        out.flush();
+        return;
+      } catch (IOException e) {
+        if (!recover(e)) throw e;
+      }
+    }
+  }
+
+  @Override public final void close() throws IOException {
+    if (closed) {
+      return;
+    }
+    while (true) {
+      try {
+        out.close();
+        closed = true;
+        return;
+      } catch (IOException e) {
+        if (!recover(e)) throw e;
+      }
+    }
+  }
+
+  /**
+   * Attempt to replace {@code out} with another equivalent stream. Returns true
+   * if a suitable replacement stream was found.
+   */
+  private boolean recover(IOException e) {
+    if (replayBuffer == null) {
+      return false; // Can't recover because we've dropped data that we would need to replay.
+    }
+
+    while (true) {
+      OutputStream replacementStream = null;
+      try {
+        replacementStream = replacementStream(e);
+        if (replacementStream == null) {
+          return false;
+        }
+        replaceStream(replacementStream);
+        return true;
+      } catch (IOException replacementStreamFailure) {
+        // The replacement was also broken. Loop to ask for another replacement.
+        Util.closeQuietly(replacementStream);
+        e = replacementStreamFailure;
+      }
+    }
+  }
+
+  /**
+   * Returns true if errors in the underlying stream can currently be recovered.
+   */
+  public boolean isRecoverable() {
+    return replayBuffer != null;
+  }
+
+  /**
+   * Replaces the current output stream with {@code replacementStream}, writing
+   * any replay bytes to it if they exist. The current output stream is closed.
+   */
+  public final void replaceStream(OutputStream replacementStream) throws IOException {
+    if (!isRecoverable()) {
+      throw new IllegalStateException();
+    }
+    if (this.out == replacementStream) {
+      return; // Don't replace a stream with itself.
+    }
+    replayBuffer.writeTo(replacementStream);
+    Util.closeQuietly(out);
+    out = replacementStream;
+  }
+
+  /**
+   * Returns a replacement output stream to recover from {@code e} thrown by the
+   * previous stream. Returns a new OutputStream if recovery was successful, in
+   * which case all previously-written data will be replayed. Returns null if
+   * the failure cannot be recovered.
+   */
+  protected abstract OutputStream replacementStream(IOException e) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/cbb0bd5e/framework/src/com/squareup/okhttp/internal/Platform.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/internal/Platform.java b/framework/src/com/squareup/okhttp/internal/Platform.java
index 75cd66f..6b4ac34 100644
--- a/framework/src/com/squareup/okhttp/internal/Platform.java
+++ b/framework/src/com/squareup/okhttp/internal/Platform.java
@@ -17,6 +17,7 @@
 package com.squareup.okhttp.internal;
 
 import com.squareup.okhttp.OkHttpClient;
+import java.io.IOException;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Constructor;
@@ -24,6 +25,7 @@ import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
+import java.net.NetworkInterface;
 import java.net.Socket;
 import java.net.SocketException;
 import java.net.URI;
@@ -123,8 +125,27 @@ public class Platform {
     }
   }
 
+  /**
+   * Returns the maximum transmission unit of the network interface used by
+   * {@code socket}, or a reasonable default if this platform doesn't expose the
+   * MTU to the application layer.
+   *
+   * <p>The returned value should only be used as an optimization; such as to
+   * size buffers efficiently.
+   */
+  public int getMtu(Socket socket) throws IOException {
+    return 1400; // Smaller than 1500 to leave room for headers on interfaces like PPPoE.
+  }
+
   /** Attempt to match the host runtime to a capable Platform implementation. */
   private static Platform findPlatform() {
+    Method getMtu;
+    try {
+      getMtu = NetworkInterface.class.getMethod("getMTU");
+    } catch (NoSuchMethodException e) {
+      return new Platform(); // No Java 1.6 APIs. It's either Java 1.5, Android 2.2 or earlier.
+    }
+
     // Attempt to find Android 2.3+ APIs.
     Class<?> openSslSocketClass;
     Method setUseSessionTickets;
@@ -138,10 +159,10 @@ public class Platform {
       try {
         Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
         Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
-        return new Android41(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols,
-            getNpnSelectedProtocol);
+        return new Android41(getMtu, openSslSocketClass, setUseSessionTickets, setHostname,
+            setNpnProtocols, getNpnSelectedProtocol);
       } catch (NoSuchMethodException ignored) {
-        return new Android23(openSslSocketClass, setUseSessionTickets, setHostname);
+        return new Android23(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
       }
     } catch (ClassNotFoundException ignored) {
       // This isn't an Android runtime.
@@ -158,12 +179,35 @@ public class Platform {
       Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
       Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
       Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
-      return new JdkWithJettyNpnPlatform(putMethod, getMethod, clientProviderClass,
+      return new JdkWithJettyNpnPlatform(getMtu, putMethod, getMethod, clientProviderClass,
           serverProviderClass);
     } catch (ClassNotFoundException ignored) {
-      return new Platform(); // NPN isn't on the classpath.
+      // NPN isn't on the classpath.
     } catch (NoSuchMethodException ignored) {
-      return new Platform(); // The NPN version isn't what we expect.
+      // The NPN version isn't what we expect.
+    }
+
+    return getMtu != null ? new Java5(getMtu) : new Platform();
+  }
+
+  private static class Java5 extends Platform {
+    private final Method getMtu;
+
+    private Java5(Method getMtu) {
+      this.getMtu = getMtu;
+    }
+
+    @Override public int getMtu(Socket socket) throws IOException {
+      try {
+        NetworkInterface networkInterface = NetworkInterface.getByInetAddress(
+            socket.getLocalAddress());
+        return (Integer) getMtu.invoke(networkInterface);
+      } catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      } catch (InvocationTargetException e) {
+        if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
+        throw new RuntimeException(e.getCause());
+      }
     }
   }
 
@@ -171,13 +215,14 @@ public class Platform {
    * Android version 2.3 and newer support TLS session tickets and server name
    * indication (SNI).
    */
-  private static class Android23 extends Platform {
+  private static class Android23 extends Java5 {
     protected final Class<?> openSslSocketClass;
     private final Method setUseSessionTickets;
     private final Method setHostname;
 
-    private Android23(Class<?> openSslSocketClass, Method setUseSessionTickets,
+    private Android23(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
         Method setHostname) {
+      super(getMtu);
       this.openSslSocketClass = openSslSocketClass;
       this.setUseSessionTickets = setUseSessionTickets;
       this.setHostname = setHostname;
@@ -204,9 +249,9 @@ public class Platform {
     private final Method setNpnProtocols;
     private final Method getNpnSelectedProtocol;
 
-    private Android41(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname,
-        Method setNpnProtocols, Method getNpnSelectedProtocol) {
-      super(openSslSocketClass, setUseSessionTickets, setHostname);
+    private Android41(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
+        Method setHostname, Method setNpnProtocols, Method getNpnSelectedProtocol) {
+      super(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
       this.setNpnProtocols = setNpnProtocols;
       this.getNpnSelectedProtocol = getNpnSelectedProtocol;
     }
@@ -242,14 +287,15 @@ public class Platform {
    * OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class
    * path.
    */
-  private static class JdkWithJettyNpnPlatform extends Platform {
+  private static class JdkWithJettyNpnPlatform extends Java5 {
     private final Method getMethod;
     private final Method putMethod;
     private final Class<?> clientProviderClass;
     private final Class<?> serverProviderClass;
 
-    public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class<?> clientProviderClass,
-        Class<?> serverProviderClass) {
+    public JdkWithJettyNpnPlatform(Method getMtu, Method putMethod, Method getMethod,
+        Class<?> clientProviderClass, Class<?> serverProviderClass) {
+      super(getMtu);
       this.putMethod = putMethod;
       this.getMethod = getMethod;
       this.clientProviderClass = clientProviderClass;