Merge tag 'AU_LINUX_ANDROID_LA.BF64.1.2.1.05.01.00.066.050' into HEAD

AU_LINUX_ANDROID_LA.BF64.1.2.1.05.01.00.066.050 based on quic/aosp/LA.BF64.1.2.1

Change-Id: Ib9df92da52900a8b42a64eeb7b702a5f91bcc695
diff --git a/Android.mk b/Android.mk
index f53e2d3..6d8ac4d 100644
--- a/Android.mk
+++ b/Android.mk
@@ -68,7 +68,7 @@
 LOCAL_MODULE_TAGS := optional
 LOCAL_SRC_FILES := $(okhttp_test_src_files)
 LOCAL_JAVACFLAGS := -encoding UTF-8
-LOCAL_JAVA_LIBRARIES := core-libart okhttp-nojarjar junit4-target bouncycastle-nojarjar tcmiface
+LOCAL_JAVA_LIBRARIES := core-libart okhttp-nojarjar junit4-target bouncycastle-nojarjar conscrypt tcmiface
 LOCAL_NO_STANDARD_LIBRARIES := true
 LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java b/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
index 36c3101..e64eec4 100644
--- a/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
+++ b/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
@@ -86,7 +86,11 @@
               // If the network config has changed then existing pooled connections should not be
               // re-used. By setting connectionPool to null it ensures that the next time
               // getConnectionPool() is called a new pool will be created.
+              ConnectionPool oldConnectionPool = connectionPool;
               connectionPool = null;
+              if (oldConnectionPool != null) {
+                oldConnectionPool.enterDrainMode();
+              }
             }
           }
         });
diff --git a/android/main/java/com/squareup/okhttp/internal/OptionalMethod.java b/android/main/java/com/squareup/okhttp/internal/OptionalMethod.java
new file mode 100644
index 0000000..81aef8e
--- /dev/null
+++ b/android/main/java/com/squareup/okhttp/internal/OptionalMethod.java
@@ -0,0 +1,169 @@
+/*
+ *  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.internal;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+/**
+ * Duck-typing for methods: Represents a method that may or may not be present on an object.
+ *
+ * @param <T> the type of the object the method might be on, typically an interface or base class
+ */
+class OptionalMethod<T> {
+
+  /** The return type of the method. null means "don't care". */
+  private final Class<?> returnType;
+
+  private final String methodName;
+
+  private final Class[] methodParams;
+
+  /**
+   * Creates an optional method.
+   *
+   * @param returnType the return type to required, null if it does not matter
+   * @param methodName the name of the method
+   * @param methodParams the method parameter types
+   */
+  public OptionalMethod(Class<?> returnType, String methodName, Class... methodParams) {
+    this.returnType = returnType;
+    this.methodName = methodName;
+    this.methodParams = methodParams;
+  }
+
+  /**
+   * Returns true if the method exists on the supplied {@code target}.
+   */
+  public boolean isSupported(T target) {
+    return getMethod(target.getClass()) != null;
+  }
+
+  /**
+   * Invokes the method on {@code target} with {@code args}. If the method does not exist or is not
+   * public then {@code null} is returned. See also
+   * {@link #invokeOptionalWithoutCheckedException(Object, Object...)}.
+   *
+   * @throws IllegalArgumentException if the arguments are invalid
+   * @throws InvocationTargetException if the invocation throws an exception
+   */
+  public Object invokeOptional(T target, Object... args) throws InvocationTargetException {
+    Method m = getMethod(target.getClass());
+    if (m == null) {
+      return null;
+    }
+    try {
+      return m.invoke(target, args);
+    } catch (IllegalAccessException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Invokes the method on {@code target}.  If the method does not exist or is not
+   * public then {@code null} is returned. Any RuntimeException thrown by the method is thrown,
+   * checked exceptions are wrapped in an {@link AssertionError}.
+   *
+   * @throws IllegalArgumentException if the arguments are invalid
+   */
+  public Object invokeOptionalWithoutCheckedException(T target, Object... args) {
+    try {
+      return invokeOptional(target, args);
+    } catch (InvocationTargetException e) {
+      Throwable targetException = e.getTargetException();
+      if (targetException instanceof RuntimeException) {
+        throw (RuntimeException) targetException;
+      }
+      throw new AssertionError("Unexpected exception", targetException);
+    }
+  }
+
+  /**
+   * Invokes the method on {@code target} with {@code args}. Throws an error if the method is not
+   * supported. See also {@link #invokeWithoutCheckedException(Object, Object...)}.
+   *
+   * @throws IllegalArgumentException if the arguments are invalid
+   * @throws InvocationTargetException if the invocation throws an exception
+   */
+  public Object invoke(T target, Object... args) throws InvocationTargetException {
+    Method m = getMethod(target.getClass());
+    if (m == null) {
+      throw new AssertionError("Method " + methodName + " not supported for object " + target);
+    }
+    try {
+      return m.invoke(target, args);
+    } catch (IllegalAccessException e) {
+      // Method should be public: we checked.
+      throw new AssertionError("Unexpectedly could not call: " + m, e);
+    }
+  }
+
+  /**
+   * Invokes the method on {@code target}. Throws an error if the method is not supported. Any
+   * RuntimeException thrown by the method is thrown, checked exceptions are wrapped in
+   * an {@link AssertionError}.
+   *
+   * @throws IllegalArgumentException if the arguments are invalid
+   */
+  public Object invokeWithoutCheckedException(T target, Object... args) {
+    try {
+      return invoke(target, args);
+    } catch (InvocationTargetException e) {
+      Throwable targetException = e.getTargetException();
+      if (targetException instanceof RuntimeException) {
+        throw (RuntimeException) targetException;
+      }
+      throw new AssertionError("Unexpected exception", targetException);
+    }
+  }
+
+  /**
+   * Perform a lookup for the method. No caching.
+   * In order to return a method the method name and arguments must match those specified when
+   * the {@link OptionalMethod} was created. If the return type is specified (i.e. non-null) it
+   * must also be compatible. The method must also be public.
+   */
+  private Method getMethod(Class<?> clazz) {
+    Method method = null;
+    if (methodName != null) {
+      method = getPublicMethod(clazz, methodName, methodParams);
+      if (method != null
+          && returnType != null
+          && !returnType.isAssignableFrom(method.getReturnType())) {
+
+        // If the return type is non-null it must be compatible.
+        method = null;
+      }
+    }
+    return method;
+  }
+
+  private static Method getPublicMethod(Class<?> clazz, String methodName, Class[] parameterTypes) {
+    Method method = null;
+    try {
+      method = clazz.getMethod(methodName, parameterTypes);
+      if ((method.getModifiers() & Modifier.PUBLIC) == 0) {
+        method = null;
+      }
+    } catch (NoSuchMethodException e) {
+      // None.
+    }
+    return method;
+  }
+}
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
index 7d0e847..121b156 100644
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/android/main/java/com/squareup/okhttp/internal/Platform.java
@@ -30,8 +30,8 @@
 import java.util.zip.DeflaterOutputStream;
 import javax.net.ssl.SSLSocket;
 
-import com.android.org.conscrypt.OpenSSLSocketImpl;
 import com.squareup.okhttp.Protocol;
+
 import okio.ByteString;
 
 /**
@@ -44,6 +44,25 @@
         return PLATFORM;
     }
 
+    /** setUseSessionTickets(boolean) */
+    private static final OptionalMethod<Socket> SET_USE_SESSION_TICKETS =
+            new OptionalMethod<Socket>(null, "setUseSessionTickets", Boolean.TYPE);
+    /** setHostname(String) */
+    private static final OptionalMethod<Socket> SET_HOSTNAME =
+            new OptionalMethod<Socket>(null, "setHostname", String.class);
+    /** byte[] getAlpnSelectedProtocol() */
+    private static final OptionalMethod<Socket> GET_ALPN_SELECTED_PROTOCOL =
+            new OptionalMethod<Socket>(byte[].class, "getAlpnSelectedProtocol");
+    /** setAlpnSelectedProtocol(byte[]) */
+    private static final OptionalMethod<Socket> SET_ALPN_PROTOCOLS =
+            new OptionalMethod<Socket>(null, "setAlpnProtocols", byte[].class );
+    /** byte[] getNpnSelectedProtocol() */
+    private static final OptionalMethod<Socket> GET_NPN_SELECTED_PROTOCOL =
+            new OptionalMethod<Socket>(byte[].class, "getNpnSelectedProtocol");
+    /** setNpnSelectedProtocol(byte[]) */
+    private static final OptionalMethod<Socket> SET_NPN_PROTOCOLS =
+            new OptionalMethod<Socket>(null, "setNpnProtocols", byte[].class);
+
     public void logW(String warning) {
         System.logW(warning);
     }
@@ -61,11 +80,8 @@
     }
 
     public void enableTlsExtensions(SSLSocket socket, String uriHost) {
-        if (socket instanceof OpenSSLSocketImpl) {
-            OpenSSLSocketImpl openSSLSocket = (OpenSSLSocketImpl) socket;
-            openSSLSocket.setUseSessionTickets(true);
-            openSSLSocket.setHostname(uriHost);
-        }
+        SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(socket, true);
+        SET_HOSTNAME.invokeOptionalWithoutCheckedException(socket, uriHost);
     }
 
     public void supportTlsIntolerantServer(SSLSocket socket) {
@@ -97,18 +113,28 @@
      * Returns the negotiated protocol, or null if no protocol was negotiated.
      */
     public ByteString getNpnSelectedProtocol(SSLSocket socket) {
-        if (!(socket instanceof OpenSSLSocketImpl)) {
+        boolean alpnSupported = GET_ALPN_SELECTED_PROTOCOL.isSupported(socket);
+        boolean npnSupported = GET_NPN_SELECTED_PROTOCOL.isSupported(socket);
+        if (!(alpnSupported || npnSupported)) {
             return null;
         }
 
-        OpenSSLSocketImpl socketImpl = (OpenSSLSocketImpl) socket;
         // Prefer ALPN's result if it is present.
-        byte[] alpnResult = socketImpl.getAlpnSelectedProtocol();
-        if (alpnResult != null) {
-            return ByteString.of(alpnResult);
+        if (alpnSupported) {
+            byte[] alpnResult =
+                (byte[]) GET_ALPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
+            if (alpnResult != null) {
+                return ByteString.of(alpnResult);
+            }
         }
-        byte[] npnResult = socketImpl.getNpnSelectedProtocol();
-        return npnResult == null ? null : ByteString.of(npnResult);
+        if (npnSupported) {
+            byte[] npnResult =
+                (byte[]) GET_NPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
+            if (npnResult != null) {
+                return ByteString.of(npnResult);
+            }
+        }
+        return null;
     }
 
     /**
@@ -116,11 +142,20 @@
      * protocols are only sent if the socket implementation supports NPN.
      */
     public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
-        if (socket instanceof OpenSSLSocketImpl) {
-            OpenSSLSocketImpl socketImpl = (OpenSSLSocketImpl) socket;
-            byte[] protocols = concatLengthPrefixed(npnProtocols);
-            socketImpl.setAlpnProtocols(protocols);
-            socketImpl.setNpnProtocols(protocols);
+        boolean alpnSupported = SET_ALPN_PROTOCOLS.isSupported(socket);
+        boolean npnSupported = SET_NPN_PROTOCOLS.isSupported(socket);
+        if (!(alpnSupported || npnSupported)) {
+            return;
+        }
+
+        byte[] protocols = concatLengthPrefixed(npnProtocols);
+        if (alpnSupported) {
+            SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(
+                socket, new Object[] { protocols });
+        }
+        if (npnSupported) {
+            SET_NPN_PROTOCOLS.invokeWithoutCheckedException(
+                socket, new Object[] { protocols });
         }
     }
 
diff --git a/android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java b/android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java
new file mode 100644
index 0000000..c53fb21
--- /dev/null
+++ b/android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java
@@ -0,0 +1,337 @@
+/*
+ *  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.internal;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests for {@link OptionalMethod}.
+ */
+public class OptionalMethodTest {
+  @SuppressWarnings("unused")
+  private static class BaseClass {
+    public String stringMethod() {
+      return "string";
+    }
+
+    public void voidMethod() {}
+  }
+
+  @SuppressWarnings("unused")
+  private static class SubClass1 extends BaseClass {
+    public String subclassMethod() {
+      return "subclassMethod1";
+    }
+
+    public String methodWithArgs(String arg) {
+      return arg;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static class SubClass2 extends BaseClass {
+    public int subclassMethod() {
+      return 1234;
+    }
+
+    public String methodWithArgs(String arg) {
+      return arg;
+    }
+
+    public void throwsException() throws IOException {
+      throw new IOException();
+    }
+
+    public void throwsRuntimeException() throws Exception {
+      throw new NumberFormatException();
+    }
+
+    protected void nonPublic() {}
+  }
+
+  private final static OptionalMethod<BaseClass> STRING_METHOD_RETURNS_ANY =
+      new OptionalMethod<BaseClass>(null, "stringMethod");
+  private final static OptionalMethod<BaseClass> STRING_METHOD_RETURNS_STRING =
+      new OptionalMethod<BaseClass>(String.class, "stringMethod");
+  private final static OptionalMethod<BaseClass> STRING_METHOD_RETURNS_INT =
+      new OptionalMethod<BaseClass>(Integer.TYPE, "stringMethod");
+  private final static OptionalMethod<BaseClass> VOID_METHOD_RETURNS_ANY =
+      new OptionalMethod<BaseClass>(null, "voidMethod");
+  private final static OptionalMethod<BaseClass> VOID_METHOD_RETURNS_VOID =
+      new OptionalMethod<BaseClass>(Void.TYPE, "voidMethod");
+  private final static OptionalMethod<BaseClass> SUBCLASS_METHOD_RETURNS_ANY =
+      new OptionalMethod<BaseClass>(null, "subclassMethod");
+  private final static OptionalMethod<BaseClass> SUBCLASS_METHOD_RETURNS_STRING =
+      new OptionalMethod<BaseClass>(String.class, "subclassMethod");
+  private final static OptionalMethod<BaseClass> SUBCLASS_METHOD_RETURNS_INT =
+      new OptionalMethod<BaseClass>(Integer.TYPE, "subclassMethod");
+  private final static OptionalMethod<BaseClass> METHOD_WITH_ARGS_WRONG_PARAMS =
+      new OptionalMethod<BaseClass>(null, "methodWithArgs", Integer.class);
+  private final static OptionalMethod<BaseClass> METHOD_WITH_ARGS_CORRECT_PARAMS =
+      new OptionalMethod<BaseClass>(null, "methodWithArgs", String.class);
+
+  private final static OptionalMethod<BaseClass> THROWS_EXCEPTION =
+      new OptionalMethod<BaseClass>(null, "throwsException");
+  private final static OptionalMethod<BaseClass> THROWS_RUNTIME_EXCEPTION =
+      new OptionalMethod<BaseClass>(null, "throwsRuntimeException");
+  private final static OptionalMethod<BaseClass> NON_PUBLIC =
+      new OptionalMethod<BaseClass>(null, "nonPublic");
+
+  @Test
+  public void isSupported() throws Exception {
+    {
+      BaseClass base = new BaseClass();
+      assertTrue(STRING_METHOD_RETURNS_ANY.isSupported(base));
+      assertTrue(STRING_METHOD_RETURNS_STRING.isSupported(base));
+      assertFalse(STRING_METHOD_RETURNS_INT.isSupported(base));
+      assertTrue(VOID_METHOD_RETURNS_ANY.isSupported(base));
+      assertTrue(VOID_METHOD_RETURNS_VOID.isSupported(base));
+      assertFalse(SUBCLASS_METHOD_RETURNS_ANY.isSupported(base));
+      assertFalse(SUBCLASS_METHOD_RETURNS_STRING.isSupported(base));
+      assertFalse(SUBCLASS_METHOD_RETURNS_INT.isSupported(base));
+      assertFalse(METHOD_WITH_ARGS_WRONG_PARAMS.isSupported(base));
+      assertFalse(METHOD_WITH_ARGS_CORRECT_PARAMS.isSupported(base));
+    }
+    {
+      SubClass1 subClass1 = new SubClass1();
+      assertTrue(STRING_METHOD_RETURNS_ANY.isSupported(subClass1));
+      assertTrue(STRING_METHOD_RETURNS_STRING.isSupported(subClass1));
+      assertFalse(STRING_METHOD_RETURNS_INT.isSupported(subClass1));
+      assertTrue(VOID_METHOD_RETURNS_ANY.isSupported(subClass1));
+      assertTrue(VOID_METHOD_RETURNS_VOID.isSupported(subClass1));
+      assertTrue(SUBCLASS_METHOD_RETURNS_ANY.isSupported(subClass1));
+      assertTrue(SUBCLASS_METHOD_RETURNS_STRING.isSupported(subClass1));
+      assertFalse(SUBCLASS_METHOD_RETURNS_INT.isSupported(subClass1));
+      assertFalse(METHOD_WITH_ARGS_WRONG_PARAMS.isSupported(subClass1));
+      assertTrue(METHOD_WITH_ARGS_CORRECT_PARAMS.isSupported(subClass1));
+    }
+    {
+      SubClass2 subClass2 = new SubClass2();
+      assertTrue(STRING_METHOD_RETURNS_ANY.isSupported(subClass2));
+      assertTrue(STRING_METHOD_RETURNS_STRING.isSupported(subClass2));
+      assertFalse(STRING_METHOD_RETURNS_INT.isSupported(subClass2));
+      assertTrue(VOID_METHOD_RETURNS_ANY.isSupported(subClass2));
+      assertTrue(VOID_METHOD_RETURNS_VOID.isSupported(subClass2));
+      assertTrue(SUBCLASS_METHOD_RETURNS_ANY.isSupported(subClass2));
+      assertFalse(SUBCLASS_METHOD_RETURNS_STRING.isSupported(subClass2));
+      assertTrue(SUBCLASS_METHOD_RETURNS_INT.isSupported(subClass2));
+      assertFalse(METHOD_WITH_ARGS_WRONG_PARAMS.isSupported(subClass2));
+      assertTrue(METHOD_WITH_ARGS_CORRECT_PARAMS.isSupported(subClass2));
+    }
+  }
+
+  @Test
+  public void invoke() throws Exception {
+    {
+      BaseClass base = new BaseClass();
+      assertEquals("string", STRING_METHOD_RETURNS_STRING.invoke(base));
+      assertEquals("string", STRING_METHOD_RETURNS_ANY.invoke(base));
+      assertErrorOnInvoke(STRING_METHOD_RETURNS_INT, base);
+      assertNull(VOID_METHOD_RETURNS_ANY.invoke(base));
+      assertNull(VOID_METHOD_RETURNS_VOID.invoke(base));
+      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_ANY, base);
+      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_STRING, base);
+      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_INT, base);
+      assertErrorOnInvoke(METHOD_WITH_ARGS_WRONG_PARAMS, base);
+      assertErrorOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, base);
+    }
+    {
+      SubClass1 subClass1 = new SubClass1();
+      assertEquals("string", STRING_METHOD_RETURNS_STRING.invoke(subClass1));
+      assertEquals("string", STRING_METHOD_RETURNS_ANY.invoke(subClass1));
+      assertErrorOnInvoke(STRING_METHOD_RETURNS_INT, subClass1);
+      assertNull(VOID_METHOD_RETURNS_ANY.invoke(subClass1));
+      assertNull(VOID_METHOD_RETURNS_VOID.invoke(subClass1));
+      assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_ANY.invoke(subClass1));
+      assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_STRING.invoke(subClass1));
+      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_INT, subClass1);
+      assertErrorOnInvoke(METHOD_WITH_ARGS_WRONG_PARAMS, subClass1);
+      assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invoke(subClass1, "arg"));
+    }
+
+    {
+      SubClass2 subClass2 = new SubClass2();
+      assertEquals("string", STRING_METHOD_RETURNS_STRING.invoke(subClass2));
+      assertEquals("string", STRING_METHOD_RETURNS_ANY.invoke(subClass2));
+      assertErrorOnInvoke(STRING_METHOD_RETURNS_INT, subClass2);
+      assertNull(VOID_METHOD_RETURNS_ANY.invoke(subClass2));
+      assertNull(VOID_METHOD_RETURNS_VOID.invoke(subClass2));
+      assertEquals(1234, SUBCLASS_METHOD_RETURNS_ANY.invoke(subClass2));
+      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_STRING, subClass2);
+      assertEquals(1234, SUBCLASS_METHOD_RETURNS_INT.invoke(subClass2));
+      assertErrorOnInvoke(METHOD_WITH_ARGS_WRONG_PARAMS, subClass2);
+      assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invoke(subClass2, "arg"));
+    }
+  }
+
+  @Test
+  public void invokeBadArgs() throws Exception {
+    SubClass1 subClass1 = new SubClass1();
+    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1); // no args
+    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, 123);
+    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, true);
+    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, new Object());
+    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, "one", "two");
+  }
+
+  @Test
+  public void invokeWithException() throws Exception {
+    SubClass2 subClass2 = new SubClass2();
+    try {
+      THROWS_EXCEPTION.invoke(subClass2);
+    } catch (InvocationTargetException expected) {
+      assertTrue(expected.getTargetException() instanceof IOException);
+    }
+
+    try {
+      THROWS_RUNTIME_EXCEPTION.invoke(subClass2);
+    } catch (InvocationTargetException expected) {
+      assertTrue(expected.getTargetException() instanceof NumberFormatException);
+    }
+  }
+
+  @Test
+  public void invokeNonPublic() throws Exception {
+    SubClass2 subClass2 = new SubClass2();
+    assertFalse(NON_PUBLIC.isSupported(subClass2));
+    assertErrorOnInvoke(NON_PUBLIC, subClass2);
+  }
+
+  @Test
+  public void invokeOptional() throws Exception {
+    {
+      BaseClass base = new BaseClass();
+      assertEquals("string", STRING_METHOD_RETURNS_STRING.invokeOptional(base));
+      assertEquals("string", STRING_METHOD_RETURNS_ANY.invokeOptional(base));
+      assertNull(STRING_METHOD_RETURNS_INT.invokeOptional(base));
+      assertNull(VOID_METHOD_RETURNS_ANY.invokeOptional(base));
+      assertNull(VOID_METHOD_RETURNS_VOID.invokeOptional(base));
+      assertNull(SUBCLASS_METHOD_RETURNS_ANY.invokeOptional(base));
+      assertNull(SUBCLASS_METHOD_RETURNS_STRING.invokeOptional(base));
+      assertNull(SUBCLASS_METHOD_RETURNS_INT.invokeOptional(base));
+      assertNull(METHOD_WITH_ARGS_WRONG_PARAMS.invokeOptional(base));
+      assertNull(METHOD_WITH_ARGS_CORRECT_PARAMS.invokeOptional(base));
+    }
+    {
+      SubClass1 subClass1 = new SubClass1();
+      assertEquals("string", STRING_METHOD_RETURNS_STRING.invokeOptional(subClass1));
+      assertEquals("string", STRING_METHOD_RETURNS_ANY.invokeOptional(subClass1));
+      assertNull(STRING_METHOD_RETURNS_INT.invokeOptional(subClass1));
+      assertNull(VOID_METHOD_RETURNS_ANY.invokeOptional(subClass1));
+      assertNull(VOID_METHOD_RETURNS_VOID.invokeOptional(subClass1));
+      assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_ANY.invokeOptional(subClass1));
+      assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_STRING.invokeOptional(subClass1));
+      assertNull(SUBCLASS_METHOD_RETURNS_INT.invokeOptional(subClass1));
+      assertNull(METHOD_WITH_ARGS_WRONG_PARAMS.invokeOptional(subClass1));
+      assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invokeOptional(subClass1, "arg"));
+    }
+
+    {
+      SubClass2 subClass2 = new SubClass2();
+      assertEquals("string", STRING_METHOD_RETURNS_STRING.invokeOptional(subClass2));
+      assertEquals("string", STRING_METHOD_RETURNS_ANY.invokeOptional(subClass2));
+      assertNull(STRING_METHOD_RETURNS_INT.invokeOptional(subClass2));
+      assertNull(VOID_METHOD_RETURNS_ANY.invokeOptional(subClass2));
+      assertNull(VOID_METHOD_RETURNS_VOID.invokeOptional(subClass2));
+      assertEquals(1234, SUBCLASS_METHOD_RETURNS_ANY.invokeOptional(subClass2));
+      assertNull(SUBCLASS_METHOD_RETURNS_STRING.invokeOptional(subClass2));
+      assertEquals(1234, SUBCLASS_METHOD_RETURNS_INT.invokeOptional(subClass2));
+      assertNull(METHOD_WITH_ARGS_WRONG_PARAMS.invokeOptional(subClass2));
+      assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invokeOptional(subClass2, "arg"));
+    }
+  }
+
+  @Test
+  public void invokeOptionalBadArgs() throws Exception {
+    SubClass1 subClass1 = new SubClass1();
+    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1); // no args
+    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, 123);
+    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, true);
+    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, new Object());
+    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, "one", "two");
+  }
+
+  @Test
+  public void invokeOptionalWithException() throws Exception {
+    SubClass2 subClass2 = new SubClass2();
+    try {
+      THROWS_EXCEPTION.invokeOptional(subClass2);
+    } catch (InvocationTargetException expected) {
+      assertTrue(expected.getTargetException() instanceof IOException);
+    }
+
+    try {
+      THROWS_RUNTIME_EXCEPTION.invokeOptional(subClass2);
+    } catch (InvocationTargetException expected) {
+      assertTrue(expected.getTargetException() instanceof NumberFormatException);
+    }
+  }
+
+  @Test
+  public void invokeOptionalNonPublic() throws Exception {
+    SubClass2 subClass2 = new SubClass2();
+    assertFalse(NON_PUBLIC.isSupported(subClass2));
+    assertErrorOnInvokeOptional(NON_PUBLIC, subClass2);
+  }
+
+  private static <T> void assertErrorOnInvoke(
+      OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
+    try {
+      optionalMethod.invoke(base, args);
+      fail();
+    } catch (Error expected) {
+    }
+  }
+
+  private static <T> void assertIllegalArgumentExceptionOnInvoke(
+      OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
+    try {
+      optionalMethod.invoke(base, args);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  private static <T> void assertErrorOnInvokeOptional(
+      OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
+    try {
+      optionalMethod.invokeOptional(base, args);
+      fail();
+    } catch (Error expected) {
+    }
+  }
+
+  private static <T> void assertIllegalArgumentExceptionOnInvokeOptional(
+      OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
+    try {
+      optionalMethod.invokeOptional(base, args);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+}
diff --git a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
new file mode 100644
index 0000000..9e293f6
--- /dev/null
+++ b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
@@ -0,0 +1,234 @@
+/*
+ *  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.internal;
+
+import com.android.org.conscrypt.OpenSSLSocketImpl;
+import com.squareup.okhttp.Protocol;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import javax.net.ssl.HandshakeCompletedListener;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+import okio.ByteString;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for {@link Platform}.
+ */
+public class PlatformTest {
+
+  @Test
+  public void enableTlsExtensionOptionalMethods() throws Exception {
+    Platform platform = new Platform();
+
+    // Expect no error
+    TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
+    platform.enableTlsExtensions(arbitrarySocketImpl, "host");
+
+    FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
+    platform.enableTlsExtensions(openSslSocket, "host");
+    assertTrue(openSslSocket.useSessionTickets);
+    assertEquals("host", openSslSocket.hostname);
+  }
+
+  @Test
+  public void getNpnSelectedProtocol() throws Exception {
+    Platform platform = new Platform();
+    byte[] npnBytes = "npn".getBytes();
+    byte[] alpnBytes = "alpn".getBytes();
+
+    TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
+    assertNull(platform.getNpnSelectedProtocol(arbitrarySocketImpl));
+
+    NpnOnlySSLSocketImpl npnOnlySSLSocketImpl = new NpnOnlySSLSocketImpl();
+    npnOnlySSLSocketImpl.npnProtocols = npnBytes;
+    assertEquals(ByteString.of(npnBytes), platform.getNpnSelectedProtocol(npnOnlySSLSocketImpl));
+
+    FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
+    openSslSocket.npnProtocols = npnBytes;
+    openSslSocket.alpnProtocols = alpnBytes;
+    assertEquals(ByteString.of(alpnBytes), platform.getNpnSelectedProtocol(openSslSocket));
+  }
+
+  @Test
+  public void setNpnProtocols() throws Exception {
+    Platform platform = new Platform();
+    List<Protocol> protocols = Arrays.asList(Protocol.SPDY_3);
+
+    // No error
+    TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
+    platform.setNpnProtocols(arbitrarySocketImpl, protocols);
+
+    NpnOnlySSLSocketImpl npnOnlySSLSocketImpl = new NpnOnlySSLSocketImpl();
+    platform.setNpnProtocols(npnOnlySSLSocketImpl, protocols);
+    assertNotNull(npnOnlySSLSocketImpl.npnProtocols);
+
+    FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
+    platform.setNpnProtocols(openSslSocket, protocols);
+    assertNotNull(openSslSocket.npnProtocols);
+    assertNotNull(openSslSocket.alpnProtocols);
+  }
+
+  private static class FullOpenSSLSocketImpl extends OpenSSLSocketImpl {
+    private boolean useSessionTickets;
+    private String hostname;
+    private byte[] npnProtocols;
+    private byte[] alpnProtocols;
+
+    public FullOpenSSLSocketImpl() throws IOException {
+      super(null);
+    }
+
+    @Override
+    public void setUseSessionTickets(boolean useSessionTickets) {
+      this.useSessionTickets = useSessionTickets;
+    }
+
+    @Override
+    public void setHostname(String hostname) {
+      this.hostname = hostname;
+    }
+
+    @Override
+    public void setNpnProtocols(byte[] npnProtocols) {
+      this.npnProtocols = npnProtocols;
+    }
+
+    @Override
+    public byte[] getNpnSelectedProtocol() {
+      return npnProtocols;
+    }
+
+    @Override
+    public void setAlpnProtocols(byte[] alpnProtocols) {
+      this.alpnProtocols = alpnProtocols;
+    }
+
+    @Override
+    public byte[] getAlpnSelectedProtocol() {
+      return alpnProtocols;
+    }
+  }
+
+  // Legacy case
+  private static class NpnOnlySSLSocketImpl extends TestSSLSocketImpl {
+
+    private byte[] npnProtocols;
+
+    public void setNpnProtocols(byte[] npnProtocols) {
+      this.npnProtocols = npnProtocols;
+    }
+
+    public byte[] getNpnSelectedProtocol() {
+      return npnProtocols;
+    }
+  }
+
+  private static class TestSSLSocketImpl extends SSLSocket {
+
+    @Override
+    public String[] getSupportedCipherSuites() {
+      return new String[0];
+    }
+
+    @Override
+    public String[] getEnabledCipherSuites() {
+      return new String[0];
+    }
+
+    @Override
+    public void setEnabledCipherSuites(String[] suites) {
+    }
+
+    @Override
+    public String[] getSupportedProtocols() {
+      return new String[0];
+    }
+
+    @Override
+    public String[] getEnabledProtocols() {
+      return new String[0];
+    }
+
+    @Override
+    public void setEnabledProtocols(String[] protocols) {
+    }
+
+    @Override
+    public SSLSession getSession() {
+      return null;
+    }
+
+    @Override
+    public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
+    }
+
+    @Override
+    public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
+    }
+
+    @Override
+    public void startHandshake() throws IOException {
+    }
+
+    @Override
+    public void setUseClientMode(boolean mode) {
+    }
+
+    @Override
+    public boolean getUseClientMode() {
+      return false;
+    }
+
+    @Override
+    public void setNeedClientAuth(boolean need) {
+    }
+
+    @Override
+    public void setWantClientAuth(boolean want) {
+    }
+
+    @Override
+    public boolean getNeedClientAuth() {
+      return false;
+    }
+
+    @Override
+    public boolean getWantClientAuth() {
+      return false;
+    }
+
+    @Override
+    public void setEnableSessionCreation(boolean flag) {
+    }
+
+    @Override
+    public boolean getEnableSessionCreation() {
+      return false;
+    }
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
index 3ab47b4..a2a2653 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -380,6 +380,39 @@
     assertEquals(0, pool.getSpdyConnectionCount());
   }
 
+  // Tests to demonstrate Android bug http://b/18369687 and the solution to it.
+  @Test public void connectionCleanup_draining() throws IOException, InterruptedException {
+    ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
+
+    // Add 3 connections to the pool.
+    pool.recycle(httpA);
+    pool.recycle(httpB);
+    pool.share(spdyA);
+    assertEquals(3, pool.getConnectionCount());
+    assertEquals(2, pool.getHttpConnectionCount());
+    assertEquals(1, pool.getSpdyConnectionCount());
+
+    // With no method calls made to the pool it will not clean up any connections.
+    Thread.sleep(KEEP_ALIVE_DURATION_MS * 5);
+    assertEquals(3, pool.getConnectionCount());
+    assertEquals(2, pool.getHttpConnectionCount());
+    assertEquals(1, pool.getSpdyConnectionCount());
+
+    // Change the pool into a mode that will clean up connections.
+    pool.enterDrainMode();
+
+    // Give the drain thread a chance to run.
+    for (int i = 0; i < 5; i++) {
+      Thread.sleep(KEEP_ALIVE_DURATION_MS);
+      if (pool.isDrained()) {
+        break;
+      }
+    }
+
+    // All connections should have drained.
+    assertEquals(0, pool.getConnectionCount());
+  }
+
   @Test public void evictAllConnections() throws Exception {
     resetWithPoolSize(10);
     pool.recycle(httpA);
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java
new file mode 100644
index 0000000..2b05638
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2014 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.http;
+
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+public final class DisconnectTest {
+  private final MockWebServer server = new MockWebServer();
+  private final OkHttpClient client = new OkHttpClient();
+
+  @Test public void interruptWritingRequestBody() throws Exception {
+    int requestBodySize = 10 * 1024 * 1024; // 10 MiB
+
+    server.enqueue(new MockResponse()
+        .throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    disconnectLater(connection, 500);
+
+    connection.setDoOutput(true);
+    connection.setFixedLengthStreamingMode(requestBodySize);
+    OutputStream requestBody = connection.getOutputStream();
+    byte[] buffer = new byte[1024];
+    try {
+      for (int i = 0; i < requestBodySize; i += buffer.length) {
+        requestBody.write(buffer);
+        requestBody.flush();
+      }
+      fail("Expected connection to be closed");
+    } catch (IOException expected) {
+    }
+
+    connection.disconnect();
+  }
+
+  @Test public void interruptReadingResponseBody() throws Exception {
+    int responseBodySize = 10 * 1024 * 1024; // 10 MiB
+
+    server.enqueue(new MockResponse()
+        .setBody(new byte[responseBodySize])
+        .throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    disconnectLater(connection, 500);
+
+    InputStream responseBody = connection.getInputStream();
+    byte[] buffer = new byte[1024];
+    try {
+      while (responseBody.read(buffer) != -1) {
+      }
+      fail("Expected connection to be closed");
+    } catch (IOException expected) {
+    }
+
+    connection.disconnect();
+  }
+
+  private void disconnectLater(final HttpURLConnection connection, final int delayMillis) {
+    Thread interruptingCow = new Thread() {
+      @Override public void run() {
+        try {
+          sleep(delayMillis);
+          connection.disconnect();
+        } catch (InterruptedException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    };
+    interruptingCow.start();
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
index c8e2647..4c94e91 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
@@ -401,6 +401,24 @@
     assertEquals(regularRoutes.size(), routesWithFailedRoute.size());
   }
 
+  @Test public void getHostString() throws Exception {
+    // Name proxy specification.
+    InetSocketAddress socketAddress = InetSocketAddress.createUnresolved("host", 1234);
+    assertEquals("host", RouteSelector.getHostString(socketAddress));
+    socketAddress = InetSocketAddress.createUnresolved("127.0.0.1", 1234);
+    assertEquals("127.0.0.1", RouteSelector.getHostString(socketAddress));
+
+    // InetAddress proxy specification.
+    socketAddress = new InetSocketAddress(InetAddress.getByName("localhost"), 1234);
+    assertEquals("127.0.0.1", RouteSelector.getHostString(socketAddress));
+    socketAddress = new InetSocketAddress(
+        InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }), 1234);
+    assertEquals("127.0.0.1", RouteSelector.getHostString(socketAddress));
+    socketAddress = new InetSocketAddress(
+        InetAddress.getByAddress("foobar", new byte[] { 127, 0, 0, 1 }), 1234);
+    assertEquals("127.0.0.1", RouteSelector.getHostString(socketAddress));
+  }
+
   private void assertConnection(Connection connection, Address address, Proxy proxy,
       InetAddress socketAddress, int socketPort, boolean modernTls) {
     assertEquals(address, connection.getRoute().getAddress());
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
index a9f902a..0fcc4bd 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
@@ -65,6 +65,7 @@
 import java.util.Random;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
@@ -429,6 +430,11 @@
     HttpURLConnection connection1 = client.open(server.getUrl("/a"));
     connection1.setReadTimeout(100);
     assertContent("This connection won't pool properly", connection1);
+
+    // Give the server time to enact the socket policy if it's one that could happen after the
+    // client has received the response.
+    Thread.sleep(500);
+
     assertEquals(0, server.takeRequest().getSequenceNumber());
     HttpURLConnection connection2 = client.open(server.getUrl("/b"));
     connection2.setReadTimeout(100);
@@ -686,6 +692,10 @@
     client.setHostnameVerifier(new RecordingHostnameVerifier());
 
     assertContent("abc", client.open(server.getUrl("/")));
+
+    // Give the server time to disconnect.
+    Thread.sleep(500);
+
     assertContent("def", client.open(server.getUrl("/")));
 
     RecordedRequest request1 = server.takeRequest();
@@ -1029,7 +1039,9 @@
   }
 
   @Test public void disconnectedConnection() throws IOException {
-    server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"));
+    server.enqueue(new MockResponse()
+        .throttleBody(2, 100, TimeUnit.MILLISECONDS)
+        .setBody("ABCD"));
     server.play();
 
     connection = client.open(server.getUrl("/"));
@@ -1037,6 +1049,10 @@
     assertEquals('A', (char) in.read());
     connection.disconnect();
     try {
+      // Reading 'B' may succeed if it's buffered.
+      in.read();
+
+      // But 'C' shouldn't be buffered (the response is throttled) and this should fail.
       in.read();
       fail("Expected a connection closed exception");
     } catch (IOException expected) {
@@ -1272,6 +1288,9 @@
     // Seed the pool with a bad connection.
     assertContent("a", client.open(server.getUrl("/")));
 
+    // Give the server time to disconnect.
+    Thread.sleep(500);
+
     // This connection will need to be recovered. When it is, transparent gzip should still work!
     assertContent("b", client.open(server.getUrl("/")));
 
@@ -1317,11 +1336,13 @@
     HttpURLConnection connection1 = client.open(server.getUrl("/"));
     InputStream in1 = connection1.getInputStream();
     assertEquals("ABCDE", readAscii(in1, 5));
+    in1.close();
     connection1.disconnect();
 
     HttpURLConnection connection2 = client.open(server.getUrl("/"));
     InputStream in2 = connection2.getInputStream();
     assertEquals("LMNOP", readAscii(in2, 5));
+    in2.close();
     connection2.disconnect();
 
     assertEquals(0, server.takeRequest().getSequenceNumber());
@@ -2621,6 +2642,9 @@
 
     assertContent("A", client.open(server.getUrl("/a")));
 
+    // Give the server time to disconnect.
+    Thread.sleep(500);
+
     // If the request body is larger than OkHttp's replay buffer, the failure may still occur.
     byte[] requestBody = new byte[requestSize];
     new Random(0).nextBytes(requestBody);
@@ -2937,6 +2961,39 @@
   }
 
   /**
+   * Tolerate bad https proxy response when using HttpResponseCache. Android bug 6754912.
+   */
+  @Test
+  public void testConnectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache() throws Exception {
+    initResponseCache();
+
+    server.useHttps(sslContext.getSocketFactory(), true);
+    // The inclusion of a body in the response to a CONNECT is key to reproducing b/6754912.
+    MockResponse
+        badProxyResponse = new MockResponse()
+        .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+        .clearHeaders()
+        .setBody("bogus proxy connect response content");
+
+    server.enqueue(badProxyResponse);
+    server.enqueue(new MockResponse().setBody("response"));
+
+    server.play();
+
+    URL url = new URL("https://android.com/foo");
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+    ProxyConfig proxyConfig = ProxyConfig.PROXY_SYSTEM_PROPERTY;
+    HttpsURLConnection connection = (HttpsURLConnection) proxyConfig.connect(server, client, url);
+    assertContent("response", connection);
+
+    RecordedRequest connect = server.takeRequest();
+    assertEquals("CONNECT android.com:443 HTTP/1.1", connect.getRequestLine());
+    assertContains(connect.getHeaders(), "Host: android.com");
+  }
+
+  /**
    * The RFC is unclear in this regard as it only specifies that this should
    * invalidate the cache entry (if any).
    */
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
index f1decc8..82b1952 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
@@ -293,6 +293,7 @@
     assertTrue(verifier.verify("www.foo.com", session));
     assertTrue(verifier.verify("\u82b1\u5b50.foo.com", session));
     assertFalse(verifier.verify("a.b.foo.com", session));
+    assertFalse(verifier.verify("foo.com.au", session));
   }
 
   @Test public void verifyWilcardCnOnTld() throws Exception {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
index 94527af..743c33b 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
@@ -17,10 +17,12 @@
 package com.squareup.okhttp;
 
 import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Util;
 import com.squareup.okhttp.internal.http.HttpAuthenticator;
 import com.squareup.okhttp.internal.http.HttpConnection;
 import com.squareup.okhttp.internal.http.HttpEngine;
 import com.squareup.okhttp.internal.http.HttpTransport;
+import com.squareup.okhttp.internal.http.OkHeaders;
 import com.squareup.okhttp.internal.http.SpdyTransport;
 import com.squareup.okhttp.internal.spdy.SpdyConnection;
 import java.io.Closeable;
@@ -29,6 +31,8 @@
 import java.net.Socket;
 import javax.net.ssl.SSLSocket;
 import okio.ByteString;
+import okio.OkBuffer;
+import okio.Source;
 
 import static java.net.HttpURLConnection.HTTP_OK;
 import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
@@ -353,12 +357,22 @@
       tunnelConnection.writeRequest(request.headers(), requestLine);
       tunnelConnection.flush();
       Response response = tunnelConnection.readResponse().request(request).build();
-      tunnelConnection.emptyResponseBody();
+      // The response body from a CONNECT should be empty, but if it is not then we should consume
+      // it before proceeding.
+      long contentLength = OkHeaders.contentLength(response);
+      if (contentLength != -1) {
+        Source body = tunnelConnection.newFixedLengthSource(null, contentLength);
+        Util.skipAll(body, Integer.MAX_VALUE);
+      } else {
+        tunnelConnection.emptyResponseBody();
+      }
 
       switch (response.code()) {
         case HTTP_OK:
           // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If that
           // happens, then we will have buffered bytes that are needed by the SSLSocket!
+          // This check is imperfect: it doesn't tell us whether a handshake will succeed, just that
+          // it will almost certainly fail because the proxy has sent unexpected data.
           if (tunnelConnection.bufferSize() > 0) {
             throw new IOException("TLS tunnel buffered too many bytes!");
           }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
index bcb9b12..5c3b4c0 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
@@ -83,6 +83,62 @@
   private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
       60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
       Util.threadFactory("OkHttp ConnectionPool", true));
+
+  private enum CleanMode {
+    /**
+     * Connection clean up is driven by usage of the pool. Each usage of the pool can schedule a
+     * clean up. A pool left in this state and unused may contain idle connections indefinitely.
+     */
+    NORMAL,
+    /**
+     * Entered when a pool has been orphaned and is not expected to receive more usage, except for
+     * references held by existing connections. See {@link #enterDrainMode()}.
+     * A thread runs periodically to close idle connections in the pool until the pool is empty and
+     * then the state moves to {@link #DRAINED}.
+     */
+    DRAINING,
+    /**
+     * The pool is empty and no clean-up is taking place. Connections may still be added to the
+     * pool due to latent references to the pool, in which case the pool re-enters
+     * {@link #DRAINING}. If the pool is DRAINED and no longer referenced it is safe to be garbage
+     * collected.
+     */
+    DRAINED
+  }
+  /** The current mode for cleaning connections in the pool */
+  private CleanMode cleanMode = CleanMode.NORMAL;
+
+  // A scheduled drainModeRunnable keeps a reference to the enclosing ConnectionPool,
+  // preventing the ConnectionPool from being garbage collected before all held connections have
+  // been explicitly closed. If this was not the case any open connections in the pool would trigger
+  // StrictMode violations in Android when they were garbage collected. http://b/18369687
+  private final Runnable drainModeRunnable = new Runnable() {
+    @Override public void run() {
+      // Close any connections we can.
+      connectionsCleanupRunnable.run();
+
+      synchronized (ConnectionPool.this) {
+        // See whether we should continue checking the connection pool.
+        if (connections.size() > 0) {
+          // Pause to avoid checking too regularly, which would drain the battery on mobile
+          // devices. The wait() surrenders the pool monitor and will not block other calls.
+          try {
+            // Use the keep alive duration as a rough indicator of a good check interval.
+            long keepAliveDurationMillis = keepAliveDurationNs / (1000 * 1000);
+            ConnectionPool.this.wait(keepAliveDurationMillis);
+          } catch (InterruptedException e) {
+            // Ignored.
+          }
+
+          // Reschedule "this" to perform another clean-up.
+          executorService.execute(this);
+        } else {
+          cleanMode = CleanMode.DRAINED;
+        }
+      }
+    }
+  };
+
   private final Runnable connectionsCleanupRunnable = new Runnable() {
     @Override public void run() {
       List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
@@ -125,6 +181,7 @@
   /**
    * Returns a snapshot of the connections in this pool, ordered from newest to
    * oldest. Waits for the cleanup callable to run if it is currently scheduled.
+   * Only use in tests.
    */
   List<Connection> getConnections() {
     waitForCleanupCallableToRun();
@@ -205,7 +262,7 @@
       connections.addFirst(foundConnection); // Add it back after iteration.
     }
 
-    executorService.execute(connectionsCleanupRunnable);
+    scheduleCleanupAsRequired();
     return foundConnection;
   }
 
@@ -242,9 +299,9 @@
       connections.addFirst(connection);
       connection.incrementRecycleCount();
       connection.resetIdleStartTime();
+      scheduleCleanupAsRequired();
     }
 
-    executorService.execute(connectionsCleanupRunnable);
   }
 
   /**
@@ -253,10 +310,10 @@
    */
   public void share(Connection connection) {
     if (!connection.isSpdy()) throw new IllegalArgumentException();
-    executorService.execute(connectionsCleanupRunnable);
     if (connection.isAlive()) {
       synchronized (this) {
         connections.addFirst(connection);
+        scheduleCleanupAsRequired();
       }
     }
   }
@@ -286,4 +343,38 @@
     }
     maxIdleConnections = oldMaxIdleConections;
   }
+  /**
+   * A less abrupt way of draining the pool than {@link #evictAll()}. For use when the pool
+   * may still be referenced by active shared connections which cannot safely be closed.
+   */
+  public void enterDrainMode() {
+    synchronized(this) {
+      cleanMode = CleanMode.DRAINING;
+      executorService.execute(drainModeRunnable);
+    }
+  }
+
+  public boolean isDrained() {
+    synchronized(this) {
+      return cleanMode == CleanMode.DRAINED;
+    }
+  }
+
+  // Callers must synchronize on "this".
+  private void scheduleCleanupAsRequired() {
+    switch (cleanMode) {
+      case NORMAL:
+        executorService.execute(connectionsCleanupRunnable);
+        break;
+      case DRAINING:
+        // Do nothing -drainModeRunnable is already scheduled, and will reschedules itself as
+        // needed.
+        break;
+      case DRAINED:
+        // A new connection has potentially been offered up to a drained pool. Restart the drain.
+        cleanMode = CleanMode.DRAINING;
+        executorService.execute(drainModeRunnable);
+        break;
+    }
+  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
index b12b12d..718d471 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
@@ -122,6 +122,10 @@
     return state == STATE_CLOSED;
   }
 
+  public void closeIfOwnedBy(Object owner) throws IOException {
+    connection.closeIfOwnedBy(owner);
+  }
+
   public void flush() throws IOException {
     sink.flush();
   }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
index f00fbe7..d796a6c 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -411,6 +411,18 @@
   }
 
   /**
+   * Immediately closes the socket connection if it's currently held by this
+   * engine. Use this to interrupt an in-flight request from any thread. It's
+   * the caller's responsibility to close the request body and response body
+   * streams; otherwise resources may be leaked.
+   */
+  public final void disconnect() throws IOException {
+    if (transport != null) {
+      transport.disconnect(this);
+    }
+  }
+
+  /**
    * Release any resources held by this engine. If a connection is still held by
    * this engine, it is returned.
    */
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
index a1b367f..2ffe039 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
@@ -150,4 +150,8 @@
     // reference escapes.
     return httpConnection.newUnknownLengthSource(cacheRequest);
   }
+
+  @Override public void disconnect(HttpEngine engine) throws IOException {
+    httpConnection.closeIfOwnedBy(engine);
+  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
index 899d914..32be0be 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
@@ -109,9 +109,18 @@
 
   @Override public final void disconnect() {
     // Calling disconnect() before a connection exists should have no effect.
-    if (httpEngine != null) {
-      httpEngine.close();
+    if (httpEngine == null) return;
+
+    try {
+      httpEngine.disconnect();
+    } catch (IOException ignored) {
     }
+
+    // This doesn't close the stream because doing so would require all stream
+    // access to be synchronized. It's expected that the thread using the
+    // connection will close its streams directly. If it doesn't, the worst
+    // case is that the GzipSource's Inflater won't be released until it's
+    // finalized. (This logs a warning on Android.)
   }
 
   /**
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
index c634bab..305be25 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
@@ -215,7 +215,7 @@
 
     String socketHost;
     if (proxy.type() == Proxy.Type.DIRECT) {
-      socketHost = uri.getHost();
+      socketHost = address.getUriHost();
       socketPort = getEffectivePort(uri);
     } else {
       SocketAddress proxyAddress = proxy.address();
@@ -224,7 +224,7 @@
             "Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
       }
       InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
-      socketHost = proxySocketAddress.getHostName();
+      socketHost = getHostString(proxySocketAddress);
       socketPort = proxySocketAddress.getPort();
     }
 
@@ -233,6 +233,24 @@
     nextSocketAddressIndex = 0;
   }
 
+  /**
+   * Obtain a "host" from an {@link InetSocketAddress}. This returns a string containing either an
+   * actual host name or a numeric IP address.
+   */
+  // Visible for testing
+  static String getHostString(InetSocketAddress socketAddress) {
+    InetAddress address = socketAddress.getAddress();
+    if (address == null) {
+      // The InetSocketAddress was specified with a string (either a numeric IP or a host name). If
+      // it is a name, all IPs for that name should be tried. If it is an IP address, only that IP
+      // address should be tried.
+      return socketAddress.getHostName();
+    }
+    // The InetSocketAddress has a specific address: we should only try that address. Therefore we
+    // return the address and ignore any host name that may be available.
+    return address.getHostAddress();
+  }
+
   /** Returns true if there's another socket address to try. */
   private boolean hasNextInetSocketAddress() {
     return socketAddresses != null;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
index e775d34..9db9643 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
@@ -219,6 +219,10 @@
   @Override public void releaseConnectionOnIdle() {
   }
 
+  @Override public void disconnect(HttpEngine engine) throws IOException {
+    stream.close(ErrorCode.CANCEL);
+  }
+
   @Override public boolean canReuseConnection() {
     return true; // TODO: spdyConnection.isClosed() ?
   }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
index 94c90d4..852a15b 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
@@ -76,6 +76,8 @@
    */
   void releaseConnectionOnIdle() throws IOException;
 
+  void disconnect(HttpEngine engine) throws IOException;
+
   /**
    * Returns true if the socket connection held by this transport can be reused
    * for a follow-up exchange.
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
index a08773f..21e539c 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
@@ -162,7 +162,7 @@
       return hostName.equals(cn);
     }
 
-    if (cn.startsWith("*.") && hostName.regionMatches(0, cn, 2, cn.length() - 2)) {
+    if (cn.startsWith("*.") && hostName.equals(cn.substring(2))) {
       return true; // "*.foo.com" matches "foo.com"
     }