Merge "Initial checkin for okhttp."
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/README.android b/README.android
new file mode 100644
index 0000000..7ce5ec6
--- /dev/null
+++ b/README.android
@@ -0,0 +1,4 @@
+URL: https://github.com/square/okhttp
+License: Apache 2
+Description: "OkHttp: An HTTP+SPDY client for Android and Java applications."
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..410f18b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,71 @@
+OkHttp
+======
+
+An HTTP+SPDY client for Android and Java applications.
+
+
+Download
+--------
+
+Downloadable .jars can be found on the [GitHub download page][1].
+
+You can also depend on the .jar through Maven:
+
+```xml
+<dependency>
+    <groupId>com.squareup</groupId>
+    <artifactId>okhttp</artifactId>
+    <version>(insert latest version)</version>
+</dependency>
+```
+
+
+Known Issues
+------------
+
+The SPDY implementation is incomplete:
+
+* Settings frames are not honored. Flow control is not implemented.
+* It assumes a well-behaved peer. If the peer sends an invalid frame, OkHttp's SPDY client will not respond with the required `RST` frame.
+
+OkHttp uses the platform's [ProxySelector][2]. Prior to Android 4.0, `ProxySelector` didn't honor the `proxyHost` and `proxyPort` system properties for HTTPS connections. Work around this by specifying the `https.proxyHost` and `https.proxyPort` system properties when using a proxy with HTTPS.
+
+OkHttp's test suite creates an in-process HTTPS server. Prior to Android 2.3, SSL server sockets were broken, and so HTTPS tests will time out when run on such devices.
+
+
+Contributing
+------------
+
+If you would like to contribute code to OkHttp you can do so through GitHub by
+forking the repository and sending a pull request.
+
+When submitting code, please make every effort to follow existing conventions
+and style in order to keep the code as readable as possible. Please also make
+sure your code compiles by running `mvn clean verify`. Checkstyle failures
+during compilation indicate errors in your style and can be viewed in the
+`checkstyle-result.xml` file.
+
+Before your code can be accepted into the project you must also sign the
+[Individual Contributor License Agreement (CLA)][3].
+
+
+License
+-------
+
+    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.
+
+
+
+ [1]: http://github.com/square/okhttp/downloads
+ [2]: http://developer.android.com/reference/java/net/ProxySelector.html
+ [3]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1
diff --git a/checkstyle.xml b/checkstyle.xml
new file mode 100644
index 0000000..2864215
--- /dev/null
+++ b/checkstyle.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>

+<!DOCTYPE module PUBLIC

+    "-//Puppy Crawl//DTD Check Configuration 1.2//EN"

+    "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">

+

+<module name="Checker">

+    <module name="NewlineAtEndOfFile"/>

+    <module name="FileLength"/>

+    <module name="FileTabCharacter"/>

+

+    <!-- Trailing spaces -->

+    <module name="RegexpSingleline">

+        <property name="format" value="\s+$"/>

+        <property name="message" value="Line has trailing spaces."/>

+    </module>

+

+    <module name="TreeWalker">

+        <property name="cacheFile" value="${checkstyle.cache.file}"/>

+

+        <!-- Checks for Javadoc comments.                     -->

+        <!-- See http://checkstyle.sf.net/config_javadoc.html -->

+        <!--module name="JavadocMethod"/-->

+        <!--module name="JavadocType"/-->

+        <!--module name="JavadocVariable"/-->

+        <module name="JavadocStyle"/>

+

+

+        <!-- Checks for Naming Conventions.                  -->

+        <!-- See http://checkstyle.sf.net/config_naming.html -->

+        <module name="ConstantName"/>

+        <module name="LocalFinalVariableName"/>

+        <module name="LocalVariableName"/>

+        <module name="MemberName"/>

+        <module name="MethodName"/>

+        <module name="PackageName"/>

+        <module name="ParameterName"/>

+        <module name="StaticVariableName"/>

+        <module name="TypeName"/>

+

+

+        <!-- Checks for imports                              -->

+        <!-- See http://checkstyle.sf.net/config_import.html -->

+        <module name="AvoidStarImport"/>

+        <module name="IllegalImport"/> <!-- defaults to sun.* packages -->

+        <module name="RedundantImport"/>

+        <module name="UnusedImports"/>

+

+

+        <!-- Checks for Size Violations.                    -->

+        <!-- See http://checkstyle.sf.net/config_sizes.html -->

+        <module name="LineLength">

+            <property name="max" value="120"/>

+        </module>

+        <module name="MethodLength"/>

+        <module name="ParameterNumber"/>

+

+

+        <!-- Checks for whitespace                               -->

+        <!-- See http://checkstyle.sf.net/config_whitespace.html -->

+        <module name="GenericWhitespace"/>

+        <module name="EmptyForIteratorPad"/>

+        <module name="MethodParamPad"/>

+        <module name="NoWhitespaceAfter"/>

+        <module name="NoWhitespaceBefore"/>

+        <module name="OperatorWrap"/>

+        <module name="ParenPad"/>

+        <module name="TypecastParenPad"/>

+        <module name="WhitespaceAfter"/>

+        <module name="WhitespaceAround"/>

+

+

+        <!-- Modifier Checks                                    -->

+        <!-- See http://checkstyle.sf.net/config_modifiers.html -->

+        <!--module name="ModifierOrder"/-->

+        <module name="RedundantModifier"/>

+

+

+        <!-- Checks for blocks. You know, those {}'s         -->

+        <!-- See http://checkstyle.sf.net/config_blocks.html -->

+        <module name="AvoidNestedBlocks"/>

+        <!--module name="EmptyBlock"/-->

+        <module name="LeftCurly"/>

+        <module name="NeedBraces"/>

+        <module name="RightCurly"/>

+

+

+        <!-- Checks for common coding problems               -->

+        <!-- See http://checkstyle.sf.net/config_coding.html -->

+        <!--module name="AvoidInlineConditionals"/-->

+        <module name="CovariantEquals"/>

+        <module name="DoubleCheckedLocking"/>

+        <module name="EmptyStatement"/>

+        <!--<module name="EqualsAvoidNull"/>-->

+        <module name="EqualsHashCode"/>

+        <!--module name="HiddenField"/-->

+        <module name="IllegalInstantiation"/>

+        <!--module name="InnerAssignment"/-->

+        <!--module name="MagicNumber"/-->

+        <!--module name="MissingSwitchDefault"/-->

+        <module name="RedundantThrows"/>

+        <module name="SimplifyBooleanExpression"/>

+        <module name="SimplifyBooleanReturn"/>

+

+        <!-- Checks for class design                         -->

+        <!-- See http://checkstyle.sf.net/config_design.html -->

+        <!--module name="DesignForExtension"/-->

+        <module name="FinalClass"/>

+        <module name="HideUtilityClassConstructor"/>

+        <module name="InterfaceIsType"/>

+        <!--s/module name="VisibilityModifier"/-->

+

+

+        <!-- Miscellaneous other checks.                   -->

+        <!-- See http://checkstyle.sf.net/config_misc.html -->

+        <module name="ArrayTypeStyle"/>

+        <!--module name="FinalParameters"/-->

+        <!--module name="TodoComment"/-->

+        <module name="UpperEll"/>

+    </module>

+</module>

diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..336c964
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (C) 2012 Square, Inc.
+ Copyright (C) 2012 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.sonatype.oss</groupId>
+        <artifactId>oss-parent</artifactId>
+        <version>7</version>
+    </parent>
+    <groupId>com.squareup</groupId>
+    <artifactId>okhttp</artifactId>
+    <version>0.8-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <name>okhttp</name>
+    <description>An HTTP+SPDY client for Android and Java applications</description>
+    <url>https://github.com/square/okhttp</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+
+        <!-- Compilation -->
+        <java.version>1.6</java.version>
+        <npn.version>8.1.2.v20120308</npn.version>
+        <mockwebserver.version>20120731</mockwebserver.version>
+        <bouncycastle.version>1.47</bouncycastle.version>
+
+        <!-- Test Dependencies -->
+        <junit.version>3.8.2</junit.version>
+    </properties>
+
+    <scm>
+        <url>https://github.com/square/okhttp/</url>
+        <connection>scm:git:https://github.com/square/okhttp.git</connection>
+        <developerConnection>scm:git:git@github.com:square/okhttp.git</developerConnection>
+    </scm>
+
+    <issueManagement>
+        <system>GitHub Issues</system>
+        <url>https://github.com/square/okhttp/issues</url>
+    </issueManagement>
+
+    <licenses>
+        <license>
+            <name>Apache 2.0</name>
+            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+        </license>
+    </licenses>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.mortbay.jetty.npn</groupId>
+            <artifactId>npn-boot</artifactId>
+            <version>${npn.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.mockwebserver</groupId>
+            <artifactId>mockwebserver</artifactId>
+            <version>${mockwebserver.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15on</artifactId>
+            <version>${bouncycastle.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>2.5</version>
+                <configuration>
+                    <source>${java.version}</source>
+                    <target>${java.version}</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.sonatype.plugins</groupId>
+                <artifactId>jarjar-maven-plugin</artifactId>
+                <version>1.5</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>jarjar</goal>
+                        </goals>
+                        <configuration>
+                            <includes>
+                                <include>asm:asm</include>
+                                <include>org.sonatype.sisu.inject:cglib</include>
+                            </includes>
+                            <rules>
+                                <rule>
+                                    <pattern>libcore.**</pattern>
+                                    <result>com.squareup.okhttp.libcore.@1</result>
+                                </rule>
+                            </rules>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.9</version>
+                <configuration>
+                    <argLine>-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar</argLine>
+                </configuration>
+            </plugin>
+            <plugin>
+                <!--
+                  OkHttp requires with javac >= 1.7 for syncFlush on DeflaterOutputStream.
+                  Its language version must be <= 1.6 for dx.
+
+                  Running this code on Java 6 or earlier will fail at runtime due to the missing
+                  syncFlush API.
+
+                  Dalvik's core library includes syncFlush, but with an @hide tag so that it doesn't
+                  show up in the documentation or the android.jar stubs. This code works fine on
+                  Dalvik.
+                -->
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-enforcer-plugin</artifactId>
+                <version>1.1</version>
+                <executions>
+                    <execution>
+                        <id>enforce-java</id>
+                        <goals>
+                            <goal>enforce</goal>
+                        </goals>
+                        <configuration>
+                            <rules>
+                                <requireJavaVersion>
+                                    <version>[1.7.0,)</version>
+                                </requireJavaVersion>
+                            </rules>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-checkstyle-plugin</artifactId>
+                <version>2.9.1</version>
+                <configuration>
+                    <failsOnError>true</failsOnError>
+                    <configLocation>checkstyle.xml</configLocation>
+                    <excludes>**/OsConstants.java</excludes>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>verify</phase>
+                        <goals>
+                            <goal>checkstyle</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
+
diff --git a/src/main/java/com/squareup/okhttp/OkHttpConnection.java b/src/main/java/com/squareup/okhttp/OkHttpConnection.java
new file mode 100644
index 0000000..5df657a
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/OkHttpConnection.java
@@ -0,0 +1,829 @@
+/*
+ *  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 java.io.IOException;
+import java.io.InputStream;
+import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.SocketPermission;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Arrays;
+import libcore.net.http.HttpEngine;
+
+/**
+ * An {@link java.net.URLConnection} for HTTP (<a
+ * href="http://tools.ietf.org/html/rfc2616">RFC 2616</a>) used to send and
+ * receive data over the web. Data may be of any type and length. This class may
+ * be used to send and receive streaming data whose length is not known in
+ * advance.
+ *
+ * <p>Uses of this class follow a pattern:
+ * <ol>
+ *   <li>Obtain a new {@code HttpURLConnection} by calling {@link
+ *       java.net.URL#openConnection() URL.openConnection()} and casting the result to
+ *       {@code HttpURLConnection}.
+ *   <li>Prepare the request. The primary property of a request is its URI.
+ *       Request headers may also include metadata such as credentials, preferred
+ *       content types, and session cookies.
+ *   <li>Optionally upload a request body. Instances must be configured with
+ *       {@link #setDoOutput(boolean) setDoOutput(true)} if they include a
+ *       request body. Transmit data by writing to the stream returned by {@link
+ *       #getOutputStream()}.
+ *   <li>Read the response. Response headers typically include metadata such as
+ *       the response body's content type and length, modified dates and session
+ *       cookies. The response body may be read from the stream returned by {@link
+ *       #getInputStream()}. If the response has no body, that method returns an
+ *       empty stream.
+ *   <li>Disconnect. Once the response body has been read, the {@code
+ *       HttpURLConnection} should be closed by calling {@link #disconnect()}.
+ *       Disconnecting releases the resources held by a connection so they may
+ *       be closed or reused.
+ * </ol>
+ *
+ * <p>For example, to retrieve the webpage at {@code http://www.android.com/}:
+ * <pre>   {@code
+ *   URL url = new URL("http://www.android.com/");
+ *   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+ *   try {
+ *     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
+ *     readStream(in);
+ *   } finally {
+ *     urlConnection.disconnect();
+ *   }
+ * }</pre>
+ *
+ * <h3>Secure Communication with HTTPS</h3>
+ * Calling {@link java.net.URL#openConnection()} on a URL with the "https"
+ * scheme will return an {@code HttpsURLConnection}, which allows for
+ * overriding the default {@link javax.net.ssl.HostnameVerifier
+ * HostnameVerifier} and {@link javax.net.ssl.SSLSocketFactory
+ * SSLSocketFactory}. An application-supplied {@code SSLSocketFactory}
+ * created from an {@link javax.net.ssl.SSLContext SSLContext} can
+ * provide a custom {@link javax.net.ssl.X509TrustManager
+ * X509TrustManager} for verifying certificate chains and a custom
+ * {@link javax.net.ssl.X509KeyManager X509KeyManager} for supplying
+ * client certificates. See {@link OkHttpsConnection HttpsURLConnection} for
+ * more details.
+ *
+ * <h3>Response Handling</h3>
+ * {@code HttpURLConnection} will follow up to five HTTP redirects. It will
+ * follow redirects from one origin server to another. This implementation
+ * doesn't follow redirects from HTTPS to HTTP or vice versa.
+ *
+ * <p>If the HTTP response indicates that an error occurred, {@link
+ * #getInputStream()} will throw an {@link java.io.IOException}. Use {@link
+ * #getErrorStream()} to read the error response. The headers can be read in
+ * the normal way using {@link #getHeaderFields()},
+ *
+ * <h3>Posting Content</h3>
+ * To upload data to a web server, configure the connection for output using
+ * {@link #setDoOutput(boolean) setDoOutput(true)}.
+ *
+ * <p>For best performance, you should call either {@link
+ * #setFixedLengthStreamingMode(int)} when the body length is known in advance,
+ * or {@link #setChunkedStreamingMode(int)} when it is not. Otherwise {@code
+ * HttpURLConnection} will be forced to buffer the complete request body in
+ * memory before it is transmitted, wasting (and possibly exhausting) heap and
+ * increasing latency.
+ *
+ * <p>For example, to perform an upload: <pre>   {@code
+ *   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+ *   try {
+ *     urlConnection.setDoOutput(true);
+ *     urlConnection.setChunkedStreamingMode(0);
+ *
+ *     OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream());
+ *     writeStream(out);
+ *
+ *     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
+ *     readStream(in);
+ *   } finally {
+ *     urlConnection.disconnect();
+ *   }
+ * }</pre>
+ *
+ * <h3>Performance</h3>
+ * The input and output streams returned by this class are <strong>not
+ * buffered</strong>. Most callers should wrap the returned streams with {@link
+ * java.io.BufferedInputStream BufferedInputStream} or {@link
+ * java.io.BufferedOutputStream BufferedOutputStream}. Callers that do only bulk
+ * reads or writes may omit buffering.
+ *
+ * <p>When transferring large amounts of data to or from a server, use streams
+ * to limit how much data is in memory at once. Unless you need the entire
+ * body to be in memory at once, process it as a stream (rather than storing
+ * the complete body as a single byte array or string).
+ *
+ * <p>To reduce latency, this class may reuse the same underlying {@code Socket}
+ * for multiple request/response pairs. As a result, HTTP connections may be
+ * held open longer than necessary. Calls to {@link #disconnect()} may return
+ * the socket to a pool of connected sockets. This behavior can be disabled by
+ * setting the {@code http.keepAlive} system property to {@code false} before
+ * issuing any HTTP requests. The {@code http.maxConnections} property may be
+ * used to control how many idle connections to each server will be held.
+ *
+ * <p>By default, this implementation of {@code HttpURLConnection} requests that
+ * servers use gzip compression. Since {@link #getContentLength()} returns the
+ * number of bytes transmitted, you cannot use that method to predict how many
+ * bytes can be read from {@link #getInputStream()}. Instead, read that stream
+ * until it is exhausted: when {@link java.io.InputStream#read} returns -1. Gzip
+ * compression can be disabled by setting the acceptable encodings in the
+ * request header: <pre>   {@code
+ *   urlConnection.setRequestProperty("Accept-Encoding", "identity");
+ * }</pre>
+ *
+ * <h3>Handling Network Sign-On</h3>
+ * Some Wi-Fi networks block Internet access until the user clicks through a
+ * sign-on page. Such sign-on pages are typically presented by using HTTP
+ * redirects. You can use {@link #getURL()} to test if your connection has been
+ * unexpectedly redirected. This check is not valid until <strong>after</strong>
+ * the response headers have been received, which you can trigger by calling
+ * {@link #getHeaderFields()} or {@link #getInputStream()}. For example, to
+ * check that a response was not redirected to an unexpected host:
+ * <pre>   {@code
+ *   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+ *   try {
+ *     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
+ *     if (!url.getHost().equals(urlConnection.getURL().getHost())) {
+ *       // we were redirected! Kick the user out to the browser to sign on?
+ *     }
+ *     ...
+ *   } finally {
+ *     urlConnection.disconnect();
+ *   }
+ * }</pre>
+ *
+ * <h3>HTTP Authentication</h3>
+ * {@code HttpURLConnection} supports <a
+ * href="http://www.ietf.org/rfc/rfc2617">HTTP basic authentication</a>. Use
+ * {@link java.net.Authenticator} to set the VM-wide authentication handler:
+ * <pre>   {@code
+ *   Authenticator.setDefault(new Authenticator() {
+ *     protected PasswordAuthentication getPasswordAuthentication() {
+ *       return new PasswordAuthentication(username, password.toCharArray());
+ *     }
+ *   });
+ * }</pre>
+ * Unless paired with HTTPS, this is <strong>not</strong> a secure mechanism for
+ * user authentication. In particular, the username, password, request and
+ * response are all transmitted over the network without encryption.
+ *
+ * <h3>Sessions with Cookies</h3>
+ * To establish and maintain a potentially long-lived session between client
+ * and server, {@code HttpURLConnection} includes an extensible cookie manager.
+ * Enable VM-wide cookie management using {@link java.net.CookieHandler} and {@link
+ * java.net.CookieManager}: <pre>   {@code
+ *   CookieManager cookieManager = new CookieManager();
+ *   CookieHandler.setDefault(cookieManager);
+ * }</pre>
+ * By default, {@code CookieManager} accepts cookies from the <a
+ * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec1.html">origin
+ * server</a> only. Two other policies are included: {@link
+ * java.net.CookiePolicy#ACCEPT_ALL} and {@link java.net.CookiePolicy#ACCEPT_NONE}. Implement
+ * {@link java.net.CookiePolicy} to define a custom policy.
+ *
+ * <p>The default {@code CookieManager} keeps all accepted cookies in memory. It
+ * will forget these cookies when the VM exits. Implement {@link java.net.CookieStore} to
+ * define a custom cookie store.
+ *
+ * <p>In addition to the cookies set by HTTP responses, you may set cookies
+ * programmatically. To be included in HTTP request headers, cookies must have
+ * the domain and path properties set.
+ *
+ * <p>By default, new instances of {@code HttpCookie} work only with servers
+ * that support <a href="http://www.ietf.org/rfc/rfc2965.txt">RFC 2965</a>
+ * cookies. Many web servers support only the older specification, <a
+ * href="http://www.ietf.org/rfc/rfc2109.txt">RFC 2109</a>. For compatibility
+ * with the most web servers, set the cookie version to 0.
+ *
+ * <p>For example, to receive {@code www.twitter.com} in French: <pre>   {@code
+ *   HttpCookie cookie = new HttpCookie("lang", "fr");
+ *   cookie.setDomain("twitter.com");
+ *   cookie.setPath("/");
+ *   cookie.setVersion(0);
+ *   cookieManager.getCookieStore().add(new URI("http://twitter.com/"), cookie);
+ * }</pre>
+ *
+ * <h3>HTTP Methods</h3>
+ * <p>{@code HttpURLConnection} uses the {@code GET} method by default. It will
+ * use {@code POST} if {@link #setDoOutput setDoOutput(true)} has been called.
+ * Other HTTP methods ({@code OPTIONS}, {@code HEAD}, {@code PUT}, {@code
+ * DELETE} and {@code TRACE}) can be used with {@link #setRequestMethod}.
+ *
+ * <h3>Proxies</h3>
+ * By default, this class will connect directly to the <a
+ * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec1.html">origin
+ * server</a>. It can also connect via an {@link java.net.Proxy.Type#HTTP HTTP} or {@link
+ * java.net.Proxy.Type#SOCKS SOCKS} proxy. To use a proxy, use {@link
+ * java.net.URL#openConnection(java.net.Proxy) URL.openConnection(Proxy)} when creating the
+ * connection.
+ *
+ * <h3>IPv6 Support</h3>
+ * <p>This class includes transparent support for IPv6. For hosts with both IPv4
+ * and IPv6 addresses, it will attempt to connect to each of a host's addresses
+ * until a connection is established.
+ *
+ * <h3>Response Caching</h3>
+ * Android 4.0 (Ice Cream Sandwich) includes a response cache. See {@code
+ * android.net.http.HttpResponseCache} for instructions on enabling HTTP caching
+ * in your application.
+ *
+ * <h3>Avoiding Bugs In Earlier Releases</h3>
+ * Prior to Android 2.2 (Froyo), this class had some frustrating bugs. In
+ * particular, calling {@code close()} on a readable {@code InputStream} could
+ * <a href="http://code.google.com/p/android/issues/detail?id=2939">poison the
+ * connection pool</a>. Work around this by disabling connection pooling:
+ * <pre>   {@code
+ * private void disableConnectionReuseIfNecessary() {
+ *   // Work around pre-Froyo bugs in HTTP connection reuse.
+ *   if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {
+ *     System.setProperty("http.keepAlive", "false");
+ *   }
+ * }}</pre>
+ *
+ * <p>Each instance of {@code HttpURLConnection} may be used for one
+ * request/response pair. Instances of this class are not thread safe.
+ */
+public abstract class OkHttpConnection extends URLConnection {
+
+    /**
+     * The subset of HTTP methods that the user may select via {@link
+     * #setRequestMethod(String)}.
+     */
+    private static final String[] PERMITTED_USER_METHODS = {
+            HttpEngine.OPTIONS,
+            HttpEngine.GET,
+            HttpEngine.HEAD,
+            HttpEngine.POST,
+            HttpEngine.PUT,
+            HttpEngine.DELETE,
+            HttpEngine.TRACE
+            // Note: we don't allow users to specify "CONNECT"
+    };
+
+    /**
+     * The HTTP request method of this {@code HttpURLConnection}. The default
+     * value is {@code "GET"}.
+     */
+    protected String method = HttpEngine.GET;
+
+    /**
+     * The status code of the response obtained from the HTTP request. The
+     * default value is {@code -1}.
+     * <p>
+     * <li>1xx: Informational</li>
+     * <li>2xx: Success</li>
+     * <li>3xx: Relocation/Redirection</li>
+     * <li>4xx: Client Error</li>
+     * <li>5xx: Server Error</li>
+     */
+    protected int responseCode = -1;
+
+    /**
+     * The HTTP response message which corresponds to the response code.
+     */
+    protected String responseMessage;
+
+    /**
+     * Flag to define whether the protocol will automatically follow redirects
+     * or not. The default value is {@code true}.
+     */
+    protected boolean instanceFollowRedirects = followRedirects;
+
+    private static boolean followRedirects = true;
+
+    /**
+     * If the HTTP chunked encoding is enabled this parameter defines the
+     * chunk-length. Default value is {@code -1} that means the chunked encoding
+     * mode is disabled.
+     */
+    protected int chunkLength = -1;
+
+    /**
+     * If using HTTP fixed-length streaming mode this parameter defines the
+     * fixed length of content. Default value is {@code -1} that means the
+     * fixed-length streaming mode is disabled.
+     */
+    protected int fixedContentLength = -1;
+
+    // 2XX: generally "OK"
+    // 3XX: relocation/redirect
+    // 4XX: client error
+    // 5XX: server error
+    /**
+     * Numeric status code, 202: Accepted.
+     */
+    public static final int HTTP_ACCEPTED = 202;
+
+    /**
+     * Numeric status code, 502: Bad Gateway.
+     */
+    public static final int HTTP_BAD_GATEWAY = 502;
+
+    /**
+     * Numeric status code, 405: Bad Method.
+     */
+    public static final int HTTP_BAD_METHOD = 405;
+
+    /**
+     * Numeric status code, 400: Bad Request.
+     */
+    public static final int HTTP_BAD_REQUEST = 400;
+
+    /**
+     * Numeric status code, 408: Client Timeout.
+     */
+    public static final int HTTP_CLIENT_TIMEOUT = 408;
+
+    /**
+     * Numeric status code, 409: Conflict.
+     */
+    public static final int HTTP_CONFLICT = 409;
+
+    /**
+     * Numeric status code, 201: Created.
+     */
+    public static final int HTTP_CREATED = 201;
+
+    /**
+     * Numeric status code, 413: Entity too large.
+     */
+    public static final int HTTP_ENTITY_TOO_LARGE = 413;
+
+    /**
+     * Numeric status code, 403: Forbidden.
+     */
+    public static final int HTTP_FORBIDDEN = 403;
+
+    /**
+     * Numeric status code, 504: Gateway timeout.
+     */
+    public static final int HTTP_GATEWAY_TIMEOUT = 504;
+
+    /**
+     * Numeric status code, 410: Gone.
+     */
+    public static final int HTTP_GONE = 410;
+
+    /**
+     * Numeric status code, 500: Internal error.
+     */
+    public static final int HTTP_INTERNAL_ERROR = 500;
+
+    /**
+     * Numeric status code, 411: Length required.
+     */
+    public static final int HTTP_LENGTH_REQUIRED = 411;
+
+    /**
+     * Numeric status code, 301 Moved permanently.
+     */
+    public static final int HTTP_MOVED_PERM = 301;
+
+    /**
+     * Numeric status code, 302: Moved temporarily.
+     */
+    public static final int HTTP_MOVED_TEMP = 302;
+
+    /**
+     * Numeric status code, 300: Multiple choices.
+     */
+    public static final int HTTP_MULT_CHOICE = 300;
+
+    /**
+     * Numeric status code, 204: No content.
+     */
+    public static final int HTTP_NO_CONTENT = 204;
+
+    /**
+     * Numeric status code, 406: Not acceptable.
+     */
+    public static final int HTTP_NOT_ACCEPTABLE = 406;
+
+    /**
+     * Numeric status code, 203: Not authoritative.
+     */
+    public static final int HTTP_NOT_AUTHORITATIVE = 203;
+
+    /**
+     * Numeric status code, 404: Not found.
+     */
+    public static final int HTTP_NOT_FOUND = 404;
+
+    /**
+     * Numeric status code, 501: Not implemented.
+     */
+    public static final int HTTP_NOT_IMPLEMENTED = 501;
+
+    /**
+     * Numeric status code, 304: Not modified.
+     */
+    public static final int HTTP_NOT_MODIFIED = 304;
+
+    /**
+     * Numeric status code, 200: OK.
+     */
+    public static final int HTTP_OK = 200;
+
+    /**
+     * Numeric status code, 206: Partial.
+     */
+    public static final int HTTP_PARTIAL = 206;
+
+    /**
+     * Numeric status code, 402: Payment required.
+     */
+    public static final int HTTP_PAYMENT_REQUIRED = 402;
+
+    /**
+     * Numeric status code, 412: Precondition failed.
+     */
+    public static final int HTTP_PRECON_FAILED = 412;
+
+    /**
+     * Numeric status code, 407: Proxy authentication required.
+     */
+    public static final int HTTP_PROXY_AUTH = 407;
+
+    /**
+     * Numeric status code, 414: Request too long.
+     */
+    public static final int HTTP_REQ_TOO_LONG = 414;
+
+    /**
+     * Numeric status code, 205: Reset.
+     */
+    public static final int HTTP_RESET = 205;
+
+    /**
+     * Numeric status code, 303: See other.
+     */
+    public static final int HTTP_SEE_OTHER = 303;
+
+    /**
+     * Numeric status code, 500: Internal error.
+     *
+     * @deprecated Use {@link #HTTP_INTERNAL_ERROR}
+     */
+    @Deprecated
+    public static final int HTTP_SERVER_ERROR = 500;
+
+    /**
+     * Numeric status code, 305: Use proxy.
+     *
+     * <p>Like Firefox and Chrome, this class doesn't honor this response code.
+     * Other implementations respond to this status code by retrying the request
+     * using the HTTP proxy named by the response's Location header field.
+     */
+    public static final int HTTP_USE_PROXY = 305;
+
+    /**
+     * Numeric status code, 401: Unauthorized.
+     */
+    public static final int HTTP_UNAUTHORIZED = 401;
+
+    /**
+     * Numeric status code, 415: Unsupported type.
+     */
+    public static final int HTTP_UNSUPPORTED_TYPE = 415;
+
+    /**
+     * Numeric status code, 503: Unavailable.
+     */
+    public static final int HTTP_UNAVAILABLE = 503;
+
+    /**
+     * Numeric status code, 505: Version not supported.
+     */
+    public static final int HTTP_VERSION = 505;
+
+    /**
+     * Returns a new OkHttpConnection or OkHttpsConnection to {@code url}.
+     */
+    public static OkHttpConnection open(URL url) {
+        String protocol = url.getProtocol();
+        if (protocol.equals("http")) {
+            return new libcore.net.http.HttpURLConnectionImpl(url, 80);
+        } else if (protocol.equals("https")) {
+            return new libcore.net.http.HttpsURLConnectionImpl(url, 443);
+        } else {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    /**
+     * Returns a new OkHttpConnection or OkHttpsConnection to {@code url} that
+     * connects via {@code proxy}.
+     */
+    public static OkHttpConnection open(URL url, Proxy proxy) {
+        String protocol = url.getProtocol();
+        if (protocol.equals("http")) {
+            return new libcore.net.http.HttpURLConnectionImpl(url, 80, proxy);
+        } else if (protocol.equals("https")) {
+            return new libcore.net.http.HttpsURLConnectionImpl(url, 443, proxy);
+        } else {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    /**
+     * Constructs a new {@code HttpURLConnection} instance pointing to the
+     * resource specified by the {@code url}.
+     *
+     * @param url
+     *            the URL of this connection.
+     * @see java.net.URL
+     * @see java.net.URLConnection
+     */
+    protected OkHttpConnection(URL url) {
+        super(url);
+    }
+
+    /**
+     * Releases this connection so that its resources may be either reused or
+     * closed.
+     *
+     * <p>Unlike other Java implementations, this will not necessarily close
+     * socket connections that can be reused. You can disable all connection
+     * reuse by setting the {@code http.keepAlive} system property to {@code
+     * false} before issuing any HTTP requests.
+     */
+    public abstract void disconnect();
+
+    /**
+     * Returns an input stream from the server in the case of an error such as
+     * the requested file has not been found on the remote server. This stream
+     * can be used to read the data the server will send back.
+     *
+     * @return the error input stream returned by the server.
+     */
+    public InputStream getErrorStream() {
+        return null;
+    }
+
+    /**
+     * Returns the value of {@code followRedirects} which indicates if this
+     * connection follows a different URL redirected by the server. It is
+     * enabled by default.
+     *
+     * @return the value of the flag.
+     * @see #setFollowRedirects
+     */
+    public static boolean getFollowRedirects() {
+        return followRedirects;
+    }
+
+    /**
+     * Returns the permission object (in this case {@code SocketPermission})
+     * with the host and the port number as the target name and {@code
+     * "resolve, connect"} as the action list. If the port number of this URL
+     * instance is lower than {@code 0} the port will be set to {@code 80}.
+     *
+     * @return the permission object required for this connection.
+     * @throws java.io.IOException
+     *             if an IO exception occurs during the creation of the
+     *             permission object.
+     */
+    @Override
+    public java.security.Permission getPermission() throws IOException {
+        int port = url.getPort();
+        if (port < 0) {
+            port = 80;
+        }
+        return new SocketPermission(url.getHost() + ":" + port,
+                "connect, resolve");
+    }
+
+    /**
+     * Returns the request method which will be used to make the request to the
+     * remote HTTP server. All possible methods of this HTTP implementation is
+     * listed in the class definition.
+     *
+     * @return the request method string.
+     * @see #method
+     * @see #setRequestMethod
+     */
+    public String getRequestMethod() {
+        return method;
+    }
+
+    /**
+     * Returns the response code returned by the remote HTTP server.
+     *
+     * @return the response code, -1 if no valid response code.
+     * @throws java.io.IOException
+     *             if there is an IO error during the retrieval.
+     * @see #getResponseMessage
+     */
+    public int getResponseCode() throws IOException {
+        // Call getInputStream() first since getHeaderField() doesn't return
+        // exceptions
+        getInputStream();
+        String response = getHeaderField(0);
+        if (response == null) {
+            return -1;
+        }
+        response = response.trim();
+        int mark = response.indexOf(" ") + 1;
+        if (mark == 0) {
+            return -1;
+        }
+        int last = mark + 3;
+        if (last > response.length()) {
+            last = response.length();
+        }
+        responseCode = Integer.parseInt(response.substring(mark, last));
+        if (last + 1 <= response.length()) {
+            responseMessage = response.substring(last + 1);
+        }
+        return responseCode;
+    }
+
+    /**
+     * Returns the response message returned by the remote HTTP server.
+     *
+     * @return the response message. {@code null} if no such response exists.
+     * @throws java.io.IOException
+     *             if there is an error during the retrieval.
+     * @see #getResponseCode()
+     */
+    public String getResponseMessage() throws IOException {
+        if (responseMessage != null) {
+            return responseMessage;
+        }
+        getResponseCode();
+        return responseMessage;
+    }
+
+    /**
+     * Sets the flag of whether this connection will follow redirects returned
+     * by the remote server.
+     *
+     * @param auto
+     *            the value to enable or disable this option.
+     */
+    public static void setFollowRedirects(boolean auto) {
+        followRedirects = auto;
+    }
+
+    /**
+     * Sets the request command which will be sent to the remote HTTP server.
+     * This method can only be called before the connection is made.
+     *
+     * @param method
+     *            the string representing the method to be used.
+     * @throws java.net.ProtocolException
+     *             if this is called after connected, or the method is not
+     *             supported by this HTTP implementation.
+     * @see #getRequestMethod()
+     * @see #method
+     */
+    public void setRequestMethod(String method) throws ProtocolException {
+        if (connected) {
+            throw new ProtocolException("Connection already established");
+        }
+        for (String permittedUserMethod : PERMITTED_USER_METHODS) {
+            if (permittedUserMethod.equals(method)) {
+                // if there is a supported method that matches the desired
+                // method, then set the current method and return
+                this.method = permittedUserMethod;
+                return;
+            }
+        }
+        // if none matches, then throw ProtocolException
+        throw new ProtocolException("Unknown method '" + method + "'; must be one of "
+                + Arrays.toString(PERMITTED_USER_METHODS));
+    }
+
+    /**
+     * Returns whether this connection uses a proxy server or not.
+     *
+     * @return {@code true} if this connection passes a proxy server, false
+     *         otherwise.
+     */
+    public abstract boolean usingProxy();
+
+    /**
+     * Returns the encoding used to transmit the response body over the network.
+     * This is null or "identity" if the content was not encoded, or "gzip" if
+     * the body was gzip compressed. Most callers will be more interested in the
+     * {@link #getContentType() content type}, which may also include the
+     * content's character encoding.
+     */
+    @Override public String getContentEncoding() {
+        return super.getContentEncoding(); // overridden for Javadoc only
+    }
+
+    /**
+     * Returns whether this connection follows redirects.
+     *
+     * @return {@code true} if this connection follows redirects, false
+     *         otherwise.
+     */
+    public boolean getInstanceFollowRedirects() {
+        return instanceFollowRedirects;
+    }
+
+    /**
+     * Sets whether this connection follows redirects.
+     *
+     * @param followRedirects
+     *            {@code true} if this connection will follows redirects, false
+     *            otherwise.
+     */
+    public void setInstanceFollowRedirects(boolean followRedirects) {
+        instanceFollowRedirects = followRedirects;
+    }
+
+    /**
+     * Returns the date value in milliseconds since {@code 01.01.1970, 00:00h}
+     * corresponding to the header field {@code field}. The {@code defaultValue}
+     * will be returned if no such field can be found in the response header.
+     *
+     * @param field
+     *            the header field name.
+     * @param defaultValue
+     *            the default value to use if the specified header field wont be
+     *            found.
+     * @return the header field represented in milliseconds since January 1,
+     *         1970 GMT.
+     */
+    @Override
+    public long getHeaderFieldDate(String field, long defaultValue) {
+        return super.getHeaderFieldDate(field, defaultValue);
+    }
+
+    /**
+     * If the length of a HTTP request body is known ahead, sets fixed length to
+     * enable streaming without buffering. Sets after connection will cause an
+     * exception.
+     *
+     * @see #setChunkedStreamingMode
+     * @param contentLength
+     *            the fixed length of the HTTP request body.
+     * @throws IllegalStateException
+     *             if already connected or another mode already set.
+     * @throws IllegalArgumentException
+     *             if {@code contentLength} is less than zero.
+     */
+    public void setFixedLengthStreamingMode(int contentLength) {
+        if (super.connected) {
+            throw new IllegalStateException("Already connected");
+        }
+        if (chunkLength > 0) {
+            throw new IllegalStateException("Already in chunked mode");
+        }
+        if (contentLength < 0) {
+            throw new IllegalArgumentException("contentLength < 0");
+        }
+        this.fixedContentLength = contentLength;
+    }
+
+    /**
+     * Stream a request body whose length is not known in advance. Old HTTP/1.0
+     * only servers may not support this mode.
+     *
+     * <p>When HTTP chunked encoding is used, the stream is divided into
+     * chunks, each prefixed with a header containing the chunk's size. Setting
+     * a large chunk length requires a large internal buffer, potentially
+     * wasting memory. Setting a small chunk length increases the number of
+     * bytes that must be transmitted because of the header on every chunk.
+     * Most caller should use {@code 0} to get the system default.
+     *
+     * @see #setFixedLengthStreamingMode
+     * @param chunkLength the length to use, or {@code 0} for the default chunk
+     *     length.
+     * @throws IllegalStateException if already connected or another mode
+     *     already set.
+     */
+    public void setChunkedStreamingMode(int chunkLength) {
+        if (super.connected) {
+            throw new IllegalStateException("Already connected");
+        }
+        if (fixedContentLength >= 0) {
+            throw new IllegalStateException("Already in fixed-length mode");
+        }
+        if (chunkLength <= 0) {
+            this.chunkLength = HttpEngine.DEFAULT_CHUNK_LENGTH;
+        } else {
+            this.chunkLength = chunkLength;
+        }
+    }
+}
diff --git a/src/main/java/com/squareup/okhttp/OkHttpsConnection.java b/src/main/java/com/squareup/okhttp/OkHttpsConnection.java
new file mode 100644
index 0000000..669a26e
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/OkHttpsConnection.java
@@ -0,0 +1,300 @@
+/*
+ *  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 java.net.URL;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * An {@link java.net.HttpURLConnection} for HTTPS (<a
+ * href="http://tools.ietf.org/html/rfc2818">RFC 2818</a>). A
+ * connected {@code HttpsURLConnection} allows access to the
+ * negotiated cipher suite, the server certificate chain, and the
+ * client certificate chain if any.
+ *
+ * <h3>Providing an application specific X509TrustManager</h3>
+ *
+ * If an application wants to trust Certificate Authority (CA)
+ * certificates that are not part of the system, it should specify its
+ * own {@code X509TrustManager} via a {@code SSLSocketFactory} set on
+ * the {@code HttpsURLConnection}. The {@code X509TrustManager} can be
+ * created based on a {@code KeyStore} using a {@code
+ * TrustManagerFactory} to supply trusted CA certificates. Note that
+ * self-signed certificates are effectively their own CA and can be
+ * trusted by including them in a {@code KeyStore}.
+ *
+ * <p>For example, to trust a set of certificates specified by a {@code KeyStore}:
+ * <pre>   {@code
+ *   KeyStore keyStore = ...;
+ *   TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
+ *   tmf.init(keyStore);
+ *
+ *   SSLContext context = SSLContext.getInstance("TLS");
+ *   context.init(null, tmf.getTrustManagers(), null);
+ *
+ *   URL url = new URL("https://www.example.com/");
+ *   HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
+ *   urlConnection.setSSLSocketFactory(context.getSocketFactory());
+ *   InputStream in = urlConnection.getInputStream();
+ * }</pre>
+ *
+ * <p>It is possible to implement {@code X509TrustManager} directly
+ * instead of using one created by a {@code
+ * TrustManagerFactory}. While this is straightforward in the insecure
+ * case of allowing all certificate chains to pass verification,
+ * writing a proper implementation will usually want to take advantage
+ * of {@link java.security.cert.CertPathValidator
+ * CertPathValidator}. In general, it might be better to write a
+ * custom {@code KeyStore} implementation to pass to the {@code
+ * TrustManagerFactory} than to try and write a custom {@code
+ * X509TrustManager}.
+ *
+ * <h3>Providing an application specific X509KeyManager</h3>
+ *
+ * A custom {@code X509KeyManager} can be used to supply a client
+ * certificate and its associated private key to authenticate a
+ * connection to the server. The {@code X509KeyManager} can be created
+ * based on a {@code KeyStore} using a {@code KeyManagerFactory}.
+ *
+ * <p>For example, to supply client certificates from a {@code KeyStore}:
+ * <pre>   {@code
+ *   KeyStore keyStore = ...;
+ *   KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509");
+ *   kmf.init(keyStore);
+ *
+ *   SSLContext context = SSLContext.getInstance("TLS");
+ *   context.init(kmf.getKeyManagers(), null, null);
+ *
+ *   URL url = new URL("https://www.example.com/");
+ *   HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
+ *   urlConnection.setSSLSocketFactory(context.getSocketFactory());
+ *   InputStream in = urlConnection.getInputStream();
+ * }</pre>
+ *
+ * <p>A {@code X509KeyManager} can also be implemented directly. This
+ * can allow an application to return a certificate and private key
+ * from a non-{@code KeyStore} source or to specify its own logic for
+ * selecting a specific credential to use when many may be present in
+ * a single {@code KeyStore}.
+ *
+ * <h3>TLS Intolerance Support</h3>
+ *
+ * This class attempts to create secure connections using common TLS
+ * extensions and SSL deflate compression. Should that fail, the
+ * connection will be retried with SSLv3 only.
+ */
+public abstract class OkHttpsConnection extends OkHttpConnection {
+
+    private static HostnameVerifier defaultHostnameVerifier
+            = HttpsURLConnection.getDefaultHostnameVerifier();
+
+    private static SSLSocketFactory defaultSSLSocketFactory = (SSLSocketFactory) SSLSocketFactory
+            .getDefault();
+
+    /**
+     * Sets the default hostname verifier to be used by new instances.
+     *
+     * @param v
+     *            the new default hostname verifier
+     * @throws IllegalArgumentException
+     *             if the specified verifier is {@code null}.
+     */
+    public static void setDefaultHostnameVerifier(HostnameVerifier v) {
+        if (v == null) {
+            throw new IllegalArgumentException("HostnameVerifier is null");
+        }
+        defaultHostnameVerifier = v;
+    }
+
+    /**
+     * Returns the default hostname verifier.
+     *
+     * @return the default hostname verifier.
+     */
+    public static HostnameVerifier getDefaultHostnameVerifier() {
+        return defaultHostnameVerifier;
+    }
+
+    /**
+     * Sets the default SSL socket factory to be used by new instances.
+     *
+     * @param sf
+     *            the new default SSL socket factory.
+     * @throws IllegalArgumentException
+     *             if the specified socket factory is {@code null}.
+     */
+    public static void setDefaultSSLSocketFactory(SSLSocketFactory sf) {
+        if (sf == null) {
+            throw new IllegalArgumentException("SSLSocketFactory is null");
+        }
+        defaultSSLSocketFactory = sf;
+    }
+
+    /**
+     * Returns the default SSL socket factory for new instances.
+     *
+     * @return the default SSL socket factory for new instances.
+     */
+    public static SSLSocketFactory getDefaultSSLSocketFactory() {
+        return defaultSSLSocketFactory;
+    }
+
+    /**
+     * The host name verifier used by this connection. It is initialized from
+     * the default hostname verifier
+     * {@link #setDefaultHostnameVerifier(javax.net.ssl.HostnameVerifier)} or
+     * {@link #getDefaultHostnameVerifier()}.
+     */
+    protected HostnameVerifier hostnameVerifier;
+
+    private SSLSocketFactory sslSocketFactory;
+
+    /**
+     * Creates a new {@code HttpsURLConnection} with the specified {@code URL}.
+     *
+     * @param url
+     *            the {@code URL} to connect to.
+     */
+    protected OkHttpsConnection(URL url) {
+        super(url);
+        hostnameVerifier = defaultHostnameVerifier;
+        sslSocketFactory = defaultSSLSocketFactory;
+    }
+
+    /**
+     * Returns the name of the cipher suite negotiated during the SSL handshake.
+     *
+     * @return the name of the cipher suite negotiated during the SSL handshake.
+     * @throws IllegalStateException
+     *             if no connection has been established yet.
+     */
+    public abstract String getCipherSuite();
+
+    /**
+     * Returns the list of local certificates used during the handshake. These
+     * certificates were sent to the peer.
+     *
+     * @return Returns the list of certificates used during the handshake with
+     *         the local identity certificate followed by CAs, or {@code null}
+     *         if no certificates were used during the handshake.
+     * @throws IllegalStateException
+     *             if no connection has been established yet.
+     */
+    public abstract Certificate[] getLocalCertificates();
+
+    /**
+     * Return the list of certificates identifying the peer during the
+     * handshake.
+     *
+     * @return the list of certificates identifying the peer with the peer's
+     *         identity certificate followed by CAs.
+     * @throws javax.net.ssl.SSLPeerUnverifiedException
+     *             if the identity of the peer has not been verified..
+     * @throws IllegalStateException
+     *             if no connection has been established yet.
+     */
+    public abstract Certificate[] getServerCertificates() throws SSLPeerUnverifiedException;
+
+    /**
+     * Returns the {@code Principal} identifying the peer.
+     *
+     * @return the {@code Principal} identifying the peer.
+     * @throws javax.net.ssl.SSLPeerUnverifiedException
+     *             if the identity of the peer has not been verified.
+     * @throws IllegalStateException
+     *             if no connection has been established yet.
+     */
+    public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+        Certificate[] certs = getServerCertificates();
+        if (certs == null || certs.length == 0 || (!(certs[0] instanceof X509Certificate))) {
+            throw new SSLPeerUnverifiedException("No server's end-entity certificate");
+        }
+        return ((X509Certificate) certs[0]).getSubjectX500Principal();
+    }
+
+    /**
+     * Returns the {@code Principal} used to identify the local host during the handshake.
+     *
+     * @return the {@code Principal} used to identify the local host during the handshake, or
+     *         {@code null} if none was used.
+     * @throws IllegalStateException
+     *             if no connection has been established yet.
+     */
+    public Principal getLocalPrincipal() {
+        Certificate[] certs = getLocalCertificates();
+        if (certs == null || certs.length == 0 || (!(certs[0] instanceof X509Certificate))) {
+            return null;
+        }
+        return ((X509Certificate) certs[0]).getSubjectX500Principal();
+    }
+
+    /**
+     * Sets the hostname verifier for this instance.
+     *
+     * @param v
+     *            the hostname verifier for this instance.
+     * @throws IllegalArgumentException
+     *             if the specified verifier is {@code null}.
+     */
+    public void setHostnameVerifier(HostnameVerifier v) {
+        if (v == null) {
+            throw new IllegalArgumentException("HostnameVerifier is null");
+        }
+        hostnameVerifier = v;
+    }
+
+    /**
+     * Returns the hostname verifier used by this instance.
+     *
+     * @return the hostname verifier used by this instance.
+     */
+    public HostnameVerifier getHostnameVerifier() {
+        return hostnameVerifier;
+    }
+
+    /**
+     * Sets the SSL socket factory for this instance.
+     *
+     * @param sf
+     *            the SSL socket factory to be used by this instance.
+     * @throws IllegalArgumentException
+     *             if the specified socket factory is {@code null}.
+     */
+    public void setSSLSocketFactory(SSLSocketFactory sf) {
+        if (sf == null) {
+            throw new IllegalArgumentException("SSLSocketFactory is null");
+        }
+        sslSocketFactory = sf;
+    }
+
+    /**
+     * Returns the SSL socket factory used by this instance.
+     *
+     * @return the SSL socket factory used by this instance.
+     */
+    public SSLSocketFactory getSSLSocketFactory() {
+        return sslSocketFactory;
+    }
+
+}
diff --git a/src/main/java/libcore/io/AsynchronousCloseMonitor.java b/src/main/java/libcore/io/AsynchronousCloseMonitor.java
new file mode 100644
index 0000000..62eec24
--- /dev/null
+++ b/src/main/java/libcore/io/AsynchronousCloseMonitor.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2011 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 libcore.io;
+
+import java.io.FileDescriptor;
+
+public final class AsynchronousCloseMonitor {
+    private AsynchronousCloseMonitor() {
+    }
+
+    public static native void signalBlockedThreads(FileDescriptor fd);
+}
diff --git a/src/main/java/libcore/io/Base64.java b/src/main/java/libcore/io/Base64.java
new file mode 100644
index 0000000..96d3b9d
--- /dev/null
+++ b/src/main/java/libcore/io/Base64.java
@@ -0,0 +1,165 @@
+/*
+ *  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.
+ */
+
+/**
+* @author Alexander Y. Kleymenov
+*/
+
+package libcore.io;
+
+import java.io.UnsupportedEncodingException;
+import libcore.util.EmptyArray;
+
+/**
+ * <a href="http://www.ietf.org/rfc/rfc2045.txt">Base64</a> encoder/decoder.
+ * In violation of the RFC, this encoder doesn't wrap lines at 76 columns.
+ */
+public final class Base64 {
+    private Base64() {
+    }
+
+    public static byte[] decode(byte[] in) {
+        return decode(in, in.length);
+    }
+
+    public static byte[] decode(byte[] in, int len) {
+        // approximate output length
+        int length = len / 4 * 3;
+        // return an empty array on empty or short input without padding
+        if (length == 0) {
+            return EmptyArray.BYTE;
+        }
+        // temporary array
+        byte[] out = new byte[length];
+        // number of padding characters ('=')
+        int pad = 0;
+        byte chr;
+        // compute the number of the padding characters
+        // and adjust the length of the input
+        for (;; len--) {
+            chr = in[len - 1];
+            // skip the neutral characters
+            if ((chr == '\n') || (chr == '\r')
+                    || (chr == ' ') || (chr == '\t')) {
+                continue;
+            }
+            if (chr == '=') {
+                pad++;
+            } else {
+                break;
+            }
+        }
+        // index in the output array
+        int outIndex = 0;
+        // index in the input array
+        int inIndex = 0;
+        // holds the value of the input character
+        int bits = 0;
+        // holds the value of the input quantum
+        int quantum = 0;
+        for (int i = 0; i < len; i++) {
+            chr = in[i];
+            // skip the neutral characters
+            if ((chr == '\n') || (chr == '\r')
+                    || (chr == ' ') || (chr == '\t')) {
+                continue;
+            }
+            if ((chr >= 'A') && (chr <= 'Z')) {
+                // char ASCII value
+                //  A    65    0
+                //  Z    90    25 (ASCII - 65)
+                bits = chr - 65;
+            } else if ((chr >= 'a') && (chr <= 'z')) {
+                // char ASCII value
+                //  a    97    26
+                //  z    122   51 (ASCII - 71)
+                bits = chr - 71;
+            } else if ((chr >= '0') && (chr <= '9')) {
+                // char ASCII value
+                //  0    48    52
+                //  9    57    61 (ASCII + 4)
+                bits = chr + 4;
+            } else if (chr == '+') {
+                bits = 62;
+            } else if (chr == '/') {
+                bits = 63;
+            } else {
+                return null;
+            }
+            // append the value to the quantum
+            quantum = (quantum << 6) | (byte) bits;
+            if (inIndex % 4 == 3) {
+                // 4 characters were read, so make the output:
+                out[outIndex++] = (byte) (quantum >> 16);
+                out[outIndex++] = (byte) (quantum >> 8);
+                out[outIndex++] = (byte) quantum;
+            }
+            inIndex++;
+        }
+        if (pad > 0) {
+            // adjust the quantum value according to the padding
+            quantum = quantum << (6 * pad);
+            // make output
+            out[outIndex++] = (byte) (quantum >> 16);
+            if (pad == 1) {
+                out[outIndex++] = (byte) (quantum >> 8);
+            }
+        }
+        // create the resulting array
+        byte[] result = new byte[outIndex];
+        System.arraycopy(out, 0, result, 0, outIndex);
+        return result;
+    }
+
+    private static final byte[] MAP = new byte[]
+        {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
+         'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b',
+         'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
+         'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
+         '4', '5', '6', '7', '8', '9', '+', '/'};
+
+    public static String encode(byte[] in) {
+        int length = (in.length + 2) * 4 / 3;
+        byte[] out = new byte[length];
+        int index = 0, end = in.length - in.length % 3;
+        for (int i = 0; i < end; i += 3) {
+            out[index++] = MAP[(in[i] & 0xff) >> 2];
+            out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)];
+            out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)];
+            out[index++] = MAP[(in[i + 2] & 0x3f)];
+        }
+        switch (in.length % 3) {
+            case 1:
+                out[index++] = MAP[(in[end] & 0xff) >> 2];
+                out[index++] = MAP[(in[end] & 0x03) << 4];
+                out[index++] = '=';
+                out[index++] = '=';
+                break;
+            case 2:
+                out[index++] = MAP[(in[end] & 0xff) >> 2];
+                out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)];
+                out[index++] = MAP[((in[end + 1] & 0x0f) << 2)];
+                out[index++] = '=';
+                break;
+        }
+        try {
+            return new String(out, 0, index, "US-ASCII");
+        } catch (UnsupportedEncodingException e) {
+            throw new AssertionError(e);
+        }
+    }
+}
diff --git a/src/main/java/libcore/io/BufferIterator.java b/src/main/java/libcore/io/BufferIterator.java
new file mode 100644
index 0000000..7f3ad47
--- /dev/null
+++ b/src/main/java/libcore/io/BufferIterator.java
@@ -0,0 +1,62 @@
+/*
+ * 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 libcore.io;
+
+/**
+ * Iterates over big- or little-endian bytes. See {@link MemoryMappedFile#bigEndianIterator} and
+ * {@link MemoryMappedFile#littleEndianIterator}.
+ *
+ * @hide don't make this public without adding bounds checking.
+ */
+public abstract class BufferIterator {
+    /**
+     * Seeks to the absolute position {@code offset}, measured in bytes from the start.
+     */
+    public abstract void seek(int offset);
+
+    /**
+     * Skips forwards or backwards {@code byteCount} bytes from the current position.
+     */
+    public abstract void skip(int byteCount);
+
+    /**
+     * Copies {@code byteCount} bytes from the current position into {@code dst}, starting at
+     * {@code dstOffset}, and advances the current position {@code byteCount} bytes.
+     */
+    public abstract void readByteArray(byte[] dst, int dstOffset, int byteCount);
+
+    /**
+     * Returns the byte at the current position, and advances the current position one byte.
+     */
+    public abstract byte readByte();
+
+    /**
+     * Returns the 32-bit int at the current position, and advances the current position four bytes.
+     */
+    public abstract int readInt();
+
+    /**
+     * Copies {@code intCount} 32-bit ints from the current position into {@code dst}, starting at
+     * {@code dstOffset}, and advances the current position {@code 4 * intCount} bytes.
+     */
+    public abstract void readIntArray(int[] dst, int dstOffset, int intCount);
+
+    /**
+     * Returns the 16-bit short at the current position, and advances the current position two bytes.
+     */
+    public abstract short readShort();
+}
diff --git a/src/main/java/libcore/io/DiskLruCache.java b/src/main/java/libcore/io/DiskLruCache.java
new file mode 100644
index 0000000..6911834
--- /dev/null
+++ b/src/main/java/libcore/io/DiskLruCache.java
@@ -0,0 +1,834 @@
+/*
+ * Copyright (C) 2011 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 libcore.io;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.EOFException;
+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;
+import java.io.InputStreamReader;
+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 libcore.util.Charsets;
+import libcore.util.Libcore;
+
+/**
+ * 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.
+ *
+ * <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
+ * files from its directory. It is an error for multiple processes to use the
+ * same cache directory at the same time.
+ *
+ * <p>This cache limits the number of bytes that it will store on the
+ * filesystem. When the number of stored bytes exceeds the limit, the cache will
+ * remove entries in the background until the limit is satisfied. The limit is
+ * not strict: the cache may temporarily exceed it while waiting for files to be
+ * deleted. The limit does not include filesystem overhead or the cache
+ * journal so space-sensitive applications should set a conservative limit.
+ *
+ * <p>Clients call {@link #edit} to create or update the values of an entry. An
+ * 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.
+ * </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
+ * of values as they were before or after the commit, but never a mix of values.
+ *
+ * <p>Clients call {@link #get} to read a snapshot of an entry. The read will
+ * observe the value at the time that {@link #get} was called. Updates and
+ * removals after the call do not impact ongoing reads.
+ *
+ * <p>This class is tolerant of some I/O errors. If files are missing from the
+ * filesystem, the corresponding entries will be dropped from the cache. If
+ * an error occurs while writing a cache value, the edit will fail silently.
+ * Callers should handle other problems by catching {@code IOException} and
+ * responding appropriately.
+ */
+public final class DiskLruCache implements Closeable {
+    static final String JOURNAL_FILE = "journal";
+    static final String JOURNAL_FILE_TMP = "journal.tmp";
+    static final String MAGIC = "libcore.io.DiskLruCache";
+    static final String VERSION_1 = "1";
+    static final long ANY_SEQUENCE_NUMBER = -1;
+    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.
+     */
+
+    private final File directory;
+    private final File journalFile;
+    private final File journalFileTmp;
+    private final int appVersion;
+    private final long maxSize;
+    private final int valueCount;
+    private long size = 0;
+    private Writer journalWriter;
+    private final LinkedHashMap<String, Entry> lruEntries
+            = new LinkedHashMap<String, Entry>(0, 0.75f, true);
+    private int redundantOpCount;
+
+    /**
+     * To differentiate between old and current snapshots, each entry is given
+     * a sequence number each time an edit is committed. A snapshot is stale if
+     * its sequence number is not equal to its entry's sequence number.
+     */
+    private long nextSequenceNumber = 0;
+
+    /** This cache uses a single background thread to evict entries. */
+    private final ExecutorService 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 {
+            synchronized (DiskLruCache.this) {
+                if (journalWriter == null) {
+                    return null; // closed
+                }
+                trimToSize();
+                if (journalRebuildRequired()) {
+                    rebuildJournal();
+                    redundantOpCount = 0;
+                }
+            }
+            return null;
+        }
+    };
+
+    private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
+        this.directory = directory;
+        this.appVersion = appVersion;
+        this.journalFile = new File(directory, JOURNAL_FILE);
+        this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
+        this.valueCount = valueCount;
+        this.maxSize = maxSize;
+    }
+
+    /**
+     * Opens the cache in {@code directory}, creating a cache if none exists
+     * there.
+     *
+     * @param directory a writable directory
+     * @param appVersion
+     * @param valueCount the number of values per cache entry. Must be positive.
+     * @param maxSize the maximum number of bytes this cache should use to store
+     * @throws IOException if reading or writing the cache directory fails
+     */
+    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
+            throws IOException {
+        if (maxSize <= 0) {
+            throw new IllegalArgumentException("maxSize <= 0");
+        }
+        if (valueCount <= 0) {
+            throw new IllegalArgumentException("valueCount <= 0");
+        }
+
+        // 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));
+                return cache;
+            } catch (IOException journalIsCorrupt) {
+                Libcore.logW("DiskLruCache " + directory + " is corrupt: "
+                        + journalIsCorrupt.getMessage() + ", removing");
+                cache.delete();
+            }
+        }
+
+        // create a new empty cache
+        directory.mkdirs();
+        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+        cache.rebuildJournal();
+        return cache;
+    }
+
+    private void readJournal() throws IOException {
+        InputStream in = new BufferedInputStream(new FileInputStream(journalFile));
+        try {
+            String magic = Streams.readAsciiLine(in);
+            String version = Streams.readAsciiLine(in);
+            String appVersionString = Streams.readAsciiLine(in);
+            String valueCountString = Streams.readAsciiLine(in);
+            String blank = Streams.readAsciiLine(in);
+            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 + "]");
+            }
+
+            while (true) {
+                try {
+                    readJournalLine(Streams.readAsciiLine(in));
+                } catch (EOFException endOfJournal) {
+                    break;
+                }
+            }
+        } finally {
+            IoUtils.closeQuietly(in);
+        }
+    }
+
+    private void readJournalLine(String line) throws IOException {
+        String[] parts = line.split(" ");
+        if (parts.length < 2) {
+            throw new IOException("unexpected journal line: " + line);
+        }
+
+        String key = parts[1];
+        if (parts[0].equals(REMOVE) && parts.length == 2) {
+            lruEntries.remove(key);
+            return;
+        }
+
+        Entry entry = lruEntries.get(key);
+        if (entry == null) {
+            entry = new Entry(key);
+            lruEntries.put(key, entry);
+        }
+
+        if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
+            entry.readable = true;
+            entry.currentEditor = null;
+            entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length));
+        } else if (parts[0].equals(DIRTY) && parts.length == 2) {
+            entry.currentEditor = new Editor(entry);
+        } else if (parts[0].equals(READ) && parts.length == 2) {
+            // this work was already done by calling lruEntries.get()
+        } else {
+            throw new IOException("unexpected journal line: " + line);
+        }
+    }
+
+    /**
+     * Computes the initial size and collects garbage as a part of opening the
+     * cache. Dirty entries are assumed to be inconsistent and will be deleted.
+     */
+    private void processJournal() throws IOException {
+        deleteIfExists(journalFileTmp);
+        for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext();) {
+            Entry entry = i.next();
+            if (entry.currentEditor == null) {
+                for (int t = 0; t < valueCount; t++) {
+                    size += entry.lengths[t];
+                }
+            } else {
+                entry.currentEditor = null;
+                for (int t = 0; t < valueCount; t++) {
+                    deleteIfExists(entry.getCleanFile(t));
+                    deleteIfExists(entry.getDirtyFile(t));
+                }
+                i.remove();
+            }
+        }
+    }
+
+    /**
+     * Creates a new journal that omits redundant information. This replaces the
+     * current journal if it exists.
+     */
+    private synchronized void rebuildJournal() throws IOException {
+        if (journalWriter != null) {
+            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.close();
+        journalFileTmp.renameTo(journalFile);
+        journalWriter = new BufferedWriter(new FileWriter(journalFile, true));
+    }
+
+    private static void deleteIfExists(File file) throws IOException {
+        Libcore.deleteIfExists(file);
+    }
+
+    /**
+     * Returns a snapshot of the entry named {@code key}, or null if it doesn't
+     * exist is not currently readable. If a value is returned, it is moved to
+     * the head of the LRU queue.
+     */
+    public synchronized Snapshot get(String key) throws IOException {
+        checkNotClosed();
+        validateKey(key);
+        Entry entry = lruEntries.get(key);
+        if (entry == null) {
+            return null;
+        }
+
+        if (!entry.readable) {
+            return null;
+        }
+
+        /*
+         * Open all streams eagerly to guarantee that we see a single published
+         * snapshot. If we opened streams lazily then the streams could come
+         * from different edits.
+         */
+        InputStream[] ins = new InputStream[valueCount];
+        try {
+            for (int i = 0; i < valueCount; i++) {
+                ins[i] = new FileInputStream(entry.getCleanFile(i));
+            }
+        } catch (FileNotFoundException e) {
+            // a file must have been deleted manually!
+            return null;
+        }
+
+        redundantOpCount++;
+        journalWriter.append(READ + ' ' + key + '\n');
+        if (journalRebuildRequired()) {
+            executorService.submit(cleanupCallable);
+        }
+
+        return new Snapshot(key, entry.sequenceNumber, ins);
+    }
+
+    /**
+     * Returns an editor for the entry named {@code key}, or null if another
+     * edit is in progress.
+     */
+    public Editor edit(String key) throws IOException {
+        return edit(key, ANY_SEQUENCE_NUMBER);
+    }
+
+    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
+        checkNotClosed();
+        validateKey(key);
+        Entry entry = lruEntries.get(key);
+        if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
+                && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
+            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
+        }
+
+        Editor editor = new Editor(entry);
+        entry.currentEditor = editor;
+
+        // flush the journal before creating files to prevent file leaks
+        journalWriter.write(DIRTY + ' ' + key + '\n');
+        journalWriter.flush();
+        return editor;
+    }
+
+    /**
+     * Returns the directory where this cache stores its data.
+     */
+    public File getDirectory() {
+        return directory;
+    }
+
+    /**
+     * Returns the maximum number of bytes that this cache should use to store
+     * its data.
+     */
+    public long maxSize() {
+        return maxSize;
+    }
+
+    /**
+     * 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.
+     */
+    public synchronized long size() {
+        return size;
+    }
+
+    private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
+        Entry entry = editor.entry;
+        if (entry.currentEditor != editor) {
+            throw new IllegalStateException();
+        }
+
+        // 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 (!entry.getDirtyFile(i).exists()) {
+                    editor.abort();
+                    throw new IllegalStateException("edit didn't create file " + i);
+                }
+            }
+        }
+
+        for (int i = 0; i < valueCount; i++) {
+            File dirty = entry.getDirtyFile(i);
+            if (success) {
+                if (dirty.exists()) {
+                    File clean = entry.getCleanFile(i);
+                    dirty.renameTo(clean);
+                    long oldLength = entry.lengths[i];
+                    long newLength = clean.length();
+                    entry.lengths[i] = newLength;
+                    size = size - oldLength + newLength;
+                }
+            } else {
+                deleteIfExists(dirty);
+            }
+        }
+
+        redundantOpCount++;
+        entry.currentEditor = null;
+        if (entry.readable | success) {
+            entry.readable = true;
+            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+            if (success) {
+                entry.sequenceNumber = nextSequenceNumber++;
+            }
+        } else {
+            lruEntries.remove(entry.key);
+            journalWriter.write(REMOVE + ' ' + entry.key + '\n');
+        }
+
+        if (size > maxSize || journalRebuildRequired()) {
+            executorService.submit(cleanupCallable);
+        }
+    }
+
+    /**
+     * We only rebuild the journal when it will halve the size of the journal
+     * and eliminate at least 2000 ops.
+     */
+    private boolean journalRebuildRequired() {
+        final int redundantOpCompactThreshold = 2000;
+        return redundantOpCount >= redundantOpCompactThreshold
+                && redundantOpCount >= lruEntries.size();
+    }
+
+    /**
+     * Drops the entry for {@code key} if it exists and can be removed. Entries
+     * actively being edited cannot be removed.
+     *
+     * @return true if an entry was removed.
+     */
+    public synchronized boolean remove(String key) throws IOException {
+        checkNotClosed();
+        validateKey(key);
+        Entry entry = lruEntries.get(key);
+        if (entry == null || entry.currentEditor != null) {
+            return false;
+        }
+
+        for (int i = 0; i < valueCount; i++) {
+            File file = entry.getCleanFile(i);
+            if (!file.delete()) {
+                throw new IOException("failed to delete " + file);
+            }
+            size -= entry.lengths[i];
+            entry.lengths[i] = 0;
+        }
+
+        redundantOpCount++;
+        journalWriter.append(REMOVE + ' ' + key + '\n');
+        lruEntries.remove(key);
+
+        if (journalRebuildRequired()) {
+            executorService.submit(cleanupCallable);
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns true if this cache has been closed.
+     */
+    public boolean isClosed() {
+        return journalWriter == null;
+    }
+
+    private void checkNotClosed() {
+        if (journalWriter == null) {
+            throw new IllegalStateException("cache is closed");
+        }
+    }
+
+    /**
+     * Force buffered operations to the filesystem.
+     */
+    public synchronized void flush() throws IOException {
+        checkNotClosed();
+        trimToSize();
+        journalWriter.flush();
+    }
+
+    /**
+     * Closes this cache. Stored values will remain on the filesystem.
+     */
+    public synchronized void close() throws IOException {
+        if (journalWriter == null) {
+            return; // already closed
+        }
+        for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
+            if (entry.currentEditor != null) {
+                entry.currentEditor.abort();
+            }
+        }
+        trimToSize();
+        journalWriter.close();
+        journalWriter = null;
+    }
+
+    private void trimToSize() throws IOException {
+        while (size > maxSize) {
+            Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
+            remove(toEvict.getKey());
+        }
+    }
+
+    /**
+     * 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 {
+        close();
+        IoUtils.deleteContents(directory);
+    }
+
+    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 + "\"");
+        }
+    }
+
+    private static String inputStreamToString(InputStream in) throws IOException {
+        return Streams.readFully(new InputStreamReader(in, Charsets.UTF_8));
+    }
+
+    /**
+     * A snapshot of the values for an entry.
+     */
+    public final class Snapshot implements Closeable {
+        private final String key;
+        private final long sequenceNumber;
+        private final InputStream[] ins;
+
+        private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
+            this.key = key;
+            this.sequenceNumber = sequenceNumber;
+            this.ins = ins;
+        }
+
+        /**
+         * Returns an editor for this snapshot's entry, or null if either the
+         * entry has changed since this snapshot was created or if another edit
+         * is in progress.
+         */
+        public Editor edit() throws IOException {
+            return DiskLruCache.this.edit(key, sequenceNumber);
+        }
+
+        /**
+         * Returns the unbuffered stream with the value for {@code index}.
+         */
+        public InputStream getInputStream(int index) {
+            return ins[index];
+        }
+
+        /**
+         * Returns the string value for {@code index}.
+         */
+        public String getString(int index) throws IOException {
+            return inputStreamToString(getInputStream(index));
+        }
+
+        @Override public void close() {
+            for (InputStream in : ins) {
+                IoUtils.closeQuietly(in);
+            }
+        }
+    }
+
+    /**
+     * Edits the values for an entry.
+     */
+    public final class Editor {
+        private final Entry entry;
+        private boolean hasErrors;
+
+        private Editor(Entry entry) {
+            this.entry = entry;
+        }
+
+        /**
+         * Returns an unbuffered input stream to read the last committed value,
+         * or null if no value has been committed.
+         */
+        public InputStream newInputStream(int index) throws IOException {
+            synchronized (DiskLruCache.this) {
+                if (entry.currentEditor != this) {
+                    throw new IllegalStateException();
+                }
+                if (!entry.readable) {
+                    return null;
+                }
+                return new FileInputStream(entry.getCleanFile(index));
+            }
+        }
+
+        /**
+         * Returns the last committed value as a string, or null if no value
+         * has been committed.
+         */
+        public String getString(int index) throws IOException {
+            InputStream in = newInputStream(index);
+            return in != null ? inputStreamToString(in) : null;
+        }
+
+        /**
+         * Returns a new unbuffered output stream to write the value at
+         * {@code index}. If the underlying output stream encounters errors
+         * when writing to the filesystem, this edit will be aborted when
+         * {@link #commit} is called. The returned output stream does not throw
+         * IOExceptions.
+         */
+        public OutputStream newOutputStream(int index) throws IOException {
+            synchronized (DiskLruCache.this) {
+                if (entry.currentEditor != this) {
+                    throw new IllegalStateException();
+                }
+                return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
+            }
+        }
+
+        /**
+         * Sets the value at {@code index} to {@code value}.
+         */
+        public void set(int index, String value) throws IOException {
+            Writer writer = null;
+            try {
+                writer = new OutputStreamWriter(newOutputStream(index), Charsets.UTF_8);
+                writer.write(value);
+            } finally {
+                IoUtils.closeQuietly(writer);
+            }
+        }
+
+        /**
+         * Commits this edit so it is visible to readers.  This releases the
+         * edit lock so another edit may be started on the same key.
+         */
+        public void commit() throws IOException {
+            if (hasErrors) {
+                completeEdit(this, false);
+                remove(entry.key); // the previous entry is stale
+            } else {
+                completeEdit(this, true);
+            }
+        }
+
+        /**
+         * Aborts this edit. This releases the edit lock so another edit may be
+         * started on the same key.
+         */
+        public void abort() throws IOException {
+            completeEdit(this, false);
+        }
+
+        private final class FaultHidingOutputStream extends FilterOutputStream {
+            private FaultHidingOutputStream(OutputStream out) {
+                super(out);
+            }
+
+            @Override public void write(int oneByte) {
+                try {
+                    out.write(oneByte);
+                } catch (IOException e) {
+                    hasErrors = true;
+                }
+            }
+
+            @Override public void write(byte[] buffer, int offset, int length) {
+                try {
+                    out.write(buffer, offset, length);
+                } catch (IOException e) {
+                    hasErrors = true;
+                }
+            }
+
+            @Override public void close() {
+                try {
+                    out.close();
+                } catch (IOException e) {
+                    hasErrors = true;
+                }
+            }
+
+            @Override public void flush() {
+                try {
+                    out.flush();
+                } catch (IOException e) {
+                    hasErrors = true;
+                }
+            }
+        }
+    }
+
+    private final class Entry {
+        private final String key;
+
+        /** Lengths of this entry's files. */
+        private final long[] lengths;
+
+        /** True if this entry has ever been published. */
+        private boolean readable;
+
+        /** The ongoing edit or null if this entry is not being edited. */
+        private Editor currentEditor;
+
+        /** The sequence number of the most recently committed edit to this entry. */
+        private long sequenceNumber;
+
+        private Entry(String key) {
+            this.key = key;
+            this.lengths = new long[valueCount];
+        }
+
+        public String getLengths() throws IOException {
+            StringBuilder result = new StringBuilder();
+            for (long size : lengths) {
+                result.append(' ').append(size);
+            }
+            return result.toString();
+        }
+
+        /**
+         * Set lengths using decimal numbers like "10123".
+         */
+        private void setLengths(String[] strings) throws IOException {
+            if (strings.length != valueCount) {
+                throw invalidLengths(strings);
+            }
+
+            try {
+                for (int i = 0; i < strings.length; i++) {
+                    lengths[i] = Long.parseLong(strings[i]);
+                }
+            } catch (NumberFormatException e) {
+                throw invalidLengths(strings);
+            }
+        }
+
+        private IOException invalidLengths(String[] strings) throws IOException {
+            throw new IOException("unexpected journal line: " + Arrays.toString(strings));
+        }
+
+        public File getCleanFile(int i) {
+            return new File(directory, key + "." + i);
+        }
+
+        public File getDirtyFile(int i) {
+            return new File(directory, key + "." + i + ".tmp");
+        }
+    }
+}
diff --git a/src/main/java/libcore/io/IoUtils.java b/src/main/java/libcore/io/IoUtils.java
new file mode 100644
index 0000000..307737d
--- /dev/null
+++ b/src/main/java/libcore/io/IoUtils.java
@@ -0,0 +1,72 @@
+/*
+ * 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 libcore.io;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.net.Socket;
+
+public final class IoUtils {
+    private IoUtils() {
+    }
+
+    /**
+     * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
+     */
+    public static void closeQuietly(Closeable closeable) {
+        if (closeable != null) {
+            try {
+                closeable.close();
+            } catch (RuntimeException rethrown) {
+                throw rethrown;
+            } catch (Exception ignored) {
+            }
+        }
+    }
+
+    /**
+     * Closes 'socket', ignoring any exceptions. Does nothing if 'socket' is null.
+     */
+    public static void closeQuietly(Socket socket) {
+        if (socket != null) {
+            try {
+                socket.close();
+            } catch (Exception ignored) {
+            }
+        }
+    }
+
+    /**
+     * Recursively delete everything in {@code dir}.
+     */
+    // TODO: this should specify paths as Strings rather than as Files
+    public static void deleteContents(File dir) throws IOException {
+        File[] files = dir.listFiles();
+        if (files == null) {
+            throw new IllegalArgumentException("not a directory: " + dir);
+        }
+        for (File file : files) {
+            if (file.isDirectory()) {
+                deleteContents(file);
+            }
+            if (!file.delete()) {
+                throw new IOException("failed to delete file: " + file);
+            }
+        }
+    }
+}
diff --git a/src/main/java/libcore/io/OsConstants.java b/src/main/java/libcore/io/OsConstants.java
new file mode 100644
index 0000000..68a165c
--- /dev/null
+++ b/src/main/java/libcore/io/OsConstants.java
@@ -0,0 +1,724 @@
+/*
+ * Copyright (C) 2011 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 libcore.io;
+
+public final class OsConstants {
+    private OsConstants() { }
+
+    public static boolean S_ISBLK(int mode) { return (mode & S_IFMT) == S_IFBLK; }
+    public static boolean S_ISCHR(int mode) { return (mode & S_IFMT) == S_IFCHR; }
+    public static boolean S_ISDIR(int mode) { return (mode & S_IFMT) == S_IFDIR; }
+    public static boolean S_ISFIFO(int mode) { return (mode & S_IFMT) == S_IFIFO; }
+    public static boolean S_ISREG(int mode) { return (mode & S_IFMT) == S_IFREG; }
+    public static boolean S_ISLNK(int mode) { return (mode & S_IFMT) == S_IFLNK; }
+    public static boolean S_ISSOCK(int mode) { return (mode & S_IFMT) == S_IFSOCK; }
+
+    public static int WEXITSTATUS(int status) { return (status & 0xff00) >> 8; }
+    public static boolean WCOREDUMP(int status) { return (status & 0x80) != 0; }
+    public static int WTERMSIG(int status) { return status & 0x7f; }
+    public static int WSTOPSIG(int status) { return WEXITSTATUS(status); }
+    public static boolean WIFEXITED(int status) { return (WTERMSIG(status) == 0); }
+    public static boolean WIFSTOPPED(int status) { return (WTERMSIG(status) == 0x7f); }
+    public static boolean WIFSIGNALED(int status) { return (WTERMSIG(status + 1) >= 2); }
+
+    public static final int AF_INET = placeholder();
+    public static final int AF_INET6 = placeholder();
+    public static final int AF_UNIX = placeholder();
+    public static final int AF_UNSPEC = placeholder();
+    public static final int AI_ADDRCONFIG = placeholder();
+    public static final int AI_ALL = placeholder();
+    public static final int AI_CANONNAME = placeholder();
+    public static final int AI_NUMERICHOST = placeholder();
+    public static final int AI_NUMERICSERV = placeholder();
+    public static final int AI_PASSIVE = placeholder();
+    public static final int AI_V4MAPPED = placeholder();
+    public static final int E2BIG = placeholder();
+    public static final int EACCES = placeholder();
+    public static final int EADDRINUSE = placeholder();
+    public static final int EADDRNOTAVAIL = placeholder();
+    public static final int EAFNOSUPPORT = placeholder();
+    public static final int EAGAIN = placeholder();
+    public static final int EAI_AGAIN = placeholder();
+    public static final int EAI_BADFLAGS = placeholder();
+    public static final int EAI_FAIL = placeholder();
+    public static final int EAI_FAMILY = placeholder();
+    public static final int EAI_MEMORY = placeholder();
+    public static final int EAI_NODATA = placeholder();
+    public static final int EAI_NONAME = placeholder();
+    public static final int EAI_OVERFLOW = placeholder();
+    public static final int EAI_SERVICE = placeholder();
+    public static final int EAI_SOCKTYPE = placeholder();
+    public static final int EAI_SYSTEM = placeholder();
+    public static final int EALREADY = placeholder();
+    public static final int EBADF = placeholder();
+    public static final int EBADMSG = placeholder();
+    public static final int EBUSY = placeholder();
+    public static final int ECANCELED = placeholder();
+    public static final int ECHILD = placeholder();
+    public static final int ECONNABORTED = placeholder();
+    public static final int ECONNREFUSED = placeholder();
+    public static final int ECONNRESET = placeholder();
+    public static final int EDEADLK = placeholder();
+    public static final int EDESTADDRREQ = placeholder();
+    public static final int EDOM = placeholder();
+    public static final int EDQUOT = placeholder();
+    public static final int EEXIST = placeholder();
+    public static final int EFAULT = placeholder();
+    public static final int EFBIG = placeholder();
+    public static final int EHOSTUNREACH = placeholder();
+    public static final int EIDRM = placeholder();
+    public static final int EILSEQ = placeholder();
+    public static final int EINPROGRESS = placeholder();
+    public static final int EINTR = placeholder();
+    public static final int EINVAL = placeholder();
+    public static final int EIO = placeholder();
+    public static final int EISCONN = placeholder();
+    public static final int EISDIR = placeholder();
+    public static final int ELOOP = placeholder();
+    public static final int EMFILE = placeholder();
+    public static final int EMLINK = placeholder();
+    public static final int EMSGSIZE = placeholder();
+    public static final int EMULTIHOP = placeholder();
+    public static final int ENAMETOOLONG = placeholder();
+    public static final int ENETDOWN = placeholder();
+    public static final int ENETRESET = placeholder();
+    public static final int ENETUNREACH = placeholder();
+    public static final int ENFILE = placeholder();
+    public static final int ENOBUFS = placeholder();
+    public static final int ENODATA = placeholder();
+    public static final int ENODEV = placeholder();
+    public static final int ENOENT = placeholder();
+    public static final int ENOEXEC = placeholder();
+    public static final int ENOLCK = placeholder();
+    public static final int ENOLINK = placeholder();
+    public static final int ENOMEM = placeholder();
+    public static final int ENOMSG = placeholder();
+    public static final int ENOPROTOOPT = placeholder();
+    public static final int ENOSPC = placeholder();
+    public static final int ENOSR = placeholder();
+    public static final int ENOSTR = placeholder();
+    public static final int ENOSYS = placeholder();
+    public static final int ENOTCONN = placeholder();
+    public static final int ENOTDIR = placeholder();
+    public static final int ENOTEMPTY = placeholder();
+    public static final int ENOTSOCK = placeholder();
+    public static final int ENOTSUP = placeholder();
+    public static final int ENOTTY = placeholder();
+    public static final int ENXIO = placeholder();
+    public static final int EOPNOTSUPP = placeholder();
+    public static final int EOVERFLOW = placeholder();
+    public static final int EPERM = placeholder();
+    public static final int EPIPE = placeholder();
+    public static final int EPROTO = placeholder();
+    public static final int EPROTONOSUPPORT = placeholder();
+    public static final int EPROTOTYPE = placeholder();
+    public static final int ERANGE = placeholder();
+    public static final int EROFS = placeholder();
+    public static final int ESPIPE = placeholder();
+    public static final int ESRCH = placeholder();
+    public static final int ESTALE = placeholder();
+    public static final int ETIME = placeholder();
+    public static final int ETIMEDOUT = placeholder();
+    public static final int ETXTBSY = placeholder();
+    public static final int EWOULDBLOCK = placeholder();
+    public static final int EXDEV = placeholder();
+    public static final int EXIT_FAILURE = placeholder();
+    public static final int EXIT_SUCCESS = placeholder();
+    public static final int FD_CLOEXEC = placeholder();
+    public static final int FIONREAD = placeholder();
+    public static final int F_DUPFD = placeholder();
+    public static final int F_GETFD = placeholder();
+    public static final int F_GETFL = placeholder();
+    public static final int F_GETLK = placeholder();
+    public static final int F_GETLK64 = placeholder();
+    public static final int F_GETOWN = placeholder();
+    public static final int F_OK = placeholder();
+    public static final int F_RDLCK = placeholder();
+    public static final int F_SETFD = placeholder();
+    public static final int F_SETFL = placeholder();
+    public static final int F_SETLK = placeholder();
+    public static final int F_SETLK64 = placeholder();
+    public static final int F_SETLKW = placeholder();
+    public static final int F_SETLKW64 = placeholder();
+    public static final int F_SETOWN = placeholder();
+    public static final int F_UNLCK = placeholder();
+    public static final int F_WRLCK = placeholder();
+    public static final int IFF_ALLMULTI = placeholder();
+    public static final int IFF_AUTOMEDIA = placeholder();
+    public static final int IFF_BROADCAST = placeholder();
+    public static final int IFF_DEBUG = placeholder();
+    public static final int IFF_DYNAMIC = placeholder();
+    public static final int IFF_LOOPBACK = placeholder();
+    public static final int IFF_MASTER = placeholder();
+    public static final int IFF_MULTICAST = placeholder();
+    public static final int IFF_NOARP = placeholder();
+    public static final int IFF_NOTRAILERS = placeholder();
+    public static final int IFF_POINTOPOINT = placeholder();
+    public static final int IFF_PORTSEL = placeholder();
+    public static final int IFF_PROMISC = placeholder();
+    public static final int IFF_RUNNING = placeholder();
+    public static final int IFF_SLAVE = placeholder();
+    public static final int IFF_UP = placeholder();
+    public static final int IPPROTO_ICMP = placeholder();
+    public static final int IPPROTO_IP = placeholder();
+    public static final int IPPROTO_IPV6 = placeholder();
+    public static final int IPPROTO_RAW = placeholder();
+    public static final int IPPROTO_TCP = placeholder();
+    public static final int IPPROTO_UDP = placeholder();
+    public static final int IPV6_CHECKSUM = placeholder();
+    public static final int IPV6_MULTICAST_HOPS = placeholder();
+    public static final int IPV6_MULTICAST_IF = placeholder();
+    public static final int IPV6_MULTICAST_LOOP = placeholder();
+    public static final int IPV6_RECVDSTOPTS = placeholder();
+    public static final int IPV6_RECVHOPLIMIT = placeholder();
+    public static final int IPV6_RECVHOPOPTS = placeholder();
+    public static final int IPV6_RECVPKTINFO = placeholder();
+    public static final int IPV6_RECVRTHDR = placeholder();
+    public static final int IPV6_RECVTCLASS = placeholder();
+    public static final int IPV6_TCLASS = placeholder();
+    public static final int IPV6_UNICAST_HOPS = placeholder();
+    public static final int IPV6_V6ONLY = placeholder();
+    public static final int IP_MULTICAST_IF = placeholder();
+    public static final int IP_MULTICAST_LOOP = placeholder();
+    public static final int IP_MULTICAST_TTL = placeholder();
+    public static final int IP_TOS = placeholder();
+    public static final int IP_TTL = placeholder();
+    public static final int MAP_FIXED = placeholder();
+    public static final int MAP_PRIVATE = placeholder();
+    public static final int MAP_SHARED = placeholder();
+    public static final int MCAST_JOIN_GROUP = placeholder();
+    public static final int MCAST_LEAVE_GROUP = placeholder();
+    public static final int MCL_CURRENT = placeholder();
+    public static final int MCL_FUTURE = placeholder();
+    public static final int MSG_CTRUNC = placeholder();
+    public static final int MSG_DONTROUTE = placeholder();
+    public static final int MSG_EOR = placeholder();
+    public static final int MSG_OOB = placeholder();
+    public static final int MSG_PEEK = placeholder();
+    public static final int MSG_TRUNC = placeholder();
+    public static final int MSG_WAITALL = placeholder();
+    public static final int MS_ASYNC = placeholder();
+    public static final int MS_INVALIDATE = placeholder();
+    public static final int MS_SYNC = placeholder();
+    public static final int NI_DGRAM = placeholder();
+    public static final int NI_NAMEREQD = placeholder();
+    public static final int NI_NOFQDN = placeholder();
+    public static final int NI_NUMERICHOST = placeholder();
+    public static final int NI_NUMERICSERV = placeholder();
+    public static final int O_ACCMODE = placeholder();
+    public static final int O_APPEND = placeholder();
+    public static final int O_CREAT = placeholder();
+    public static final int O_EXCL = placeholder();
+    public static final int O_NOCTTY = placeholder();
+    public static final int O_NONBLOCK = placeholder();
+    public static final int O_RDONLY = placeholder();
+    public static final int O_RDWR = placeholder();
+    public static final int O_SYNC = placeholder();
+    public static final int O_TRUNC = placeholder();
+    public static final int O_WRONLY = placeholder();
+    public static final int POLLERR = placeholder();
+    public static final int POLLHUP = placeholder();
+    public static final int POLLIN = placeholder();
+    public static final int POLLNVAL = placeholder();
+    public static final int POLLOUT = placeholder();
+    public static final int POLLPRI = placeholder();
+    public static final int POLLRDBAND = placeholder();
+    public static final int POLLRDNORM = placeholder();
+    public static final int POLLWRBAND = placeholder();
+    public static final int POLLWRNORM = placeholder();
+    public static final int PROT_EXEC = placeholder();
+    public static final int PROT_NONE = placeholder();
+    public static final int PROT_READ = placeholder();
+    public static final int PROT_WRITE = placeholder();
+    public static final int R_OK = placeholder();
+    public static final int SEEK_CUR = placeholder();
+    public static final int SEEK_END = placeholder();
+    public static final int SEEK_SET = placeholder();
+    public static final int SHUT_RD = placeholder();
+    public static final int SHUT_RDWR = placeholder();
+    public static final int SHUT_WR = placeholder();
+    public static final int SIGABRT = placeholder();
+    public static final int SIGALRM = placeholder();
+    public static final int SIGBUS = placeholder();
+    public static final int SIGCHLD = placeholder();
+    public static final int SIGCONT = placeholder();
+    public static final int SIGFPE = placeholder();
+    public static final int SIGHUP = placeholder();
+    public static final int SIGILL = placeholder();
+    public static final int SIGINT = placeholder();
+    public static final int SIGIO = placeholder();
+    public static final int SIGKILL = placeholder();
+    public static final int SIGPIPE = placeholder();
+    public static final int SIGPROF = placeholder();
+    public static final int SIGPWR = placeholder();
+    public static final int SIGQUIT = placeholder();
+    public static final int SIGRTMAX = placeholder();
+    public static final int SIGRTMIN = placeholder();
+    public static final int SIGSEGV = placeholder();
+    public static final int SIGSTKFLT = placeholder();
+    public static final int SIGSTOP = placeholder();
+    public static final int SIGSYS = placeholder();
+    public static final int SIGTERM = placeholder();
+    public static final int SIGTRAP = placeholder();
+    public static final int SIGTSTP = placeholder();
+    public static final int SIGTTIN = placeholder();
+    public static final int SIGTTOU = placeholder();
+    public static final int SIGURG = placeholder();
+    public static final int SIGUSR1 = placeholder();
+    public static final int SIGUSR2 = placeholder();
+    public static final int SIGVTALRM = placeholder();
+    public static final int SIGWINCH = placeholder();
+    public static final int SIGXCPU = placeholder();
+    public static final int SIGXFSZ = placeholder();
+    public static final int SIOCGIFADDR = placeholder();
+    public static final int SIOCGIFBRDADDR = placeholder();
+    public static final int SIOCGIFDSTADDR = placeholder();
+    public static final int SIOCGIFNETMASK = placeholder();
+    public static final int SOCK_DGRAM = placeholder();
+    public static final int SOCK_RAW = placeholder();
+    public static final int SOCK_SEQPACKET = placeholder();
+    public static final int SOCK_STREAM = placeholder();
+    public static final int SOL_SOCKET = placeholder();
+    public static final int SO_BINDTODEVICE = placeholder();
+    public static final int SO_BROADCAST = placeholder();
+    public static final int SO_DEBUG = placeholder();
+    public static final int SO_DONTROUTE = placeholder();
+    public static final int SO_ERROR = placeholder();
+    public static final int SO_KEEPALIVE = placeholder();
+    public static final int SO_LINGER = placeholder();
+    public static final int SO_OOBINLINE = placeholder();
+    public static final int SO_RCVBUF = placeholder();
+    public static final int SO_RCVLOWAT = placeholder();
+    public static final int SO_RCVTIMEO = placeholder();
+    public static final int SO_REUSEADDR = placeholder();
+    public static final int SO_SNDBUF = placeholder();
+    public static final int SO_SNDLOWAT = placeholder();
+    public static final int SO_SNDTIMEO = placeholder();
+    public static final int SO_TYPE = placeholder();
+    public static final int STDERR_FILENO = placeholder();
+    public static final int STDIN_FILENO = placeholder();
+    public static final int STDOUT_FILENO = placeholder();
+    public static final int S_IFBLK = placeholder();
+    public static final int S_IFCHR = placeholder();
+    public static final int S_IFDIR = placeholder();
+    public static final int S_IFIFO = placeholder();
+    public static final int S_IFLNK = placeholder();
+    public static final int S_IFMT = placeholder();
+    public static final int S_IFREG = placeholder();
+    public static final int S_IFSOCK = placeholder();
+    public static final int S_IRGRP = placeholder();
+    public static final int S_IROTH = placeholder();
+    public static final int S_IRUSR = placeholder();
+    public static final int S_IRWXG = placeholder();
+    public static final int S_IRWXO = placeholder();
+    public static final int S_IRWXU = placeholder();
+    public static final int S_ISGID = placeholder();
+    public static final int S_ISUID = placeholder();
+    public static final int S_ISVTX = placeholder();
+    public static final int S_IWGRP = placeholder();
+    public static final int S_IWOTH = placeholder();
+    public static final int S_IWUSR = placeholder();
+    public static final int S_IXGRP = placeholder();
+    public static final int S_IXOTH = placeholder();
+    public static final int S_IXUSR = placeholder();
+    public static final int TCP_NODELAY = placeholder();
+    public static final int WCONTINUED = placeholder();
+    public static final int WEXITED = placeholder();
+    public static final int WNOHANG = placeholder();
+    public static final int WNOWAIT = placeholder();
+    public static final int WSTOPPED = placeholder();
+    public static final int WUNTRACED = placeholder();
+    public static final int W_OK = placeholder();
+    public static final int X_OK = placeholder();
+    public static final int _SC_2_CHAR_TERM = placeholder();
+    public static final int _SC_2_C_BIND = placeholder();
+    public static final int _SC_2_C_DEV = placeholder();
+    public static final int _SC_2_C_VERSION = placeholder();
+    public static final int _SC_2_FORT_DEV = placeholder();
+    public static final int _SC_2_FORT_RUN = placeholder();
+    public static final int _SC_2_LOCALEDEF = placeholder();
+    public static final int _SC_2_SW_DEV = placeholder();
+    public static final int _SC_2_UPE = placeholder();
+    public static final int _SC_2_VERSION = placeholder();
+    public static final int _SC_AIO_LISTIO_MAX = placeholder();
+    public static final int _SC_AIO_MAX = placeholder();
+    public static final int _SC_AIO_PRIO_DELTA_MAX = placeholder();
+    public static final int _SC_ARG_MAX = placeholder();
+    public static final int _SC_ASYNCHRONOUS_IO = placeholder();
+    public static final int _SC_ATEXIT_MAX = placeholder();
+    public static final int _SC_AVPHYS_PAGES = placeholder();
+    public static final int _SC_BC_BASE_MAX = placeholder();
+    public static final int _SC_BC_DIM_MAX = placeholder();
+    public static final int _SC_BC_SCALE_MAX = placeholder();
+    public static final int _SC_BC_STRING_MAX = placeholder();
+    public static final int _SC_CHILD_MAX = placeholder();
+    public static final int _SC_CLK_TCK = placeholder();
+    public static final int _SC_COLL_WEIGHTS_MAX = placeholder();
+    public static final int _SC_DELAYTIMER_MAX = placeholder();
+    public static final int _SC_EXPR_NEST_MAX = placeholder();
+    public static final int _SC_FSYNC = placeholder();
+    public static final int _SC_GETGR_R_SIZE_MAX = placeholder();
+    public static final int _SC_GETPW_R_SIZE_MAX = placeholder();
+    public static final int _SC_IOV_MAX = placeholder();
+    public static final int _SC_JOB_CONTROL = placeholder();
+    public static final int _SC_LINE_MAX = placeholder();
+    public static final int _SC_LOGIN_NAME_MAX = placeholder();
+    public static final int _SC_MAPPED_FILES = placeholder();
+    public static final int _SC_MEMLOCK = placeholder();
+    public static final int _SC_MEMLOCK_RANGE = placeholder();
+    public static final int _SC_MEMORY_PROTECTION = placeholder();
+    public static final int _SC_MESSAGE_PASSING = placeholder();
+    public static final int _SC_MQ_OPEN_MAX = placeholder();
+    public static final int _SC_MQ_PRIO_MAX = placeholder();
+    public static final int _SC_NGROUPS_MAX = placeholder();
+    public static final int _SC_NPROCESSORS_CONF = placeholder();
+    public static final int _SC_NPROCESSORS_ONLN = placeholder();
+    public static final int _SC_OPEN_MAX = placeholder();
+    public static final int _SC_PAGESIZE = placeholder();
+    public static final int _SC_PAGE_SIZE = placeholder();
+    public static final int _SC_PASS_MAX = placeholder();
+    public static final int _SC_PHYS_PAGES = placeholder();
+    public static final int _SC_PRIORITIZED_IO = placeholder();
+    public static final int _SC_PRIORITY_SCHEDULING = placeholder();
+    public static final int _SC_REALTIME_SIGNALS = placeholder();
+    public static final int _SC_RE_DUP_MAX = placeholder();
+    public static final int _SC_RTSIG_MAX = placeholder();
+    public static final int _SC_SAVED_IDS = placeholder();
+    public static final int _SC_SEMAPHORES = placeholder();
+    public static final int _SC_SEM_NSEMS_MAX = placeholder();
+    public static final int _SC_SEM_VALUE_MAX = placeholder();
+    public static final int _SC_SHARED_MEMORY_OBJECTS = placeholder();
+    public static final int _SC_SIGQUEUE_MAX = placeholder();
+    public static final int _SC_STREAM_MAX = placeholder();
+    public static final int _SC_SYNCHRONIZED_IO = placeholder();
+    public static final int _SC_THREADS = placeholder();
+    public static final int _SC_THREAD_ATTR_STACKADDR = placeholder();
+    public static final int _SC_THREAD_ATTR_STACKSIZE = placeholder();
+    public static final int _SC_THREAD_DESTRUCTOR_ITERATIONS = placeholder();
+    public static final int _SC_THREAD_KEYS_MAX = placeholder();
+    public static final int _SC_THREAD_PRIORITY_SCHEDULING = placeholder();
+    public static final int _SC_THREAD_PRIO_INHERIT = placeholder();
+    public static final int _SC_THREAD_PRIO_PROTECT = placeholder();
+    public static final int _SC_THREAD_SAFE_FUNCTIONS = placeholder();
+    public static final int _SC_THREAD_STACK_MIN = placeholder();
+    public static final int _SC_THREAD_THREADS_MAX = placeholder();
+    public static final int _SC_TIMERS = placeholder();
+    public static final int _SC_TIMER_MAX = placeholder();
+    public static final int _SC_TTY_NAME_MAX = placeholder();
+    public static final int _SC_TZNAME_MAX = placeholder();
+    public static final int _SC_VERSION = placeholder();
+    public static final int _SC_XBS5_ILP32_OFF32 = placeholder();
+    public static final int _SC_XBS5_ILP32_OFFBIG = placeholder();
+    public static final int _SC_XBS5_LP64_OFF64 = placeholder();
+    public static final int _SC_XBS5_LPBIG_OFFBIG = placeholder();
+    public static final int _SC_XOPEN_CRYPT = placeholder();
+    public static final int _SC_XOPEN_ENH_I18N = placeholder();
+    public static final int _SC_XOPEN_LEGACY = placeholder();
+    public static final int _SC_XOPEN_REALTIME = placeholder();
+    public static final int _SC_XOPEN_REALTIME_THREADS = placeholder();
+    public static final int _SC_XOPEN_SHM = placeholder();
+    public static final int _SC_XOPEN_UNIX = placeholder();
+    public static final int _SC_XOPEN_VERSION = placeholder();
+    public static final int _SC_XOPEN_XCU_VERSION = placeholder();
+
+    public static String gaiName(int error) {
+        if (error == EAI_AGAIN) {
+            return "EAI_AGAIN";
+        }
+        if (error == EAI_BADFLAGS) {
+            return "EAI_BADFLAGS";
+        }
+        if (error == EAI_FAIL) {
+            return "EAI_FAIL";
+        }
+        if (error == EAI_FAMILY) {
+            return "EAI_FAMILY";
+        }
+        if (error == EAI_MEMORY) {
+            return "EAI_MEMORY";
+        }
+        if (error == EAI_NODATA) {
+            return "EAI_NODATA";
+        }
+        if (error == EAI_NONAME) {
+            return "EAI_NONAME";
+        }
+        if (error == EAI_OVERFLOW) {
+            return "EAI_OVERFLOW";
+        }
+        if (error == EAI_SERVICE) {
+            return "EAI_SERVICE";
+        }
+        if (error == EAI_SOCKTYPE) {
+            return "EAI_SOCKTYPE";
+        }
+        if (error == EAI_SYSTEM) {
+            return "EAI_SYSTEM";
+        }
+        return null;
+    }
+
+    public static String errnoName(int errno) {
+        if (errno == E2BIG) {
+            return "E2BIG";
+        }
+        if (errno == EACCES) {
+            return "EACCES";
+        }
+        if (errno == EADDRINUSE) {
+            return "EADDRINUSE";
+        }
+        if (errno == EADDRNOTAVAIL) {
+            return "EADDRNOTAVAIL";
+        }
+        if (errno == EAFNOSUPPORT) {
+            return "EAFNOSUPPORT";
+        }
+        if (errno == EAGAIN) {
+            return "EAGAIN";
+        }
+        if (errno == EALREADY) {
+            return "EALREADY";
+        }
+        if (errno == EBADF) {
+            return "EBADF";
+        }
+        if (errno == EBADMSG) {
+            return "EBADMSG";
+        }
+        if (errno == EBUSY) {
+            return "EBUSY";
+        }
+        if (errno == ECANCELED) {
+            return "ECANCELED";
+        }
+        if (errno == ECHILD) {
+            return "ECHILD";
+        }
+        if (errno == ECONNABORTED) {
+            return "ECONNABORTED";
+        }
+        if (errno == ECONNREFUSED) {
+            return "ECONNREFUSED";
+        }
+        if (errno == ECONNRESET) {
+            return "ECONNRESET";
+        }
+        if (errno == EDEADLK) {
+            return "EDEADLK";
+        }
+        if (errno == EDESTADDRREQ) {
+            return "EDESTADDRREQ";
+        }
+        if (errno == EDOM) {
+            return "EDOM";
+        }
+        if (errno == EDQUOT) {
+            return "EDQUOT";
+        }
+        if (errno == EEXIST) {
+            return "EEXIST";
+        }
+        if (errno == EFAULT) {
+            return "EFAULT";
+        }
+        if (errno == EFBIG) {
+            return "EFBIG";
+        }
+        if (errno == EHOSTUNREACH) {
+            return "EHOSTUNREACH";
+        }
+        if (errno == EIDRM) {
+            return "EIDRM";
+        }
+        if (errno == EILSEQ) {
+            return "EILSEQ";
+        }
+        if (errno == EINPROGRESS) {
+            return "EINPROGRESS";
+        }
+        if (errno == EINTR) {
+            return "EINTR";
+        }
+        if (errno == EINVAL) {
+            return "EINVAL";
+        }
+        if (errno == EIO) {
+            return "EIO";
+        }
+        if (errno == EISCONN) {
+            return "EISCONN";
+        }
+        if (errno == EISDIR) {
+            return "EISDIR";
+        }
+        if (errno == ELOOP) {
+            return "ELOOP";
+        }
+        if (errno == EMFILE) {
+            return "EMFILE";
+        }
+        if (errno == EMLINK) {
+            return "EMLINK";
+        }
+        if (errno == EMSGSIZE) {
+            return "EMSGSIZE";
+        }
+        if (errno == EMULTIHOP) {
+            return "EMULTIHOP";
+        }
+        if (errno == ENAMETOOLONG) {
+            return "ENAMETOOLONG";
+        }
+        if (errno == ENETDOWN) {
+            return "ENETDOWN";
+        }
+        if (errno == ENETRESET) {
+            return "ENETRESET";
+        }
+        if (errno == ENETUNREACH) {
+            return "ENETUNREACH";
+        }
+        if (errno == ENFILE) {
+            return "ENFILE";
+        }
+        if (errno == ENOBUFS) {
+            return "ENOBUFS";
+        }
+        if (errno == ENODATA) {
+            return "ENODATA";
+        }
+        if (errno == ENODEV) {
+            return "ENODEV";
+        }
+        if (errno == ENOENT) {
+            return "ENOENT";
+        }
+        if (errno == ENOEXEC) {
+            return "ENOEXEC";
+        }
+        if (errno == ENOLCK) {
+            return "ENOLCK";
+        }
+        if (errno == ENOLINK) {
+            return "ENOLINK";
+        }
+        if (errno == ENOMEM) {
+            return "ENOMEM";
+        }
+        if (errno == ENOMSG) {
+            return "ENOMSG";
+        }
+        if (errno == ENOPROTOOPT) {
+            return "ENOPROTOOPT";
+        }
+        if (errno == ENOSPC) {
+            return "ENOSPC";
+        }
+        if (errno == ENOSR) {
+            return "ENOSR";
+        }
+        if (errno == ENOSTR) {
+            return "ENOSTR";
+        }
+        if (errno == ENOSYS) {
+            return "ENOSYS";
+        }
+        if (errno == ENOTCONN) {
+            return "ENOTCONN";
+        }
+        if (errno == ENOTDIR) {
+            return "ENOTDIR";
+        }
+        if (errno == ENOTEMPTY) {
+            return "ENOTEMPTY";
+        }
+        if (errno == ENOTSOCK) {
+            return "ENOTSOCK";
+        }
+        if (errno == ENOTSUP) {
+            return "ENOTSUP";
+        }
+        if (errno == ENOTTY) {
+            return "ENOTTY";
+        }
+        if (errno == ENXIO) {
+            return "ENXIO";
+        }
+        if (errno == EOPNOTSUPP) {
+            return "EOPNOTSUPP";
+        }
+        if (errno == EOVERFLOW) {
+            return "EOVERFLOW";
+        }
+        if (errno == EPERM) {
+            return "EPERM";
+        }
+        if (errno == EPIPE) {
+            return "EPIPE";
+        }
+        if (errno == EPROTO) {
+            return "EPROTO";
+        }
+        if (errno == EPROTONOSUPPORT) {
+            return "EPROTONOSUPPORT";
+        }
+        if (errno == EPROTOTYPE) {
+            return "EPROTOTYPE";
+        }
+        if (errno == ERANGE) {
+            return "ERANGE";
+        }
+        if (errno == EROFS) {
+            return "EROFS";
+        }
+        if (errno == ESPIPE) {
+            return "ESPIPE";
+        }
+        if (errno == ESRCH) {
+            return "ESRCH";
+        }
+        if (errno == ESTALE) {
+            return "ESTALE";
+        }
+        if (errno == ETIME) {
+            return "ETIME";
+        }
+        if (errno == ETIMEDOUT) {
+            return "ETIMEDOUT";
+        }
+        if (errno == ETXTBSY) {
+            return "ETXTBSY";
+        }
+        if (errno == EWOULDBLOCK) {
+            return "EWOULDBLOCK";
+        }
+        if (errno == EXDEV) {
+            return "EXDEV";
+        }
+        return null;
+    }
+
+    private static native void initConstants();
+
+    // A hack to avoid these constants being inlined by javac...
+    private static int placeholder() { return 0; }
+    // ...because we want to initialize them at runtime.
+    static {
+        initConstants();
+    }
+}
diff --git a/src/main/java/libcore/io/SizeOf.java b/src/main/java/libcore/io/SizeOf.java
new file mode 100644
index 0000000..728fbfc
--- /dev/null
+++ b/src/main/java/libcore/io/SizeOf.java
@@ -0,0 +1,29 @@
+/*
+ * 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 libcore.io;
+
+public final class SizeOf {
+    public static final int CHAR = 2;
+    public static final int DOUBLE = 8;
+    public static final int FLOAT = 4;
+    public static final int INT = 4;
+    public static final int LONG = 8;
+    public static final int SHORT = 2;
+
+    private SizeOf() {
+    }
+}
diff --git a/src/main/java/libcore/io/Streams.java b/src/main/java/libcore/io/Streams.java
new file mode 100644
index 0000000..1ad2356
--- /dev/null
+++ b/src/main/java/libcore/io/Streams.java
@@ -0,0 +1,217 @@
+/*
+ * 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 libcore.io;
+
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.util.concurrent.atomic.AtomicReference;
+import libcore.util.Libcore;
+
+public final class Streams {
+    private static AtomicReference<byte[]> skipBuffer = new AtomicReference<byte[]>();
+
+    private Streams() {
+    }
+
+    /**
+     * Implements InputStream.read(int) in terms of InputStream.read(byte[], int, int).
+     * InputStream assumes that you implement InputStream.read(int) and provides default
+     * implementations of the others, but often the opposite is more efficient.
+     */
+    public static int readSingleByte(InputStream in) throws IOException {
+        byte[] buffer = new byte[1];
+        int result = in.read(buffer, 0, 1);
+        return (result != -1) ? buffer[0] & 0xff : -1;
+    }
+
+    /**
+     * Implements OutputStream.write(int) in terms of OutputStream.write(byte[], int, int).
+     * OutputStream assumes that you implement OutputStream.write(int) and provides default
+     * implementations of the others, but often the opposite is more efficient.
+     */
+    public static void writeSingleByte(OutputStream out, int b) throws IOException {
+        byte[] buffer = new byte[1];
+        buffer[0] = (byte) (b & 0xff);
+        out.write(buffer);
+    }
+
+    /**
+     * Fills 'dst' with bytes from 'in', throwing EOFException if insufficient bytes are available.
+     */
+    public static void readFully(InputStream in, byte[] dst) throws IOException {
+        readFully(in, dst, 0, dst.length);
+    }
+
+    /**
+     * Reads exactly 'byteCount' bytes from 'in' (into 'dst' at offset 'offset'), and throws
+     * EOFException if insufficient bytes are available.
+     *
+     * Used to implement {@link java.io.DataInputStream#readFully(byte[], int, int)}.
+     */
+    public static void readFully(InputStream in, byte[] dst, int offset, int byteCount) throws IOException {
+        if (byteCount == 0) {
+            return;
+        }
+        if (in == null) {
+            throw new NullPointerException("in == null");
+        }
+        if (dst == null) {
+            throw new NullPointerException("dst == null");
+        }
+        Libcore.checkOffsetAndCount(dst.length, offset, byteCount);
+        while (byteCount > 0) {
+            int bytesRead = in.read(dst, offset, byteCount);
+            if (bytesRead < 0) {
+                throw new EOFException();
+            }
+            offset += bytesRead;
+            byteCount -= bytesRead;
+        }
+    }
+
+    /**
+     * Returns a byte[] containing the remainder of 'in', closing it when done.
+     */
+    public static byte[] readFully(InputStream in) throws IOException {
+        try {
+            return readFullyNoClose(in);
+        } finally {
+            in.close();
+        }
+    }
+
+    /**
+     * Returns a byte[] containing the remainder of 'in'.
+     */
+    public static byte[] readFullyNoClose(InputStream in) throws IOException {
+        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        byte[] buffer = new byte[1024];
+        int count;
+        while ((count = in.read(buffer)) != -1) {
+            bytes.write(buffer, 0, count);
+        }
+        return bytes.toByteArray();
+    }
+
+    /**
+     * Returns the remainder of 'reader' as a string, closing it when done.
+     */
+    public static String readFully(Reader reader) throws IOException {
+        try {
+            StringWriter writer = new StringWriter();
+            char[] buffer = new char[1024];
+            int count;
+            while ((count = reader.read(buffer)) != -1) {
+                writer.write(buffer, 0, count);
+            }
+            return writer.toString();
+        } finally {
+            reader.close();
+        }
+    }
+
+    public static void skipAll(InputStream in) throws IOException {
+        do {
+            in.skip(Long.MAX_VALUE);
+        } while (in.read() != -1);
+    }
+
+    /**
+     * Call {@code in.read()} repeatedly until either the stream is exhausted or
+     * {@code byteCount} bytes have been read.
+     *
+     * <p>This method reuses the skip buffer but is careful to never use it at
+     * the same time that another stream is using it. Otherwise streams that use
+     * the caller's buffer for consistency checks like CRC could be clobbered by
+     * other threads. A thread-local buffer is also insufficient because some
+     * streams may call other streams in their skip() method, also clobbering the
+     * buffer.
+     */
+    public static long skipByReading(InputStream in, long byteCount) throws IOException {
+        // acquire the shared skip buffer.
+        byte[] buffer = skipBuffer.getAndSet(null);
+        if (buffer == null) {
+            buffer = new byte[4096];
+        }
+
+        long skipped = 0;
+        while (skipped < byteCount) {
+            int toRead = (int) Math.min(byteCount - skipped, buffer.length);
+            int read = in.read(buffer, 0, toRead);
+            if (read == -1) {
+                break;
+            }
+            skipped += read;
+            if (read < toRead) {
+                break;
+            }
+        }
+
+        // release the shared skip buffer.
+        skipBuffer.set(buffer);
+
+        return skipped;
+    }
+
+    /**
+     * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
+     * Returns the total number of bytes transferred.
+     */
+    public static int copy(InputStream in, OutputStream out) throws IOException {
+        int total = 0;
+        byte[] buffer = new byte[8192];
+        int c;
+        while ((c = in.read(buffer)) != -1) {
+            total += c;
+            out.write(buffer, 0, c);
+        }
+        return total;
+    }
+
+    /**
+     * Returns the ASCII characters up to but not including the next "\r\n", or
+     * "\n".
+     *
+     * @throws java.io.EOFException if the stream is exhausted before the next newline
+     *     character.
+     */
+    public static String readAsciiLine(InputStream in) throws IOException {
+        // TODO: support UTF-8 here instead
+
+        StringBuilder result = new StringBuilder(80);
+        while (true) {
+            int c = in.read();
+            if (c == -1) {
+                throw new EOFException();
+            } else if (c == '\n') {
+                break;
+            }
+
+            result.append((char) c);
+        }
+        int length = result.length();
+        if (length > 0 && result.charAt(length - 1) == '\r') {
+            result.setLength(length - 1);
+        }
+        return result.toString();
+    }
+}
diff --git a/src/main/java/libcore/net/MimeUtils.java b/src/main/java/libcore/net/MimeUtils.java
new file mode 100644
index 0000000..76193ff
--- /dev/null
+++ b/src/main/java/libcore/net/MimeUtils.java
@@ -0,0 +1,480 @@
+/*
+ * 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 libcore.net;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Utilities for dealing with MIME types.
+ * Used to implement java.net.URLConnection and android.webkit.MimeTypeMap.
+ */
+public final class MimeUtils {
+    private static final Map<String, String> MIME_TYPE_TO_EXTENSION_MAP = new HashMap<String, String>();
+
+    private static final Map<String, String> EXTENSION_TO_MIME_TYPE_MAP = new HashMap<String, String>();
+
+    static {
+        // The following table is based on /etc/mime.types data minus
+        // chemical/* MIME types and MIME types that don't map to any
+        // file extensions. We also exclude top-level domain names to
+        // deal with cases like:
+        //
+        // mail.google.com/a/google.com
+        //
+        // and "active" MIME types (due to potential security issues).
+
+        add("application/andrew-inset", "ez");
+        add("application/dsptype", "tsp");
+        add("application/futuresplash", "spl");
+        add("application/hta", "hta");
+        add("application/mac-binhex40", "hqx");
+        add("application/mac-compactpro", "cpt");
+        add("application/mathematica", "nb");
+        add("application/msaccess", "mdb");
+        add("application/oda", "oda");
+        add("application/ogg", "ogg");
+        add("application/pdf", "pdf");
+        add("application/pgp-keys", "key");
+        add("application/pgp-signature", "pgp");
+        add("application/pics-rules", "prf");
+        add("application/rar", "rar");
+        add("application/rdf+xml", "rdf");
+        add("application/rss+xml", "rss");
+        add("application/zip", "zip");
+        add("application/vnd.android.package-archive", "apk");
+        add("application/vnd.cinderella", "cdy");
+        add("application/vnd.ms-pki.stl", "stl");
+        add("application/vnd.oasis.opendocument.database", "odb");
+        add("application/vnd.oasis.opendocument.formula", "odf");
+        add("application/vnd.oasis.opendocument.graphics", "odg");
+        add("application/vnd.oasis.opendocument.graphics-template", "otg");
+        add("application/vnd.oasis.opendocument.image", "odi");
+        add("application/vnd.oasis.opendocument.spreadsheet", "ods");
+        add("application/vnd.oasis.opendocument.spreadsheet-template", "ots");
+        add("application/vnd.oasis.opendocument.text", "odt");
+        add("application/vnd.oasis.opendocument.text-master", "odm");
+        add("application/vnd.oasis.opendocument.text-template", "ott");
+        add("application/vnd.oasis.opendocument.text-web", "oth");
+        add("application/vnd.google-earth.kml+xml", "kml");
+        add("application/vnd.google-earth.kmz", "kmz");
+        add("application/msword", "doc");
+        add("application/msword", "dot");
+        add("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx");
+        add("application/vnd.openxmlformats-officedocument.wordprocessingml.template", "dotx");
+        add("application/vnd.ms-excel", "xls");
+        add("application/vnd.ms-excel", "xlt");
+        add("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx");
+        add("application/vnd.openxmlformats-officedocument.spreadsheetml.template", "xltx");
+        add("application/vnd.ms-powerpoint", "ppt");
+        add("application/vnd.ms-powerpoint", "pot");
+        add("application/vnd.ms-powerpoint", "pps");
+        add("application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx");
+        add("application/vnd.openxmlformats-officedocument.presentationml.template", "potx");
+        add("application/vnd.openxmlformats-officedocument.presentationml.slideshow", "ppsx");
+        add("application/vnd.rim.cod", "cod");
+        add("application/vnd.smaf", "mmf");
+        add("application/vnd.stardivision.calc", "sdc");
+        add("application/vnd.stardivision.draw", "sda");
+        add("application/vnd.stardivision.impress", "sdd");
+        add("application/vnd.stardivision.impress", "sdp");
+        add("application/vnd.stardivision.math", "smf");
+        add("application/vnd.stardivision.writer", "sdw");
+        add("application/vnd.stardivision.writer", "vor");
+        add("application/vnd.stardivision.writer-global", "sgl");
+        add("application/vnd.sun.xml.calc", "sxc");
+        add("application/vnd.sun.xml.calc.template", "stc");
+        add("application/vnd.sun.xml.draw", "sxd");
+        add("application/vnd.sun.xml.draw.template", "std");
+        add("application/vnd.sun.xml.impress", "sxi");
+        add("application/vnd.sun.xml.impress.template", "sti");
+        add("application/vnd.sun.xml.math", "sxm");
+        add("application/vnd.sun.xml.writer", "sxw");
+        add("application/vnd.sun.xml.writer.global", "sxg");
+        add("application/vnd.sun.xml.writer.template", "stw");
+        add("application/vnd.visio", "vsd");
+        add("application/x-abiword", "abw");
+        add("application/x-apple-diskimage", "dmg");
+        add("application/x-bcpio", "bcpio");
+        add("application/x-bittorrent", "torrent");
+        add("application/x-cdf", "cdf");
+        add("application/x-cdlink", "vcd");
+        add("application/x-chess-pgn", "pgn");
+        add("application/x-cpio", "cpio");
+        add("application/x-debian-package", "deb");
+        add("application/x-debian-package", "udeb");
+        add("application/x-director", "dcr");
+        add("application/x-director", "dir");
+        add("application/x-director", "dxr");
+        add("application/x-dms", "dms");
+        add("application/x-doom", "wad");
+        add("application/x-dvi", "dvi");
+        add("application/x-flac", "flac");
+        add("application/x-font", "pfa");
+        add("application/x-font", "pfb");
+        add("application/x-font", "gsf");
+        add("application/x-font", "pcf");
+        add("application/x-font", "pcf.Z");
+        add("application/x-freemind", "mm");
+        add("application/x-futuresplash", "spl");
+        add("application/x-gnumeric", "gnumeric");
+        add("application/x-go-sgf", "sgf");
+        add("application/x-graphing-calculator", "gcf");
+        add("application/x-gtar", "gtar");
+        add("application/x-gtar", "tgz");
+        add("application/x-gtar", "taz");
+        add("application/x-hdf", "hdf");
+        add("application/x-ica", "ica");
+        add("application/x-internet-signup", "ins");
+        add("application/x-internet-signup", "isp");
+        add("application/x-iphone", "iii");
+        add("application/x-iso9660-image", "iso");
+        add("application/x-jmol", "jmz");
+        add("application/x-kchart", "chrt");
+        add("application/x-killustrator", "kil");
+        add("application/x-koan", "skp");
+        add("application/x-koan", "skd");
+        add("application/x-koan", "skt");
+        add("application/x-koan", "skm");
+        add("application/x-kpresenter", "kpr");
+        add("application/x-kpresenter", "kpt");
+        add("application/x-kspread", "ksp");
+        add("application/x-kword", "kwd");
+        add("application/x-kword", "kwt");
+        add("application/x-latex", "latex");
+        add("application/x-lha", "lha");
+        add("application/x-lzh", "lzh");
+        add("application/x-lzx", "lzx");
+        add("application/x-maker", "frm");
+        add("application/x-maker", "maker");
+        add("application/x-maker", "frame");
+        add("application/x-maker", "fb");
+        add("application/x-maker", "book");
+        add("application/x-maker", "fbdoc");
+        add("application/x-mif", "mif");
+        add("application/x-ms-wmd", "wmd");
+        add("application/x-ms-wmz", "wmz");
+        add("application/x-msi", "msi");
+        add("application/x-ns-proxy-autoconfig", "pac");
+        add("application/x-nwc", "nwc");
+        add("application/x-object", "o");
+        add("application/x-oz-application", "oza");
+        add("application/x-pkcs12", "p12");
+        add("application/x-pkcs12", "pfx");
+        add("application/x-pkcs7-certreqresp", "p7r");
+        add("application/x-pkcs7-crl", "crl");
+        add("application/x-quicktimeplayer", "qtl");
+        add("application/x-shar", "shar");
+        add("application/x-shockwave-flash", "swf");
+        add("application/x-stuffit", "sit");
+        add("application/x-sv4cpio", "sv4cpio");
+        add("application/x-sv4crc", "sv4crc");
+        add("application/x-tar", "tar");
+        add("application/x-texinfo", "texinfo");
+        add("application/x-texinfo", "texi");
+        add("application/x-troff", "t");
+        add("application/x-troff", "roff");
+        add("application/x-troff-man", "man");
+        add("application/x-ustar", "ustar");
+        add("application/x-wais-source", "src");
+        add("application/x-wingz", "wz");
+        add("application/x-webarchive", "webarchive");
+        add("application/x-webarchive-xml", "webarchivexml");
+        add("application/x-x509-ca-cert", "crt");
+        add("application/x-x509-user-cert", "crt");
+        add("application/x-xcf", "xcf");
+        add("application/x-xfig", "fig");
+        add("application/xhtml+xml", "xhtml");
+        add("audio/3gpp", "3gpp");
+        add("audio/amr", "amr");
+        add("audio/basic", "snd");
+        add("audio/midi", "mid");
+        add("audio/midi", "midi");
+        add("audio/midi", "kar");
+        add("audio/midi", "xmf");
+        add("audio/mobile-xmf", "mxmf");
+        add("audio/mpeg", "mpga");
+        add("audio/mpeg", "mpega");
+        add("audio/mpeg", "mp2");
+        add("audio/mpeg", "mp3");
+        add("audio/mpeg", "m4a");
+        add("audio/mpegurl", "m3u");
+        add("audio/prs.sid", "sid");
+        add("audio/x-aiff", "aif");
+        add("audio/x-aiff", "aiff");
+        add("audio/x-aiff", "aifc");
+        add("audio/x-gsm", "gsm");
+        add("audio/x-mpegurl", "m3u");
+        add("audio/x-ms-wma", "wma");
+        add("audio/x-ms-wax", "wax");
+        add("audio/x-pn-realaudio", "ra");
+        add("audio/x-pn-realaudio", "rm");
+        add("audio/x-pn-realaudio", "ram");
+        add("audio/x-realaudio", "ra");
+        add("audio/x-scpls", "pls");
+        add("audio/x-sd2", "sd2");
+        add("audio/x-wav", "wav");
+        add("image/bmp", "bmp");
+        add("image/gif", "gif");
+        add("image/ico", "cur");
+        add("image/ico", "ico");
+        add("image/ief", "ief");
+        add("image/jpeg", "jpeg");
+        add("image/jpeg", "jpg");
+        add("image/jpeg", "jpe");
+        add("image/pcx", "pcx");
+        add("image/png", "png");
+        add("image/svg+xml", "svg");
+        add("image/svg+xml", "svgz");
+        add("image/tiff", "tiff");
+        add("image/tiff", "tif");
+        add("image/vnd.djvu", "djvu");
+        add("image/vnd.djvu", "djv");
+        add("image/vnd.wap.wbmp", "wbmp");
+        add("image/x-cmu-raster", "ras");
+        add("image/x-coreldraw", "cdr");
+        add("image/x-coreldrawpattern", "pat");
+        add("image/x-coreldrawtemplate", "cdt");
+        add("image/x-corelphotopaint", "cpt");
+        add("image/x-icon", "ico");
+        add("image/x-jg", "art");
+        add("image/x-jng", "jng");
+        add("image/x-ms-bmp", "bmp");
+        add("image/x-photoshop", "psd");
+        add("image/x-portable-anymap", "pnm");
+        add("image/x-portable-bitmap", "pbm");
+        add("image/x-portable-graymap", "pgm");
+        add("image/x-portable-pixmap", "ppm");
+        add("image/x-rgb", "rgb");
+        add("image/x-xbitmap", "xbm");
+        add("image/x-xpixmap", "xpm");
+        add("image/x-xwindowdump", "xwd");
+        add("model/iges", "igs");
+        add("model/iges", "iges");
+        add("model/mesh", "msh");
+        add("model/mesh", "mesh");
+        add("model/mesh", "silo");
+        add("text/calendar", "ics");
+        add("text/calendar", "icz");
+        add("text/comma-separated-values", "csv");
+        add("text/css", "css");
+        add("text/html", "htm");
+        add("text/html", "html");
+        add("text/h323", "323");
+        add("text/iuls", "uls");
+        add("text/mathml", "mml");
+        // add ".txt" first so it will be the default for ExtensionFromMimeType
+        add("text/plain", "txt");
+        add("text/plain", "asc");
+        add("text/plain", "text");
+        add("text/plain", "diff");
+        add("text/plain", "po");     // reserve "pot" for vnd.ms-powerpoint
+        add("text/richtext", "rtx");
+        add("text/rtf", "rtf");
+        add("text/texmacs", "ts");
+        add("text/text", "phps");
+        add("text/tab-separated-values", "tsv");
+        add("text/xml", "xml");
+        add("text/x-bibtex", "bib");
+        add("text/x-boo", "boo");
+        add("text/x-c++hdr", "h++");
+        add("text/x-c++hdr", "hpp");
+        add("text/x-c++hdr", "hxx");
+        add("text/x-c++hdr", "hh");
+        add("text/x-c++src", "c++");
+        add("text/x-c++src", "cpp");
+        add("text/x-c++src", "cxx");
+        add("text/x-chdr", "h");
+        add("text/x-component", "htc");
+        add("text/x-csh", "csh");
+        add("text/x-csrc", "c");
+        add("text/x-dsrc", "d");
+        add("text/x-haskell", "hs");
+        add("text/x-java", "java");
+        add("text/x-literate-haskell", "lhs");
+        add("text/x-moc", "moc");
+        add("text/x-pascal", "p");
+        add("text/x-pascal", "pas");
+        add("text/x-pcs-gcd", "gcd");
+        add("text/x-setext", "etx");
+        add("text/x-tcl", "tcl");
+        add("text/x-tex", "tex");
+        add("text/x-tex", "ltx");
+        add("text/x-tex", "sty");
+        add("text/x-tex", "cls");
+        add("text/x-vcalendar", "vcs");
+        add("text/x-vcard", "vcf");
+        add("video/3gpp", "3gpp");
+        add("video/3gpp", "3gp");
+        add("video/3gpp", "3g2");
+        add("video/dl", "dl");
+        add("video/dv", "dif");
+        add("video/dv", "dv");
+        add("video/fli", "fli");
+        add("video/m4v", "m4v");
+        add("video/mpeg", "mpeg");
+        add("video/mpeg", "mpg");
+        add("video/mpeg", "mpe");
+        add("video/mp4", "mp4");
+        add("video/mpeg", "VOB");
+        add("video/quicktime", "qt");
+        add("video/quicktime", "mov");
+        add("video/vnd.mpegurl", "mxu");
+        add("video/x-la-asf", "lsf");
+        add("video/x-la-asf", "lsx");
+        add("video/x-mng", "mng");
+        add("video/x-ms-asf", "asf");
+        add("video/x-ms-asf", "asx");
+        add("video/x-ms-wm", "wm");
+        add("video/x-ms-wmv", "wmv");
+        add("video/x-ms-wmx", "wmx");
+        add("video/x-ms-wvx", "wvx");
+        add("video/x-msvideo", "avi");
+        add("video/x-sgi-movie", "movie");
+        add("x-conference/x-cooltalk", "ice");
+        add("x-epoc/x-sisx-app", "sisx");
+        applyOverrides();
+    }
+
+    private static void add(String mimeType, String extension) {
+        //
+        // if we have an existing x --> y mapping, we do not want to
+        // override it with another mapping x --> ?
+        // this is mostly because of the way the mime-type map below
+        // is constructed (if a mime type maps to several extensions
+        // the first extension is considered the most popular and is
+        // added first; we do not want to overwrite it later).
+        //
+        if (!MIME_TYPE_TO_EXTENSION_MAP.containsKey(mimeType)) {
+            MIME_TYPE_TO_EXTENSION_MAP.put(mimeType, extension);
+        }
+        EXTENSION_TO_MIME_TYPE_MAP.put(extension, mimeType);
+    }
+
+    private static InputStream getContentTypesPropertiesStream() {
+        // User override?
+        String userTable = System.getProperty("content.types.user.table");
+        if (userTable != null) {
+            File f = new File(userTable);
+            if (f.exists()) {
+                try {
+                    return new FileInputStream(f);
+                } catch (IOException ignored) {
+                }
+            }
+        }
+
+        // Standard location?
+        File f = new File(System.getProperty("java.home"), "lib" + File.separator + "content-types.properties");
+        if (f.exists()) {
+            try {
+                return new FileInputStream(f);
+            } catch (IOException ignored) {
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * This isn't what the RI does. The RI doesn't have hard-coded defaults, so supplying your
+     * own "content.types.user.table" means you don't get any of the built-ins, and the built-ins
+     * come from "$JAVA_HOME/lib/content-types.properties".
+     */
+    private static void applyOverrides() {
+        // Get the appropriate InputStream to read overrides from, if any.
+        InputStream stream = getContentTypesPropertiesStream();
+        if (stream == null) {
+            return;
+        }
+
+        try {
+            try {
+                // Read the properties file...
+                Properties overrides = new Properties();
+                overrides.load(stream);
+                // And translate its mapping to ours...
+                for (Map.Entry<Object, Object> entry : overrides.entrySet()) {
+                    String extension = (String) entry.getKey();
+                    String mimeType = (String) entry.getValue();
+                    add(mimeType, extension);
+                }
+            } finally {
+                stream.close();
+            }
+        } catch (IOException ignored) {
+        }
+    }
+
+    private MimeUtils() {
+    }
+
+    /**
+     * Returns true if the given MIME type has an entry in the map.
+     * @param mimeType A MIME type (i.e. text/plain)
+     * @return True iff there is a mimeType entry in the map.
+     */
+    public static boolean hasMimeType(String mimeType) {
+        if (mimeType == null || mimeType.length() == 0) {
+            return false;
+        }
+        return MIME_TYPE_TO_EXTENSION_MAP.containsKey(mimeType);
+    }
+
+    /**
+     * Returns the MIME type for the given extension.
+     * @param extension A file extension without the leading '.'
+     * @return The MIME type for the given extension or null iff there is none.
+     */
+    public static String guessMimeTypeFromExtension(String extension) {
+        if (extension == null || extension.length() == 0) {
+            return null;
+        }
+        return EXTENSION_TO_MIME_TYPE_MAP.get(extension);
+    }
+
+    /**
+     * Returns true if the given extension has a registered MIME type.
+     * @param extension A file extension without the leading '.'
+     * @return True iff there is an extension entry in the map.
+     */
+    public static boolean hasExtension(String extension) {
+        if (extension == null || extension.length() == 0) {
+            return false;
+        }
+        return EXTENSION_TO_MIME_TYPE_MAP.containsKey(extension);
+    }
+
+    /**
+     * Returns the registered extension for the given MIME type. Note that some
+     * MIME types map to multiple extensions. This call will return the most
+     * common extension for the given MIME type.
+     * @param mimeType A MIME type (i.e. text/plain)
+     * @return The extension for the given MIME type or null iff there is none.
+     */
+    public static String guessExtensionFromMimeType(String mimeType) {
+        if (mimeType == null || mimeType.length() == 0) {
+            return null;
+        }
+        return MIME_TYPE_TO_EXTENSION_MAP.get(mimeType);
+    }
+}
diff --git a/src/main/java/libcore/net/http/AbstractHttpInputStream.java b/src/main/java/libcore/net/http/AbstractHttpInputStream.java
new file mode 100644
index 0000000..70f76b7
--- /dev/null
+++ b/src/main/java/libcore/net/http/AbstractHttpInputStream.java
@@ -0,0 +1,107 @@
+/*
+ * 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 libcore.net.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheRequest;
+import libcore.io.Streams;
+
+/**
+ * An input stream for the body of an HTTP response.
+ *
+ * <p>Since a single socket's input stream may be used to read multiple HTTP
+ * responses from the same server, subclasses shouldn't close the socket stream.
+ *
+ * <p>A side effect of reading an HTTP response is that the response cache
+ * is populated. If the stream is closed early, that cache entry will be
+ * invalidated.
+ */
+abstract class AbstractHttpInputStream extends InputStream {
+    protected final InputStream in;
+    protected final HttpEngine httpEngine;
+    private final CacheRequest cacheRequest;
+    private final OutputStream cacheBody;
+    protected boolean closed;
+
+    AbstractHttpInputStream(InputStream in, HttpEngine httpEngine,
+            CacheRequest cacheRequest) throws IOException {
+        this.in = in;
+        this.httpEngine = httpEngine;
+
+        OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null;
+
+        // some apps return a null body; for compatibility we treat that like a null cache request
+        if (cacheBody == null) {
+            cacheRequest = null;
+        }
+
+        this.cacheBody = cacheBody;
+        this.cacheRequest = cacheRequest;
+    }
+
+    /**
+     * read() is implemented using read(byte[], int, int) so subclasses only
+     * need to override the latter.
+     */
+    @Override public final int read() throws IOException {
+        return Streams.readSingleByte(this);
+    }
+
+    protected final void checkNotClosed() throws IOException {
+        if (closed) {
+            throw new IOException("stream closed");
+        }
+    }
+
+    protected final void cacheWrite(byte[] buffer, int offset, int count) throws IOException {
+        if (cacheBody != null) {
+            cacheBody.write(buffer, offset, count);
+        }
+    }
+
+    /**
+     * Closes the cache entry and makes the socket available for reuse. This
+     * should be invoked when the end of the body has been reached.
+     */
+    protected final void endOfInput(boolean reuseSocket) throws IOException {
+        if (cacheRequest != null) {
+            cacheBody.close();
+        }
+        httpEngine.release(reuseSocket);
+    }
+
+    /**
+     * Calls abort on the cache entry and disconnects the socket. This
+     * should be invoked when the connection is closed unexpectedly to
+     * invalidate the cache entry and to prevent the HTTP connection from
+     * being reused. HTTP messages are sent in serial so whenever a message
+     * cannot be read to completion, subsequent messages cannot be read
+     * either and the connection must be discarded.
+     *
+     * <p>An earlier implementation skipped the remaining bytes, but this
+     * requires that the entire transfer be completed. If the intention was
+     * to cancel the transfer, closing the connection is the only solution.
+     */
+    protected final void unexpectedEndOfInput() {
+        if (cacheRequest != null) {
+            cacheRequest.abort();
+        }
+        httpEngine.release(false);
+    }
+}
diff --git a/src/main/java/libcore/net/http/AbstractHttpOutputStream.java b/src/main/java/libcore/net/http/AbstractHttpOutputStream.java
new file mode 100644
index 0000000..145bc50
--- /dev/null
+++ b/src/main/java/libcore/net/http/AbstractHttpOutputStream.java
@@ -0,0 +1,40 @@
+/*
+ * 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 libcore.net.http;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An output stream for the body of an HTTP request.
+ *
+ * <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.
+ */
+abstract class AbstractHttpOutputStream 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");
+        }
+    }
+}
diff --git a/src/main/java/libcore/net/http/Challenge.java b/src/main/java/libcore/net/http/Challenge.java
new file mode 100644
index 0000000..d373c0a
--- /dev/null
+++ b/src/main/java/libcore/net/http/Challenge.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.http;
+
+/**
+ * An RFC 2617 challenge.
+ */
+final class Challenge {
+    final String scheme;
+    final String realm;
+
+    Challenge(String scheme, String realm) {
+        this.scheme = scheme;
+        this.realm = realm;
+    }
+
+    @Override public boolean equals(Object o) {
+        return o instanceof Challenge
+                && ((Challenge) o).scheme.equals(scheme)
+                && ((Challenge) o).realm.equals(realm);
+    }
+
+    @Override public int hashCode() {
+        return scheme.hashCode() + 31 * realm.hashCode();
+    }
+}
diff --git a/src/main/java/libcore/net/http/HeaderParser.java b/src/main/java/libcore/net/http/HeaderParser.java
new file mode 100644
index 0000000..4a4ac2a
--- /dev/null
+++ b/src/main/java/libcore/net/http/HeaderParser.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.http;
+
+import java.util.ArrayList;
+import java.util.List;
+
+final class HeaderParser {
+
+    public interface CacheControlHandler {
+        void handle(String directive, String parameter);
+    }
+
+    /**
+     * Parse a comma-separated list of cache control header values.
+     */
+    public static void parseCacheControl(String value, CacheControlHandler handler) {
+        int pos = 0;
+        while (pos < value.length()) {
+            int tokenStart = pos;
+            pos = skipUntil(value, pos, "=,");
+            String directive = value.substring(tokenStart, pos).trim();
+
+            if (pos == value.length() || value.charAt(pos) == ',') {
+                pos++; // consume ',' (if necessary)
+                handler.handle(directive, null);
+                continue;
+            }
+
+            pos++; // consume '='
+            pos = skipWhitespace(value, pos);
+
+            String parameter;
+
+            // quoted string
+            if (pos < value.length() && value.charAt(pos) == '\"') {
+                pos++; // consume '"' open quote
+                int parameterStart = pos;
+                pos = skipUntil(value, pos, "\"");
+                parameter = value.substring(parameterStart, pos);
+                pos++; // consume '"' close quote (if necessary)
+
+            // unquoted string
+            } else {
+                int parameterStart = pos;
+                pos = skipUntil(value, pos, ",");
+                parameter = value.substring(parameterStart, pos).trim();
+            }
+
+            handler.handle(directive, parameter);
+        }
+    }
+
+    /**
+     * Parse RFC 2617 challenges. This API is only interested in the scheme
+     * name and realm.
+     */
+    public static List<Challenge> parseChallenges(
+            RawHeaders responseHeaders, String challengeHeader) {
+        /*
+         * auth-scheme = token
+         * auth-param  = token "=" ( token | quoted-string )
+         * challenge   = auth-scheme 1*SP 1#auth-param
+         * realm       = "realm" "=" realm-value
+         * realm-value = quoted-string
+         */
+        List<Challenge> result = new ArrayList<Challenge>();
+        for (int h = 0; h < responseHeaders.length(); h++) {
+            if (!challengeHeader.equalsIgnoreCase(responseHeaders.getFieldName(h))) {
+                continue;
+            }
+            String value = responseHeaders.getValue(h);
+            int pos = 0;
+            while (pos < value.length()) {
+                int tokenStart = pos;
+                pos = skipUntil(value, pos, " ");
+
+                String scheme = value.substring(tokenStart, pos).trim();
+                pos = skipWhitespace(value, pos);
+
+                // TODO: This currently only handles schemes with a 'realm' parameter;
+                //       It needs to be fixed to handle any scheme and any parameters
+                //       http://code.google.com/p/android/issues/detail?id=11140
+
+                if (!value.regionMatches(pos, "realm=\"", 0, "realm=\"".length())) {
+                    break; // unexpected challenge parameter; give up
+                }
+
+                pos += "realm=\"".length();
+                int realmStart = pos;
+                pos = skipUntil(value, pos, "\"");
+                String realm = value.substring(realmStart, pos);
+                pos++; // consume '"' close quote
+                pos = skipUntil(value, pos, ",");
+                pos++; // consume ',' comma
+                pos = skipWhitespace(value, pos);
+                result.add(new Challenge(scheme, realm));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Returns the next index in {@code input} at or after {@code pos} that
+     * contains a character from {@code characters}. Returns the input length if
+     * none of the requested characters can be found.
+     */
+    private static int skipUntil(String input, int pos, String characters) {
+        for (; pos < input.length(); pos++) {
+            if (characters.indexOf(input.charAt(pos)) != -1) {
+                break;
+            }
+        }
+        return pos;
+    }
+
+    /**
+     * Returns the next non-whitespace character in {@code input} that is white
+     * space. Result is undefined if input contains newline characters.
+     */
+    private static int skipWhitespace(String input, int pos) {
+        for (; pos < input.length(); pos++) {
+            char c = input.charAt(pos);
+            if (c != ' ' && c != '\t') {
+                break;
+            }
+        }
+        return pos;
+    }
+
+    /**
+     * Returns {@code value} as a positive integer, or 0 if it is negative, or
+     * -1 if it cannot be parsed.
+     */
+    public static int parseSeconds(String value) {
+        try {
+            long seconds = Long.parseLong(value);
+            if (seconds > Integer.MAX_VALUE) {
+                return Integer.MAX_VALUE;
+            } else if (seconds < 0) {
+                return 0;
+            } else {
+                return (int) seconds;
+            }
+        } catch (NumberFormatException e) {
+            return -1;
+        }
+    }
+
+    private HeaderParser() {
+    }
+}
diff --git a/src/main/java/libcore/net/http/HttpConnection.java b/src/main/java/libcore/net/http/HttpConnection.java
new file mode 100644
index 0000000..6d5b7d8
--- /dev/null
+++ b/src/main/java/libcore/net/http/HttpConnection.java
@@ -0,0 +1,373 @@
+/*
+ *  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 libcore.net.http;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import libcore.io.IoUtils;
+import libcore.net.spdy.SpdyConnection;
+import libcore.util.Libcore;
+import libcore.util.Objects;
+
+/**
+ * Holds the sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection,
+ * which may be used for multiple HTTP request/response exchanges. Connections
+ * may be direct to the origin server or via a proxy. Create an instance using
+ * the {@link Address} inner class.
+ *
+ * <p>Do not confuse this class with the misnamed {@code HttpURLConnection},
+ * which isn't so much a connection as a single request/response pair.
+ */
+final class HttpConnection {
+    private static final byte[] NPN_PROTOCOLS = new byte[] {
+            6, 's', 'p', 'd', 'y', '/', '2',
+            8, 'h', 't', 't', 'p', '/', '1', '.', '1',
+    };
+    private static final byte[] SPDY2 = new byte[] {
+            's', 'p', 'd', 'y', '/', '2',
+    };
+    private static final byte[] HTTP_11 = new byte[] {
+            'h', 't', 't', 'p', '/', '1', '.', '1',
+    };
+
+    private final Address address;
+    private final Socket socket;
+    private InputStream inputStream;
+    private OutputStream outputStream;
+    private SSLSocket sslSocket;
+    private InputStream sslInputStream;
+    private OutputStream sslOutputStream;
+    private boolean recycled = false;
+    private SpdyConnection spdyConnection;
+
+    /**
+     * The version this client will use. Either 0 for HTTP/1.0, or 1 for
+     * HTTP/1.1. Upon receiving a non-HTTP/1.1 response, this client
+     * automatically sets its version to HTTP/1.0.
+     */
+    int httpMinorVersion = 1; // Assume HTTP/1.1
+
+    private HttpConnection(Address config, int connectTimeout) throws IOException {
+        this.address = config;
+
+        /*
+         * Try each of the host's addresses for best behavior in mixed IPv4/IPv6
+         * environments. See http://b/2876927
+         * TODO: add a hidden method so that Socket.tryAllAddresses can does this for us
+         */
+        Socket socketCandidate = null;
+        InetAddress[] addresses = InetAddress.getAllByName(config.socketHost);
+        for (int i = 0; i < addresses.length; i++) {
+            socketCandidate = (config.proxy != null && config.proxy.type() != Proxy.Type.HTTP)
+                    ? new Socket(config.proxy)
+                    : new Socket();
+            try {
+                socketCandidate.connect(
+                        new InetSocketAddress(addresses[i], config.socketPort), connectTimeout);
+                break;
+            } catch (IOException e) {
+                if (i == addresses.length - 1) {
+                    throw e;
+                }
+            }
+        }
+
+        if (socketCandidate == null) {
+            throw new IOException();
+        }
+
+        this.socket = socketCandidate;
+
+        /*
+         * Buffer the socket stream to permit efficient parsing of HTTP headers
+         * and chunk sizes. Benchmarks suggest 128 is sufficient. We cannot
+         * buffer when setting up a tunnel because we may consume bytes intended
+         * for the SSL socket.
+         */
+        int bufferSize = 128;
+        inputStream = address.requiresTunnel
+                ? socket.getInputStream()
+                : new BufferedInputStream(socket.getInputStream(), bufferSize);
+        outputStream = socket.getOutputStream();
+    }
+
+    public static HttpConnection connect(URI uri, SSLSocketFactory sslSocketFactory,
+            Proxy proxy, boolean requiresTunnel, int connectTimeout) throws IOException {
+        /*
+         * Try an explicitly-specified proxy.
+         */
+        if (proxy != null) {
+            Address address = (proxy.type() == Proxy.Type.DIRECT)
+                    ? new Address(uri, sslSocketFactory)
+                    : new Address(uri, sslSocketFactory, proxy, requiresTunnel);
+            return HttpConnectionPool.INSTANCE.get(address, connectTimeout);
+        }
+
+        /*
+         * Try connecting to each of the proxies provided by the ProxySelector
+         * until a connection succeeds.
+         */
+        ProxySelector selector = ProxySelector.getDefault();
+        List<Proxy> proxyList = selector.select(uri);
+        if (proxyList != null) {
+            for (Proxy selectedProxy : proxyList) {
+                if (selectedProxy.type() == Proxy.Type.DIRECT) {
+                    // the same as NO_PROXY
+                    // TODO: if the selector recommends a direct connection, attempt that?
+                    continue;
+                }
+                try {
+                    Address address = new Address(uri, sslSocketFactory,
+                            selectedProxy, requiresTunnel);
+                    return HttpConnectionPool.INSTANCE.get(address, connectTimeout);
+                } catch (IOException e) {
+                    // failed to connect, tell it to the selector
+                    selector.connectFailed(uri, selectedProxy.address(), e);
+                }
+            }
+        }
+
+        /*
+         * Try a direct connection. If this fails, this method will throw.
+         */
+        return HttpConnectionPool.INSTANCE.get(new Address(uri, sslSocketFactory), connectTimeout);
+    }
+
+    public void closeSocketAndStreams() {
+        IoUtils.closeQuietly(sslOutputStream);
+        IoUtils.closeQuietly(sslInputStream);
+        IoUtils.closeQuietly(sslSocket);
+        IoUtils.closeQuietly(outputStream);
+        IoUtils.closeQuietly(inputStream);
+        IoUtils.closeQuietly(socket);
+    }
+
+    public void setSoTimeout(int readTimeout) throws SocketException {
+        socket.setSoTimeout(readTimeout);
+    }
+
+    Socket getSocket() {
+        return sslSocket != null ? sslSocket : socket;
+    }
+
+    public Address getAddress() {
+        return address;
+    }
+
+    /**
+     * Create an {@code SSLSocket} and perform the SSL handshake
+     * (performing certificate validation.
+     *
+     * @param sslSocketFactory Source of new {@code SSLSocket} instances.
+     * @param tlsTolerant If true, assume server can handle common
+     */
+    public SSLSocket setupSecureSocket(SSLSocketFactory sslSocketFactory,
+            HostnameVerifier hostnameVerifier, boolean tlsTolerant) throws IOException {
+        if (spdyConnection != null || sslOutputStream != null || sslInputStream != null) {
+            throw new IllegalStateException();
+        }
+
+        // Create the wrapper over connected socket.
+        sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket,
+                address.uriHost, address.uriPort, true /* autoClose */);
+        Libcore.makeTlsTolerant(sslSocket, address.socketHost, tlsTolerant);
+
+        if (tlsTolerant) {
+            Libcore.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
+        }
+
+        // Force handshake. This can throw!
+        sslSocket.startHandshake();
+
+        // Verify that the socket's certificates are acceptable for the target host.
+        if (!hostnameVerifier.verify(address.uriHost, sslSocket.getSession())) {
+            throw new IOException("Hostname '" + address.uriHost + "' was not verified");
+        }
+
+        // SSL success. Prepare to hand out Transport instances.
+        sslOutputStream = sslSocket.getOutputStream();
+        sslInputStream = sslSocket.getInputStream();
+
+        byte[] selectedProtocol;
+        if (tlsTolerant
+                && (selectedProtocol = Libcore.getNpnSelectedProtocol(sslSocket)) != null) {
+            if (Arrays.equals(selectedProtocol, SPDY2)) {
+                spdyConnection = new SpdyConnection.Builder(
+                        true, sslInputStream, sslOutputStream).build();
+                HttpConnectionPool.INSTANCE.share(this);
+            } else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
+                throw new IOException("Unexpected NPN transport "
+                        + new String(selectedProtocol, "ISO-8859-1"));
+            }
+        }
+
+        return sslSocket;
+    }
+
+    /**
+     * Return an {@code SSLSocket} if already connected, otherwise null.
+     */
+    public SSLSocket getSecureSocketIfConnected() {
+        return sslSocket;
+    }
+
+    /**
+     * Returns true if this connection has been used to satisfy an earlier
+     * HTTP request/response pair.
+     */
+    public boolean isRecycled() {
+        return recycled;
+    }
+
+    public void setRecycled() {
+        this.recycled = true;
+    }
+
+    /**
+     * Returns true if this connection is eligible to be reused for another
+     * request/response pair.
+     */
+    protected boolean isEligibleForRecycling() {
+        return !socket.isClosed()
+                && !socket.isInputShutdown()
+                && !socket.isOutputShutdown();
+    }
+
+    /**
+     * Returns the transport appropriate for this connection.
+     */
+    public Transport newTransport(HttpEngine httpEngine) throws IOException {
+        if (spdyConnection != null) {
+            return new SpdyTransport(httpEngine, spdyConnection);
+        } else if (sslSocket != null) {
+            return new HttpTransport(httpEngine, sslOutputStream, sslInputStream);
+        } else {
+            return new HttpTransport(httpEngine, outputStream, inputStream);
+        }
+    }
+
+    /**
+     * Returns true if this is a SPDY connection. Such connections can be used
+     * in multiple HTTP requests simultaneously.
+     */
+    public boolean isSpdy() {
+        return spdyConnection != null;
+    }
+
+    /**
+     * This address has two parts: the address we connect to directly and the
+     * origin address of the resource. These are the same unless a proxy is
+     * being used. It also includes the SSL socket factory so that a socket will
+     * not be reused if its SSL configuration is different.
+     */
+    public static final class Address {
+        private final Proxy proxy;
+        private final boolean requiresTunnel;
+        private final String uriHost;
+        private final int uriPort;
+        private final String socketHost;
+        private final int socketPort;
+        private final SSLSocketFactory sslSocketFactory;
+
+        public Address(URI uri, SSLSocketFactory sslSocketFactory) throws UnknownHostException {
+            this.proxy = null;
+            this.requiresTunnel = false;
+            this.uriHost = uri.getHost();
+            this.uriPort = Libcore.getEffectivePort(uri);
+            this.sslSocketFactory = sslSocketFactory;
+            this.socketHost = uriHost;
+            this.socketPort = uriPort;
+            if (uriHost == null) {
+                throw new UnknownHostException(uri.toString());
+            }
+        }
+
+        /**
+         * @param requiresTunnel true if the HTTP connection needs to tunnel one
+         *     protocol over another, such as when using HTTPS through an HTTP
+         *     proxy. When doing so, we must avoid buffering bytes intended for
+         *     the higher-level protocol.
+         */
+        public Address(URI uri, SSLSocketFactory sslSocketFactory,
+                Proxy proxy, boolean requiresTunnel) throws UnknownHostException {
+            this.proxy = proxy;
+            this.requiresTunnel = requiresTunnel;
+            this.uriHost = uri.getHost();
+            this.uriPort = Libcore.getEffectivePort(uri);
+            this.sslSocketFactory = sslSocketFactory;
+
+            SocketAddress proxyAddress = proxy.address();
+            if (!(proxyAddress instanceof InetSocketAddress)) {
+                throw new IllegalArgumentException("Proxy.address() is not an InetSocketAddress: "
+                        + proxyAddress.getClass());
+            }
+            InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
+            this.socketHost = proxySocketAddress.getHostName();
+            this.socketPort = proxySocketAddress.getPort();
+            if (uriHost == null) {
+                throw new UnknownHostException(uri.toString());
+            }
+        }
+
+        public Proxy getProxy() {
+            return proxy;
+        }
+
+        @Override public boolean equals(Object other) {
+            if (other instanceof Address) {
+                Address that = (Address) other;
+                return Objects.equal(this.proxy, that.proxy)
+                        && this.uriHost.equals(that.uriHost)
+                        && this.uriPort == that.uriPort
+                        && Objects.equal(this.sslSocketFactory, that.sslSocketFactory)
+                        && this.requiresTunnel == that.requiresTunnel;
+            }
+            return false;
+        }
+
+        @Override public int hashCode() {
+            int result = 17;
+            result = 31 * result + uriHost.hashCode();
+            result = 31 * result + uriPort;
+            result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
+            result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
+            result = 31 * result + (requiresTunnel ? 1 : 0);
+            return result;
+        }
+
+        public HttpConnection connect(int connectTimeout) throws IOException {
+            return new HttpConnection(this, connectTimeout);
+        }
+    }
+}
diff --git a/src/main/java/libcore/net/http/HttpConnectionPool.java b/src/main/java/libcore/net/http/HttpConnectionPool.java
new file mode 100644
index 0000000..490c98a
--- /dev/null
+++ b/src/main/java/libcore/net/http/HttpConnectionPool.java
@@ -0,0 +1,152 @@
+/*
+ *  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 libcore.net.http;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import libcore.util.Libcore;
+
+/**
+ * A pool of HTTP and SPDY connections. This class exposes its tuning parameters
+ * as system properties:
+ * <ul>
+ *   <li>{@code http.keepAlive} true if HTTP and SPDY connections should be
+ *       pooled at all. Default is true.
+ *   <li>{@code http.maxConnections} maximum number of connections to each host.
+ *       Default is 5.
+ * </ul>
+ *
+ * <p>This class <i>doesn't</i> adjust its configuration as system properties
+ * are changed. This assumes that the applications that set these parameters do
+ * so before making HTTP connections, and that this class is initialized lazily.
+ */
+final class HttpConnectionPool {
+    public static final HttpConnectionPool INSTANCE = new HttpConnectionPool();
+
+    private final int maxConnections;
+    private final HashMap<HttpConnection.Address, List<HttpConnection>> connectionPool
+            = new HashMap<HttpConnection.Address, List<HttpConnection>>();
+
+    private HttpConnectionPool() {
+        String keepAlive = System.getProperty("http.keepAlive");
+        if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
+            maxConnections = 0;
+            return;
+        }
+
+        String maxConnectionsString = System.getProperty("http.maxConnections");
+        this.maxConnections = maxConnectionsString != null
+                ? Integer.parseInt(maxConnectionsString)
+                : 5;
+    }
+
+    public HttpConnection get(HttpConnection.Address address, int connectTimeout)
+            throws IOException {
+        // First try to reuse an existing HTTP connection.
+        synchronized (connectionPool) {
+            List<HttpConnection> connections = connectionPool.get(address);
+            while (connections != null) {
+                HttpConnection connection = connections.get(connections.size() - 1);
+                if (!connection.isSpdy()) {
+                    connections.remove(connections.size() - 1);
+                }
+                if (connections.isEmpty()) {
+                    connectionPool.remove(address);
+                    connections = null;
+                }
+                if (connection.isEligibleForRecycling()) {
+                    // Since Socket is recycled, re-tag before using
+                    Socket socket = connection.getSocket();
+                    Libcore.tagSocket(socket);
+                    return connection;
+                }
+            }
+        }
+
+        /*
+         * We couldn't find a reusable connection, so we need to create a new
+         * connection. We're careful not to do so while holding a lock!
+         */
+        return address.connect(connectTimeout);
+    }
+
+    /**
+     * Gives the HTTP/HTTPS connection to the pool. It is an error to use {@code
+     * connection} after calling this method.
+     */
+    public void recycle(HttpConnection connection) {
+        if (connection.isSpdy()) {
+            throw new IllegalArgumentException();
+        }
+
+        Socket socket = connection.getSocket();
+        try {
+            Libcore.untagSocket(socket);
+        } catch (SocketException e) {
+            // When unable to remove tagging, skip recycling and close
+            Libcore.logW("Unable to untagSocket(): " + e);
+            connection.closeSocketAndStreams();
+            return;
+        }
+
+        if (maxConnections > 0 && connection.isEligibleForRecycling()) {
+            HttpConnection.Address address = connection.getAddress();
+            synchronized (connectionPool) {
+                List<HttpConnection> connections = connectionPool.get(address);
+                if (connections == null) {
+                    connections = new ArrayList<HttpConnection>();
+                    connectionPool.put(address, connections);
+                }
+                if (connections.size() < maxConnections) {
+                    connection.setRecycled();
+                    connections.add(connection);
+                    return; // keep the connection open
+                }
+            }
+        }
+
+        // don't close streams while holding a lock!
+        connection.closeSocketAndStreams();
+    }
+
+    /**
+     * Shares the SPDY connection with the pool. Callers to this method may
+     * continue to use {@code connection}.
+     */
+    public void share(HttpConnection connection) {
+        if (!connection.isSpdy()) {
+            throw new IllegalArgumentException();
+        }
+        if (maxConnections <= 0 || !connection.isEligibleForRecycling()) {
+            return;
+        }
+        HttpConnection.Address address = connection.getAddress();
+        synchronized (connectionPool) {
+            List<HttpConnection> connections = connectionPool.get(address);
+            if (connections == null) {
+                connections = new ArrayList<HttpConnection>(1);
+                connections.add(connection);
+                connectionPool.put(address, connections);
+            }
+        }
+    }
+}
diff --git a/src/main/java/libcore/net/http/HttpDate.java b/src/main/java/libcore/net/http/HttpDate.java
new file mode 100644
index 0000000..41ae5ef
--- /dev/null
+++ b/src/main/java/libcore/net/http/HttpDate.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.http;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Best-effort parser for HTTP dates.
+ */
+public final class HttpDate {
+
+    /**
+     * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such
+     * cookies are on the fast path.
+     */
+    private static final ThreadLocal<DateFormat> STANDARD_DATE_FORMAT
+            = new ThreadLocal<DateFormat>() {
+        @Override protected DateFormat initialValue() {
+            DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
+            rfc1123.setTimeZone(TimeZone.getTimeZone("UTC"));
+            return rfc1123;
+        }
+    };
+
+    /**
+     * If we fail to parse a date in a non-standard format, try each of these formats in sequence.
+     */
+    private static final String[] BROWSER_COMPATIBLE_DATE_FORMATS = new String[] {
+            /* This list comes from  {@code org.apache.http.impl.cookie.BrowserCompatSpec}. */
+            "EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 1036
+            "EEE MMM d HH:mm:ss yyyy", // ANSI C asctime()
+            "EEE, dd-MMM-yyyy HH:mm:ss z",
+            "EEE, dd-MMM-yyyy HH-mm-ss z",
+            "EEE, dd MMM yy HH:mm:ss z",
+            "EEE dd-MMM-yyyy HH:mm:ss z",
+            "EEE dd MMM yyyy HH:mm:ss z",
+            "EEE dd-MMM-yyyy HH-mm-ss z",
+            "EEE dd-MMM-yy HH:mm:ss z",
+            "EEE dd MMM yy HH:mm:ss z",
+            "EEE,dd-MMM-yy HH:mm:ss z",
+            "EEE,dd-MMM-yyyy HH:mm:ss z",
+            "EEE, dd-MM-yyyy HH:mm:ss z",
+
+            /* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */
+            "EEE MMM d yyyy HH:mm:ss z",
+    };
+
+    /**
+     * Returns the date for {@code value}. Returns null if the value couldn't be
+     * parsed.
+     */
+    public static Date parse(String value) {
+        try {
+            return STANDARD_DATE_FORMAT.get().parse(value);
+        } catch (ParseException ignore) {
+        }
+        for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) {
+            try {
+                return new SimpleDateFormat(formatString, Locale.US).parse(value);
+            } catch (ParseException ignore) {
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the string for {@code value}.
+     */
+    public static String format(Date value) {
+        return STANDARD_DATE_FORMAT.get().format(value);
+    }
+
+    private HttpDate() {
+    }
+}
diff --git a/src/main/java/libcore/net/http/HttpEngine.java b/src/main/java/libcore/net/http/HttpEngine.java
new file mode 100644
index 0000000..9acb72f
--- /dev/null
+++ b/src/main/java/libcore/net/http/HttpEngine.java
@@ -0,0 +1,640 @@
+/*
+ *  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 libcore.net.http;
+
+import com.squareup.okhttp.OkHttpConnection;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.CookieHandler;
+import java.net.Proxy;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.GZIPInputStream;
+import javax.net.ssl.SSLSocketFactory;
+import libcore.io.IoUtils;
+import libcore.util.EmptyArray;
+import libcore.util.ExtendedResponseCache;
+import libcore.util.Libcore;
+import libcore.util.ResponseSource;
+
+/**
+ * Handles a single HTTP request/response pair. Each HTTP engine follows this
+ * lifecycle:
+ * <ol>
+ *     <li>It is created.
+ *     <li>The HTTP request message is sent with sendRequest(). Once the request
+ *         is sent it is an error to modify the request headers. After
+ *         sendRequest() has been called the request body can be written to if
+ *         it exists.
+ *     <li>The HTTP response message is read with readResponse(). After the
+ *         response has been read the response headers and body can be read.
+ *         All responses have a response body input stream, though in some
+ *         instances this stream is empty.
+ * </ol>
+ *
+ * <p>The request and response may be served by the HTTP response cache, by the
+ * network, or by both in the event of a conditional GET.
+ *
+ * <p>This class may hold a socket connection that needs to be released or
+ * recycled. By default, this socket connection is held when the last byte of
+ * the response is consumed. To release the connection when it is no longer
+ * required, use {@link #automaticallyReleaseConnectionToPool()}.
+ */
+public class HttpEngine {
+    private static final CacheResponse BAD_GATEWAY_RESPONSE = new CacheResponse() {
+        @Override public Map<String, List<String>> getHeaders() throws IOException {
+            Map<String, List<String>> result = new HashMap<String, List<String>>();
+            result.put(null, Collections.singletonList("HTTP/1.1 502 Bad Gateway"));
+            return result;
+        }
+        @Override public InputStream getBody() throws IOException {
+            return new ByteArrayInputStream(EmptyArray.BYTE);
+        }
+    };
+    public static final int DEFAULT_CHUNK_LENGTH = 1024;
+
+    public static final String OPTIONS = "OPTIONS";
+    public static final String GET = "GET";
+    public static final String HEAD = "HEAD";
+    public static final String POST = "POST";
+    public static final String PUT = "PUT";
+    public static final String DELETE = "DELETE";
+    public static final String TRACE = "TRACE";
+    public static final String CONNECT = "CONNECT";
+
+    public static final int HTTP_CONTINUE = 100;
+
+    protected final HttpURLConnectionImpl policy;
+
+    protected final String method;
+
+    private ResponseSource responseSource;
+
+    protected HttpConnection connection;
+    private OutputStream requestBodyOut;
+
+    private Transport transport;
+
+    private InputStream responseBodyIn;
+
+    private final ResponseCache responseCache = ResponseCache.getDefault();
+    private CacheResponse cacheResponse;
+    private CacheRequest cacheRequest;
+
+    /** The time when the request headers were written, or -1 if they haven't been written yet. */
+    long sentRequestMillis = -1;
+
+    /**
+     * True if this client added an "Accept-Encoding: gzip" header field and is
+     * therefore responsible for also decompressing the transfer stream.
+     */
+    private boolean transparentGzip;
+
+    final URI uri;
+
+    final RequestHeaders requestHeaders;
+
+    /** Null until a response is received from the network or the cache. */
+    ResponseHeaders responseHeaders;
+
+    /*
+     * The cache response currently being validated on a conditional get. Null
+     * if the cached response doesn't exist or doesn't need validation. If the
+     * conditional get succeeds, these will be used for the response headers and
+     * body. If it fails, these be closed and set to null.
+     */
+    private ResponseHeaders cachedResponseHeaders;
+    private InputStream cachedResponseBody;
+
+    /**
+     * True if the socket connection should be released to the connection pool
+     * when the response has been fully read.
+     */
+    private boolean automaticallyReleaseConnectionToPool;
+
+    /** True if the socket connection is no longer needed by this engine. */
+    private boolean connectionReleased;
+
+    /**
+     * @param requestHeaders the client's supplied request headers. This class
+     *     creates a private copy that it can mutate.
+     * @param connection the connection used for an intermediate response
+     *     immediately prior to this request/response pair, such as a same-host
+     *     redirect. This engine assumes ownership of the connection and must
+     *     release it when it is unneeded.
+     */
+    public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
+            HttpConnection connection, RetryableOutputStream requestBodyOut) throws IOException {
+        this.policy = policy;
+        this.method = method;
+        this.connection = connection;
+        this.requestBodyOut = requestBodyOut;
+
+        try {
+            uri = Libcore.toUriLenient(policy.getURL());
+        } catch (URISyntaxException e) {
+            throw new IOException(e);
+        }
+
+        this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
+    }
+
+    public URI getUri() {
+        return uri;
+    }
+
+    /**
+     * Figures out what the response source will be, and opens a socket to that
+     * source if necessary. Prepares the request headers and gets ready to start
+     * writing the request body if it exists.
+     */
+    public final void sendRequest() throws IOException {
+        if (responseSource != null) {
+            return;
+        }
+
+        prepareRawRequestHeaders();
+        initResponseSource();
+        if (responseCache instanceof ExtendedResponseCache) {
+            ((ExtendedResponseCache) responseCache).trackResponse(responseSource);
+        }
+
+        /*
+         * The raw response source may require the network, but the request
+         * headers may forbid network use. In that case, dispose of the network
+         * response and use a BAD_GATEWAY response instead.
+         */
+        if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) {
+            if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
+                IoUtils.closeQuietly(cachedResponseBody);
+            }
+            this.responseSource = ResponseSource.CACHE;
+            this.cacheResponse = BAD_GATEWAY_RESPONSE;
+            RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(cacheResponse.getHeaders());
+            setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody());
+        }
+
+        if (responseSource.requiresConnection()) {
+            sendSocketRequest();
+        } else if (connection != null) {
+            HttpConnectionPool.INSTANCE.recycle(connection);
+            connection = null;
+        }
+    }
+
+    /**
+     * Initialize the source for this response. It may be corrected later if the
+     * request headers forbids network use.
+     */
+    private void initResponseSource() throws IOException {
+        responseSource = ResponseSource.NETWORK;
+        if (!policy.getUseCaches() || responseCache == null) {
+            return;
+        }
+
+        CacheResponse candidate = responseCache.get(uri, method,
+                requestHeaders.getHeaders().toMultimap());
+        if (candidate == null) {
+            return;
+        }
+
+        Map<String, List<String>> responseHeadersMap = candidate.getHeaders();
+        cachedResponseBody = candidate.getBody();
+        if (!acceptCacheResponseType(candidate)
+                || responseHeadersMap == null
+                || cachedResponseBody == null) {
+            IoUtils.closeQuietly(cachedResponseBody);
+            return;
+        }
+
+        RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap);
+        cachedResponseHeaders = new ResponseHeaders(uri, rawResponseHeaders);
+        long now = System.currentTimeMillis();
+        this.responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders);
+        if (responseSource == ResponseSource.CACHE) {
+            this.cacheResponse = candidate;
+            setResponse(cachedResponseHeaders, cachedResponseBody);
+        } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
+            this.cacheResponse = candidate;
+        } else if (responseSource == ResponseSource.NETWORK) {
+            IoUtils.closeQuietly(cachedResponseBody);
+        } else {
+            throw new AssertionError();
+        }
+    }
+
+    private void sendSocketRequest() throws IOException {
+        if (connection == null) {
+            connect();
+        }
+
+        if (transport != null) {
+            throw new IllegalStateException();
+        }
+
+        transport = connection.newTransport(this);
+
+        if (hasRequestBody() && requestBodyOut == null) {
+            // Create a request body if we don't have one already. We'll already
+            // have one if we're retrying a failed POST.
+            requestBodyOut = transport.createRequestBody();
+        }
+    }
+
+    /**
+     * Connect to the origin server either directly or via a proxy.
+     */
+    protected void connect() throws IOException {
+        if (connection == null) {
+            connection = openSocketConnection();
+        }
+    }
+
+    protected final HttpConnection openSocketConnection() throws IOException {
+        HttpConnection result = HttpConnection.connect(uri, getSslSocketFactory(),
+                policy.getProxy(), requiresTunnel(), policy.getConnectTimeout());
+        Proxy proxy = result.getAddress().getProxy();
+        if (proxy != null) {
+            policy.setProxy(proxy);
+            // Add the authority to the request line when we're using a proxy.
+            requestHeaders.getHeaders().setStatusLine(getRequestLine());
+        }
+        result.setSoTimeout(policy.getReadTimeout());
+        return result;
+    }
+
+    /**
+     * @param body the response body, or null if it doesn't exist or isn't
+     *     available.
+     */
+    private void setResponse(ResponseHeaders headers, InputStream body) throws IOException {
+        if (this.responseBodyIn != null) {
+            throw new IllegalStateException();
+        }
+        this.responseHeaders = headers;
+        if (body != null) {
+            initContentStream(body);
+        }
+    }
+
+    boolean hasRequestBody() {
+        return method == POST || method == PUT;
+    }
+
+    /**
+     * Returns the request body or null if this request doesn't have a body.
+     */
+    public final OutputStream getRequestBody() {
+        if (responseSource == null) {
+            throw new IllegalStateException();
+        }
+        return requestBodyOut;
+    }
+
+    public final boolean hasResponse() {
+        return responseHeaders != null;
+    }
+
+    public final RequestHeaders getRequestHeaders() {
+        return requestHeaders;
+    }
+
+    public final ResponseHeaders getResponseHeaders() {
+        if (responseHeaders == null) {
+            throw new IllegalStateException();
+        }
+        return responseHeaders;
+    }
+
+    public final int getResponseCode() {
+        if (responseHeaders == null) {
+            throw new IllegalStateException();
+        }
+        return responseHeaders.getHeaders().getResponseCode();
+    }
+
+    public final InputStream getResponseBody() {
+        if (responseHeaders == null) {
+            throw new IllegalStateException();
+        }
+        return responseBodyIn;
+    }
+
+    public final CacheResponse getCacheResponse() {
+        return cacheResponse;
+    }
+
+    public final HttpConnection getConnection() {
+        return connection;
+    }
+
+    public final boolean hasRecycledConnection() {
+        return connection != null && connection.isRecycled();
+    }
+
+    /**
+     * Returns true if {@code cacheResponse} is of the right type. This
+     * condition is necessary but not sufficient for the cached response to
+     * be used.
+     */
+    protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
+        return true;
+    }
+
+    private void maybeCache() throws IOException {
+        // Are we caching at all?
+        if (!policy.getUseCaches() || responseCache == null) {
+            return;
+        }
+
+        // Should we cache this response for this request?
+        if (!responseHeaders.isCacheable(requestHeaders)) {
+            return;
+        }
+
+        // Offer this request to the cache.
+        cacheRequest = responseCache.put(uri, getHttpConnectionToCache());
+    }
+
+    protected OkHttpConnection getHttpConnectionToCache() {
+        return policy;
+    }
+
+    /**
+     * Cause the socket connection to be released to the connection pool when
+     * it is no longer needed. If it is already unneeded, it will be pooled
+     * immediately. Otherwise the connection is held so that redirects can be
+     * handled by the same connection.
+     */
+    public final void automaticallyReleaseConnectionToPool() {
+        automaticallyReleaseConnectionToPool = true;
+        if (connection != null && connectionReleased) {
+            HttpConnectionPool.INSTANCE.recycle(connection);
+            connection = null;
+        }
+    }
+
+    /**
+     * Releases this engine so that its resources may be either reused or
+     * closed. Also call {@link #automaticallyReleaseConnectionToPool} unless
+     * the connection will be used to follow a redirect.
+     */
+    public final void release(boolean reusable) {
+        // If the response body comes from the cache, close it.
+        if (responseBodyIn == cachedResponseBody) {
+            IoUtils.closeQuietly(responseBodyIn);
+        }
+
+        if (!connectionReleased && connection != null) {
+            connectionReleased = true;
+
+            if (!reusable || !transport.makeReusable(requestBodyOut, responseBodyIn)) {
+                connection.closeSocketAndStreams();
+                connection = null;
+            } else if (automaticallyReleaseConnectionToPool) {
+                HttpConnectionPool.INSTANCE.recycle(connection);
+                connection = null;
+            }
+        }
+    }
+
+    private void initContentStream(InputStream transferStream) throws IOException {
+        if (transparentGzip && responseHeaders.isContentEncodingGzip()) {
+            /*
+             * If the response was transparently gzipped, remove the gzip header field
+             * so clients don't double decompress. http://b/3009828
+             */
+            responseHeaders.stripContentEncoding();
+            responseBodyIn = new GZIPInputStream(transferStream);
+        } else {
+            responseBodyIn = transferStream;
+        }
+    }
+
+    /**
+     * Returns true if the response must have a (possibly 0-length) body.
+     * See RFC 2616 section 4.3.
+     */
+    public final boolean hasResponseBody() {
+        int responseCode = responseHeaders.getHeaders().getResponseCode();
+
+        // HEAD requests never yield a body regardless of the response headers.
+        if (method == HEAD) {
+            return false;
+        }
+
+        if (method != CONNECT
+                && (responseCode < HTTP_CONTINUE || responseCode >= 200)
+                && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT
+                && responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) {
+            return true;
+        }
+
+        /*
+         * If the Content-Length or Transfer-Encoding headers disagree with the
+         * response code, the response is malformed. For best compatibility, we
+         * honor the headers.
+         */
+        if (responseHeaders.getContentLength() != -1 || responseHeaders.isChunked()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Populates requestHeaders with defaults and cookies.
+     *
+     * <p>This client doesn't specify a default {@code Accept} header because it
+     * doesn't know what content types the application is interested in.
+     */
+    private void prepareRawRequestHeaders() throws IOException {
+        requestHeaders.getHeaders().setStatusLine(getRequestLine());
+
+        if (requestHeaders.getUserAgent() == null) {
+            requestHeaders.setUserAgent(getDefaultUserAgent());
+        }
+
+        if (requestHeaders.getHost() == null) {
+            requestHeaders.setHost(getOriginAddress(policy.getURL()));
+        }
+
+        // TODO: this shouldn't be set for SPDY (it's ignored)
+        if ((connection == null || connection.httpMinorVersion != 0)
+                && requestHeaders.getConnection() == null) {
+            requestHeaders.setConnection("Keep-Alive");
+        }
+
+        if (requestHeaders.getAcceptEncoding() == null) {
+            transparentGzip = true;
+            // TODO: this shouldn't be set for SPDY (it isn't necessary)
+            requestHeaders.setAcceptEncoding("gzip");
+        }
+
+        if (hasRequestBody() && requestHeaders.getContentType() == null) {
+            requestHeaders.setContentType("application/x-www-form-urlencoded");
+        }
+
+        long ifModifiedSince = policy.getIfModifiedSince();
+        if (ifModifiedSince != 0) {
+            requestHeaders.setIfModifiedSince(new Date(ifModifiedSince));
+        }
+
+        CookieHandler cookieHandler = CookieHandler.getDefault();
+        if (cookieHandler != null) {
+            requestHeaders.addCookies(
+                    cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap()));
+        }
+    }
+
+    /**
+     * Returns the request status line, like "GET / HTTP/1.1". This is exposed
+     * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so
+     * it needs to be set even if the transport is SPDY.
+     */
+    String getRequestLine() {
+        String protocol = (connection == null || connection.httpMinorVersion != 0)
+                ? "HTTP/1.1"
+                : "HTTP/1.0";
+        return method + " " + requestString() + " " + protocol;
+    }
+
+    private String requestString() {
+        URL url = policy.getURL();
+        if (includeAuthorityInRequestLine()) {
+            return url.toString();
+        } else {
+            String fileOnly = url.getFile();
+            if (fileOnly == null) {
+                fileOnly = "/";
+            } else if (!fileOnly.startsWith("/")) {
+                fileOnly = "/" + fileOnly;
+            }
+            return fileOnly;
+        }
+    }
+
+    /**
+     * Returns true if the request line should contain the full URL with host
+     * and port (like "GET http://android.com/foo HTTP/1.1") or only the path
+     * (like "GET /foo HTTP/1.1").
+     *
+     * <p>This is non-final because for HTTPS it's never necessary to supply the
+     * full URL, even if a proxy is in use.
+     */
+    protected boolean includeAuthorityInRequestLine() {
+        return policy.usingProxy();
+    }
+
+    /**
+     * Returns the SSL configuration for connections created by this engine.
+     * We cannot reuse HTTPS connections if the socket factory has changed.
+     */
+    protected SSLSocketFactory getSslSocketFactory() {
+        return null;
+    }
+
+    protected final String getDefaultUserAgent() {
+        String agent = System.getProperty("http.agent");
+        return agent != null ? agent : ("Java" + System.getProperty("java.version"));
+    }
+
+    protected final String getOriginAddress(URL url) {
+        int port = url.getPort();
+        String result = url.getHost();
+        if (port > 0 && port != policy.getDefaultPort()) {
+            result = result + ":" + port;
+        }
+        return result;
+    }
+
+    protected boolean requiresTunnel() {
+        return false;
+    }
+
+    /**
+     * Flushes the remaining request header and body, parses the HTTP response
+     * headers and starts reading the HTTP response body if it exists.
+     */
+    public final void readResponse() throws IOException {
+        if (hasResponse()) {
+            return;
+        }
+
+        if (responseSource == null) {
+            throw new IllegalStateException("readResponse() without sendRequest()");
+        }
+
+        if (!responseSource.requiresConnection()) {
+            return;
+        }
+
+        if (sentRequestMillis == -1) {
+            if (requestBodyOut instanceof RetryableOutputStream) {
+                int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength();
+                requestHeaders.setContentLength(contentLength);
+            }
+            transport.writeRequestHeaders();
+        }
+
+        if (requestBodyOut != null) {
+            requestBodyOut.close();
+            if (requestBodyOut instanceof RetryableOutputStream) {
+                transport.writeRequestBody((RetryableOutputStream) requestBodyOut);
+            }
+        }
+
+        transport.flushRequest();
+
+        responseHeaders = transport.readResponseHeaders();
+        responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
+
+        if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
+            if (cachedResponseHeaders.validate(responseHeaders)) {
+                release(true);
+                ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
+                setResponse(combinedHeaders, cachedResponseBody);
+                if (responseCache instanceof ExtendedResponseCache) {
+                    ExtendedResponseCache httpResponseCache = (ExtendedResponseCache) responseCache;
+                    httpResponseCache.trackConditionalCacheHit();
+                    httpResponseCache.update(cacheResponse, getHttpConnectionToCache());
+                }
+                return;
+            } else {
+                IoUtils.closeQuietly(cachedResponseBody);
+            }
+        }
+
+        if (hasResponseBody()) {
+            maybeCache(); // reentrant. this calls into user code which may call back into this!
+        }
+
+        initContentStream(transport.getTransferStream(cacheRequest));
+    }
+}
diff --git a/src/main/java/libcore/net/http/HttpResponseCache.java b/src/main/java/libcore/net/http/HttpResponseCache.java
new file mode 100644
index 0000000..13d540e
--- /dev/null
+++ b/src/main/java/libcore/net/http/HttpResponseCache.java
@@ -0,0 +1,599 @@
+/*
+ * 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 libcore.net.http;
+
+import com.squareup.okhttp.OkHttpConnection;
+import com.squareup.okhttp.OkHttpsConnection;
+import java.io.BufferedInputStream;
+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.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.SSLPeerUnverifiedException;
+import libcore.io.Base64;
+import libcore.io.DiskLruCache;
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+import libcore.util.Charsets;
+import libcore.util.ExtendedResponseCache;
+import libcore.util.IntegralToString;
+import libcore.util.ResponseSource;
+
+/**
+ * Cache responses in a directory on the file system. Most clients should use
+ * {@code android.net.HttpResponseCache}, the stable, documented front end for
+ * this.
+ */
+public final class HttpResponseCache extends ResponseCache implements ExtendedResponseCache {
+    // 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;
+
+    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 IntegralToString.bytesToHexString(md5bytes, false);
+        } catch (NoSuchAlgorithmException e) {
+            throw new AssertionError(e);
+        } catch (UnsupportedEncodingException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    @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(new BufferedInputStream(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 OkHttpConnection)) {
+            return null;
+        }
+
+        OkHttpConnection httpConnection = (OkHttpConnection) urlConnection;
+        String requestMethod = httpConnection.getRequestMethod();
+        String key = uriToKey(uri);
+
+        if (requestMethod.equals(HttpEngine.POST)
+                || requestMethod.equals(HttpEngine.PUT)
+                || requestMethod.equals(HttpEngine.DELETE)) {
+            try {
+                cache.remove(key);
+            } catch (IOException ignored) {
+                // The cache cannot be written.
+            }
+            return null;
+        } else if (!requestMethod.equals(HttpEngine.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;
+        }
+    }
+
+    /**
+     * Handles a conditional request hit by updating the stored cache response
+     * with the headers from {@code httpConnection}. The cached response body is
+     * not updated. If the stored response has changed since {@code
+     * conditionalCacheHit} was returned, this does nothing.
+     */
+    @Override
+    public void update(CacheResponse conditionalCacheHit, OkHttpConnection httpConnection) {
+        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;
+        }
+    }
+
+    public DiskLruCache getCache() {
+        return cache;
+    }
+
+    public synchronized int getWriteAbortCount() {
+        return writeAbortCount;
+    }
+
+    public synchronized int getWriteSuccessCount() {
+        return writeSuccessCount;
+    }
+
+    public synchronized void trackResponse(ResponseSource source) {
+        requestCount++;
+
+        switch (source) {
+        case CACHE:
+            hitCount++;
+            break;
+        case CONDITIONAL_CACHE:
+        case NETWORK:
+            networkCount++;
+            break;
+        }
+    }
+
+    public 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 abort() {
+            synchronized (HttpResponseCache.this) {
+                if (done) {
+                    return;
+                }
+                done = true;
+                writeAbortCount++;
+            }
+            IoUtils.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:
+         *   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
+         *
+         * A typical HTTPS file looks like this:
+         *   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
+         *
+         * 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.
+         *
+         * Next is the response status line, followed by the number of HTTP
+         * response header lines, followed by those lines.
+         *
+         * 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 {
+                uri = Streams.readAsciiLine(in);
+                requestMethod = Streams.readAsciiLine(in);
+                varyHeaders = new RawHeaders();
+                int varyRequestHeaderLineCount = readInt(in);
+                for (int i = 0; i < varyRequestHeaderLineCount; i++) {
+                    varyHeaders.addLine(Streams.readAsciiLine(in));
+                }
+
+                responseHeaders = new RawHeaders();
+                responseHeaders.setStatusLine(Streams.readAsciiLine(in));
+                int responseHeaderLineCount = readInt(in);
+                for (int i = 0; i < responseHeaderLineCount; i++) {
+                    responseHeaders.addLine(Streams.readAsciiLine(in));
+                }
+
+                if (isHttps()) {
+                    String blank = Streams.readAsciiLine(in);
+                    if (blank.length() != 0) {
+                        throw new IOException("expected \"\" but was \"" + blank + "\"");
+                    }
+                    cipherSuite = Streams.readAsciiLine(in);
+                    peerCertificates = readCertArray(in);
+                    localCertificates = readCertArray(in);
+                } else {
+                    cipherSuite = null;
+                    peerCertificates = null;
+                    localCertificates = null;
+                }
+            } finally {
+                in.close();
+            }
+        }
+
+        public Entry(URI uri, RawHeaders varyHeaders, OkHttpConnection httpConnection) {
+            this.uri = uri.toString();
+            this.varyHeaders = varyHeaders;
+            this.requestMethod = httpConnection.getRequestMethod();
+            this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields());
+
+            if (isHttps()) {
+                OkHttpsConnection httpsConnection
+                        = (OkHttpsConnection) 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(0);
+            Writer writer = new BufferedWriter(new OutputStreamWriter(out, Charsets.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 int readInt(InputStream in) throws IOException {
+            String intString = Streams.readAsciiLine(in);
+            try {
+                return Integer.parseInt(intString);
+            } catch (NumberFormatException e) {
+                throw new IOException("expected an int but was \"" + intString + "\"");
+            }
+        }
+
+        private Certificate[] readCertArray(InputStream in) throws IOException {
+            int length = readInt(in);
+            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 = Streams.readAsciiLine(in);
+                    byte[] bytes = Base64.decode(line.getBytes("US-ASCII"));
+                    result[i] = certificateFactory.generateCertificate(
+                            new ByteArrayInputStream(bytes));
+                }
+                return result;
+            } catch (CertificateException e) {
+                throw new IOException(e);
+            }
+        }
+
+        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);
+            }
+        }
+
+        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(), 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();
+        }
+
+        @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();
+        }
+
+        @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();
+        }
+    }
+}
diff --git a/src/main/java/libcore/net/http/HttpTransport.java b/src/main/java/libcore/net/http/HttpTransport.java
new file mode 100644
index 0000000..d307349
--- /dev/null
+++ b/src/main/java/libcore/net/http/HttpTransport.java
@@ -0,0 +1,595 @@
+/*
+ * Copyright (C) 2012 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 libcore.net.http;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheRequest;
+import java.net.CookieHandler;
+import java.net.URL;
+import libcore.io.Streams;
+import libcore.util.Libcore;
+
+final class HttpTransport implements Transport {
+    /**
+     * The maximum number of bytes to buffer when sending headers and a request
+     * body. When the headers and body can be sent in a single write, the
+     * request completes sooner. In one WiFi benchmark, using a large enough
+     * buffer sped up some uploads by half.
+     */
+    private static final int MAX_REQUEST_BUFFER_LENGTH = 32768;
+
+    private final HttpEngine httpEngine;
+    private final InputStream socketIn;
+    private final OutputStream socketOut;
+
+    /**
+     * This stream buffers the request headers and the request body when their
+     * combined size is less than MAX_REQUEST_BUFFER_LENGTH. By combining them
+     * we can save socket writes, which in turn saves a packet transmission.
+     * This is socketOut if the request size is large or unknown.
+     */
+    private OutputStream requestOut;
+
+    public HttpTransport(HttpEngine httpEngine,
+            OutputStream outputStream, InputStream inputStream) {
+        this.httpEngine = httpEngine;
+        this.socketOut = outputStream;
+        this.requestOut = outputStream;
+        this.socketIn = inputStream;
+    }
+
+    @Override public OutputStream createRequestBody() throws IOException {
+        boolean chunked = httpEngine.requestHeaders.isChunked();
+        if (!chunked
+                && httpEngine.policy.getChunkLength() > 0
+                && httpEngine.connection.httpMinorVersion != 0) {
+            httpEngine.requestHeaders.setChunked();
+            chunked = true;
+        }
+
+        // Stream a request body of unknown length.
+        if (chunked) {
+            int chunkLength = httpEngine.policy.getChunkLength();
+            if (chunkLength == -1) {
+                chunkLength = HttpEngine.DEFAULT_CHUNK_LENGTH;
+            }
+            writeRequestHeaders();
+            return new ChunkedOutputStream(requestOut, chunkLength);
+        }
+
+        // Stream a request body of a known length.
+        int fixedContentLength = httpEngine.policy.getFixedContentLength();
+        if (fixedContentLength != -1) {
+            httpEngine.requestHeaders.setContentLength(fixedContentLength);
+            writeRequestHeaders();
+            return new FixedLengthOutputStream(requestOut, fixedContentLength);
+        }
+
+        // Buffer a request body of a known length.
+        int contentLength = httpEngine.requestHeaders.getContentLength();
+        if (contentLength != -1) {
+            writeRequestHeaders();
+            return new RetryableOutputStream(contentLength);
+        }
+
+        // Buffer a request body of an unknown length. Don't write request
+        // headers until the entire body is ready; otherwise we can't set the
+        // Content-Length header correctly.
+        return new RetryableOutputStream();
+    }
+
+    @Override public void flushRequest() throws IOException {
+        requestOut.flush();
+        requestOut = socketOut;
+    }
+
+    @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
+        requestBody.writeToSocket(requestOut);
+    }
+
+    /**
+     * Prepares the HTTP headers and sends them to the server.
+     *
+     * <p>For streaming requests with a body, headers must be prepared
+     * <strong>before</strong> the output stream has been written to. Otherwise
+     * the body would need to be buffered!
+     *
+     * <p>For non-streaming requests with a body, headers must be prepared
+     * <strong>after</strong> the output stream has been written to and closed.
+     * This ensures that the {@code Content-Length} header field receives the
+     * proper value.
+     */
+    public void writeRequestHeaders() throws IOException {
+        if (httpEngine.sentRequestMillis != -1) {
+            throw new IllegalStateException();
+        }
+        httpEngine.sentRequestMillis = System.currentTimeMillis();
+
+        int contentLength = httpEngine.requestHeaders.getContentLength();
+        RawHeaders headersToSend = getNetworkRequestHeaders();
+        byte[] bytes = headersToSend.toHeaderString().getBytes("ISO-8859-1");
+
+        if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) {
+            requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength);
+        }
+
+        requestOut.write(bytes);
+    }
+
+    private RawHeaders getNetworkRequestHeaders() {
+        return httpEngine.method == HttpEngine.CONNECT
+                ? getTunnelNetworkRequestHeaders()
+                : httpEngine.requestHeaders.getHeaders();
+    }
+
+    /**
+     * If we're establishing an HTTPS tunnel with CONNECT (RFC 2817 5.2), send
+     * only the minimum set of headers. This avoids sending potentially
+     * sensitive data like HTTP cookies to the proxy unencrypted.
+     */
+    private RawHeaders getTunnelNetworkRequestHeaders() {
+        RequestHeaders privateHeaders = httpEngine.requestHeaders;
+        URL url = httpEngine.policy.getURL();
+
+        RawHeaders result = new RawHeaders();
+        result.setStatusLine("CONNECT " + url.getHost() + ":" + Libcore.getEffectivePort(url)
+                + " HTTP/1.1");
+
+        // Always set Host and User-Agent.
+        String host = privateHeaders.getHost();
+        if (host == null) {
+            host = httpEngine.getOriginAddress(url);
+        }
+        result.set("Host", host);
+
+        String userAgent = privateHeaders.getUserAgent();
+        if (userAgent == null) {
+            userAgent = httpEngine.getDefaultUserAgent();
+        }
+        result.set("User-Agent", userAgent);
+
+        // Copy over the Proxy-Authorization header if it exists.
+        String proxyAuthorization = privateHeaders.getProxyAuthorization();
+        if (proxyAuthorization != null) {
+            result.set("Proxy-Authorization", proxyAuthorization);
+        }
+
+        // Always set the Proxy-Connection to Keep-Alive for the benefit of
+        // HTTP/1.0 proxies like Squid.
+        result.set("Proxy-Connection", "Keep-Alive");
+        return result;
+    }
+
+    @Override public ResponseHeaders readResponseHeaders() throws IOException {
+        RawHeaders headers;
+        do {
+            headers = new RawHeaders();
+            headers.setStatusLine(Streams.readAsciiLine(socketIn));
+            httpEngine.connection.httpMinorVersion = headers.getHttpMinorVersion();
+            readHeaders(headers);
+        } while (headers.getResponseCode() == HttpEngine.HTTP_CONTINUE);
+        return new ResponseHeaders(httpEngine.uri, headers);
+    }
+
+    /**
+     * Reads headers or trailers and updates the cookie store.
+     */
+    private void readHeaders(RawHeaders headers) throws IOException {
+        // parse the result headers until the first blank line
+        String line;
+        while ((line = Streams.readAsciiLine(socketIn)).length() != 0) {
+            headers.addLine(line);
+        }
+
+        CookieHandler cookieHandler = CookieHandler.getDefault();
+        if (cookieHandler != null) {
+            cookieHandler.put(httpEngine.uri, headers.toMultimap());
+        }
+    }
+
+    public boolean makeReusable(OutputStream requestBodyOut, InputStream responseBodyIn) {
+        // We cannot reuse sockets that have incomplete output.
+        if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) {
+            return false;
+        }
+
+        // If the headers specify that the connection shouldn't be reused, don't reuse it.
+        if (httpEngine.requestHeaders.hasConnectionClose()
+                || (httpEngine.responseHeaders != null
+                && httpEngine.responseHeaders.hasConnectionClose())) {
+            return false;
+        }
+
+        if (responseBodyIn instanceof UnknownLengthHttpInputStream) {
+            return false;
+        }
+
+        if (responseBodyIn != null) {
+            // Discard the response body before the connection can be reused.
+            try {
+                Streams.skipAll(responseBodyIn);
+            } catch (IOException e) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
+        if (!httpEngine.hasResponseBody()) {
+            return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, 0);
+        }
+
+        if (httpEngine.responseHeaders.isChunked()) {
+            return new ChunkedInputStream(socketIn, cacheRequest, this);
+        }
+
+        if (httpEngine.responseHeaders.getContentLength() != -1) {
+            return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine,
+                    httpEngine.responseHeaders.getContentLength());
+        }
+
+        /*
+         * Wrap the input stream from the HttpConnection (rather than
+         * just returning "socketIn" directly here), so that we can control
+         * its use after the reference escapes.
+         */
+        return new UnknownLengthHttpInputStream(socketIn, cacheRequest, httpEngine);
+    }
+
+    /**
+     * An HTTP body with a fixed length known in advance.
+     */
+    private static final class FixedLengthOutputStream extends AbstractHttpOutputStream {
+        private final OutputStream socketOut;
+        private int bytesRemaining;
+
+        private FixedLengthOutputStream(OutputStream socketOut, int bytesRemaining) {
+            this.socketOut = socketOut;
+            this.bytesRemaining = bytesRemaining;
+        }
+
+        @Override public void write(byte[] buffer, int offset, int count) throws IOException {
+            checkNotClosed();
+            Libcore.checkOffsetAndCount(buffer.length, offset, count);
+            if (count > bytesRemaining) {
+                throw new IOException("expected " + bytesRemaining
+                        + " bytes but received " + count);
+            }
+            socketOut.write(buffer, offset, count);
+            bytesRemaining -= count;
+        }
+
+        @Override public void flush() throws IOException {
+            if (closed) {
+                return; // don't throw; this stream might have been closed on the caller's behalf
+            }
+            socketOut.flush();
+        }
+
+        @Override public void close() throws IOException {
+            if (closed) {
+                return;
+            }
+            closed = true;
+            if (bytesRemaining > 0) {
+                throw new IOException("unexpected end of stream");
+            }
+        }
+    }
+
+    /**
+     * An HTTP body with alternating chunk sizes and chunk bodies. Chunks are
+     * buffered until {@code maxChunkLength} bytes are ready, at which point the
+     * chunk is written and the buffer is cleared.
+     */
+    private static final class ChunkedOutputStream extends AbstractHttpOutputStream {
+        private static final byte[] CRLF = {'\r', '\n'};
+        private static final byte[] HEX_DIGITS = {
+                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+        };
+        private static final byte[] FINAL_CHUNK = new byte[] {'0', '\r', '\n', '\r', '\n'};
+
+        /** Scratch space for up to 8 hex digits, and then a constant CRLF. */
+        private final byte[] hex = {0, 0, 0, 0, 0, 0, 0, 0, '\r', '\n'};
+
+        private final OutputStream socketOut;
+        private final int maxChunkLength;
+        private final ByteArrayOutputStream bufferedChunk;
+
+        private ChunkedOutputStream(OutputStream socketOut, int maxChunkLength) {
+            this.socketOut = socketOut;
+            this.maxChunkLength = Math.max(1, dataLength(maxChunkLength));
+            this.bufferedChunk = new ByteArrayOutputStream(maxChunkLength);
+        }
+
+        /**
+         * Returns the amount of data that can be transmitted in a chunk whose total
+         * length (data+headers) is {@code dataPlusHeaderLength}. This is presumably
+         * useful to match sizes with wire-protocol packets.
+         */
+        private int dataLength(int dataPlusHeaderLength) {
+            int headerLength = 4; // "\r\n" after the size plus another "\r\n" after the data
+            for (int i = dataPlusHeaderLength - headerLength; i > 0; i >>= 4) {
+                headerLength++;
+            }
+            return dataPlusHeaderLength - headerLength;
+        }
+
+        @Override public synchronized void write(byte[] buffer, int offset, int count)
+                throws IOException {
+            checkNotClosed();
+            Libcore.checkOffsetAndCount(buffer.length, offset, count);
+
+            while (count > 0) {
+                int numBytesWritten;
+
+                if (bufferedChunk.size() > 0 || count < maxChunkLength) {
+                    // fill the buffered chunk and then maybe write that to the stream
+                    numBytesWritten = Math.min(count, maxChunkLength - bufferedChunk.size());
+                    // TODO: skip unnecessary copies from buffer->bufferedChunk?
+                    bufferedChunk.write(buffer, offset, numBytesWritten);
+                    if (bufferedChunk.size() == maxChunkLength) {
+                        writeBufferedChunkToSocket();
+                    }
+
+                } else {
+                    // write a single chunk of size maxChunkLength to the stream
+                    numBytesWritten = maxChunkLength;
+                    writeHex(numBytesWritten);
+                    socketOut.write(buffer, offset, numBytesWritten);
+                    socketOut.write(CRLF);
+                }
+
+                offset += numBytesWritten;
+                count -= numBytesWritten;
+            }
+        }
+
+        /**
+         * Equivalent to, but cheaper than writing Integer.toHexString().getBytes()
+         * followed by CRLF.
+         */
+        private void writeHex(int i) throws IOException {
+            int cursor = 8;
+            do {
+                hex[--cursor] = HEX_DIGITS[i & 0xf];
+            } while ((i >>>= 4) != 0);
+            socketOut.write(hex, cursor, hex.length - cursor);
+        }
+
+        @Override public synchronized void flush() throws IOException {
+            if (closed) {
+                return; // don't throw; this stream might have been closed on the caller's behalf
+            }
+            writeBufferedChunkToSocket();
+            socketOut.flush();
+        }
+
+        @Override public synchronized void close() throws IOException {
+            if (closed) {
+                return;
+            }
+            closed = true;
+            writeBufferedChunkToSocket();
+            socketOut.write(FINAL_CHUNK);
+        }
+
+        private void writeBufferedChunkToSocket() throws IOException {
+            int size = bufferedChunk.size();
+            if (size <= 0) {
+                return;
+            }
+
+            writeHex(size);
+            bufferedChunk.writeTo(socketOut);
+            bufferedChunk.reset();
+            socketOut.write(CRLF);
+        }
+    }
+
+    /**
+     * An HTTP body with a fixed length specified in advance.
+     */
+    private static class FixedLengthInputStream extends AbstractHttpInputStream {
+        private int bytesRemaining;
+
+        public FixedLengthInputStream(InputStream is, CacheRequest cacheRequest,
+                HttpEngine httpEngine, int length) throws IOException {
+            super(is, httpEngine, cacheRequest);
+            bytesRemaining = length;
+            if (bytesRemaining == 0) {
+                endOfInput(true);
+            }
+        }
+
+        @Override public int read(byte[] buffer, int offset, int count) throws IOException {
+            Libcore.checkOffsetAndCount(buffer.length, offset, count);
+            checkNotClosed();
+            if (bytesRemaining == 0) {
+                return -1;
+            }
+            int read = in.read(buffer, offset, Math.min(count, bytesRemaining));
+            if (read == -1) {
+                unexpectedEndOfInput(); // the server didn't supply the promised content length
+                throw new IOException("unexpected end of stream");
+            }
+            bytesRemaining -= read;
+            cacheWrite(buffer, offset, read);
+            if (bytesRemaining == 0) {
+                endOfInput(true);
+            }
+            return read;
+        }
+
+        @Override public int available() throws IOException {
+            checkNotClosed();
+            return bytesRemaining == 0 ? 0 : Math.min(in.available(), bytesRemaining);
+        }
+
+        @Override public void close() throws IOException {
+            if (closed) {
+                return;
+            }
+            closed = true;
+            if (bytesRemaining != 0) {
+                unexpectedEndOfInput();
+            }
+        }
+    }
+
+    /**
+     * An HTTP body with alternating chunk sizes and chunk bodies.
+     */
+    private static class ChunkedInputStream extends AbstractHttpInputStream {
+        private static final int MIN_LAST_CHUNK_LENGTH = "\r\n0\r\n\r\n".length();
+        private static final int NO_CHUNK_YET = -1;
+        private final HttpTransport transport;
+        private int bytesRemainingInChunk = NO_CHUNK_YET;
+        private boolean hasMoreChunks = true;
+
+        ChunkedInputStream(InputStream is, CacheRequest cacheRequest,
+                HttpTransport transport) throws IOException {
+            super(is, transport.httpEngine, cacheRequest);
+            this.transport = transport;
+        }
+
+        @Override public int read(byte[] buffer, int offset, int count) throws IOException {
+            Libcore.checkOffsetAndCount(buffer.length, offset, count);
+            checkNotClosed();
+
+            if (!hasMoreChunks) {
+                return -1;
+            }
+            if (bytesRemainingInChunk == 0 || bytesRemainingInChunk == NO_CHUNK_YET) {
+                readChunkSize();
+                if (!hasMoreChunks) {
+                    return -1;
+                }
+            }
+            int read = in.read(buffer, offset, Math.min(count, bytesRemainingInChunk));
+            if (read == -1) {
+                unexpectedEndOfInput(); // the server didn't supply the promised chunk length
+                throw new IOException("unexpected end of stream");
+            }
+            bytesRemainingInChunk -= read;
+            cacheWrite(buffer, offset, read);
+
+            /*
+            * If we're at the end of a chunk and the next chunk size is readable,
+            * read it! Reading the last chunk causes the underlying connection to
+            * be recycled and we want to do that as early as possible. Otherwise
+            * self-delimiting streams like gzip will never be recycled.
+            * http://code.google.com/p/android/issues/detail?id=7059
+            */
+            if (bytesRemainingInChunk == 0 && in.available() >= MIN_LAST_CHUNK_LENGTH) {
+                readChunkSize();
+            }
+
+            return read;
+        }
+
+        private void readChunkSize() throws IOException {
+            // read the suffix of the previous chunk
+            if (bytesRemainingInChunk != NO_CHUNK_YET) {
+                Streams.readAsciiLine(in);
+            }
+            String chunkSizeString = Streams.readAsciiLine(in);
+            int index = chunkSizeString.indexOf(";");
+            if (index != -1) {
+                chunkSizeString = chunkSizeString.substring(0, index);
+            }
+            try {
+                bytesRemainingInChunk = Integer.parseInt(chunkSizeString.trim(), 16);
+            } catch (NumberFormatException e) {
+                throw new IOException("Expected a hex chunk size, but was " + chunkSizeString);
+            }
+            if (bytesRemainingInChunk == 0) {
+                hasMoreChunks = false;
+                transport.readHeaders(httpEngine.responseHeaders.getHeaders());
+                endOfInput(true);
+            }
+        }
+
+        @Override public int available() throws IOException {
+            checkNotClosed();
+            if (!hasMoreChunks || bytesRemainingInChunk == NO_CHUNK_YET) {
+                return 0;
+            }
+            return Math.min(in.available(), bytesRemainingInChunk);
+        }
+
+        @Override public void close() throws IOException {
+            if (closed) {
+                return;
+            }
+
+            closed = true;
+            if (hasMoreChunks) {
+                unexpectedEndOfInput();
+            }
+        }
+    }
+
+    /**
+     * An HTTP payload terminated by the end of the socket stream.
+     */
+    private static final class UnknownLengthHttpInputStream extends AbstractHttpInputStream {
+        private boolean inputExhausted;
+
+        private UnknownLengthHttpInputStream(InputStream is, CacheRequest cacheRequest,
+                HttpEngine httpEngine) throws IOException {
+            super(is, httpEngine, cacheRequest);
+        }
+
+        @Override public int read(byte[] buffer, int offset, int count) throws IOException {
+            Libcore.checkOffsetAndCount(buffer.length, offset, count);
+            checkNotClosed();
+            if (in == null || inputExhausted) {
+                return -1;
+            }
+            int read = in.read(buffer, offset, count);
+            if (read == -1) {
+                inputExhausted = true;
+                endOfInput(false);
+                return -1;
+            }
+            cacheWrite(buffer, offset, read);
+            return read;
+        }
+
+        @Override public int available() throws IOException {
+            checkNotClosed();
+            return in == null ? 0 : in.available();
+        }
+
+        @Override public void close() throws IOException {
+            if (closed) {
+                return;
+            }
+            closed = true;
+            if (!inputExhausted) {
+                unexpectedEndOfInput();
+            }
+        }
+    }
+}
diff --git a/src/main/java/libcore/net/http/HttpURLConnectionImpl.java b/src/main/java/libcore/net/http/HttpURLConnectionImpl.java
new file mode 100644
index 0000000..99a6ac4
--- /dev/null
+++ b/src/main/java/libcore/net/http/HttpURLConnectionImpl.java
@@ -0,0 +1,513 @@
+/*
+ *  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 libcore.net.http;
+
+import com.squareup.okhttp.OkHttpConnection;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Authenticator;
+import java.net.HttpRetryException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.PasswordAuthentication;
+import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.SocketPermission;
+import java.net.URL;
+import java.security.Permission;
+import java.util.List;
+import java.util.Map;
+import libcore.io.Base64;
+import libcore.util.Libcore;
+
+/**
+ * This implementation uses HttpEngine to send requests and receive responses.
+ * This class may use multiple HttpEngines to follow redirects, authentication
+ * retries, etc. to retrieve the final response body.
+ *
+ * <h3>What does 'connected' mean?</h3>
+ * This class inherits a {@code connected} field from the superclass. That field
+ * is <strong>not</strong> used to indicate not whether this URLConnection is
+ * currently connected. Instead, it indicates whether a connection has ever been
+ * attempted. Once a connection has been attempted, certain properties (request
+ * header fields, request method, etc.) are immutable. Test the {@code
+ * connection} field on this class for null/non-null to determine of an instance
+ * is currently connected to a server.
+ */
+public class HttpURLConnectionImpl extends OkHttpConnection {
+    /**
+     * HTTP 1.1 doesn't specify how many redirects to follow, but HTTP/1.0
+     * recommended 5. http://www.w3.org/Protocols/HTTP/1.0/spec.html#Code3xx
+     */
+    private static final int MAX_REDIRECTS = 5;
+
+    private final int defaultPort;
+
+    private Proxy proxy;
+
+    private final RawHeaders rawRequestHeaders = new RawHeaders();
+
+    private int redirectionCount;
+
+    protected IOException httpEngineFailure;
+    protected HttpEngine httpEngine;
+
+    public HttpURLConnectionImpl(URL url, int port) {
+        super(url);
+        defaultPort = port;
+    }
+
+    public HttpURLConnectionImpl(URL url, int port, Proxy proxy) {
+        this(url, port);
+        this.proxy = proxy;
+    }
+
+    @Override public final void connect() throws IOException {
+        initHttpEngine();
+        try {
+            httpEngine.sendRequest();
+        } catch (IOException e) {
+            httpEngineFailure = e;
+            throw e;
+        }
+    }
+
+    @Override public final void disconnect() {
+        // Calling disconnect() before a connection exists should have no effect.
+        if (httpEngine != null) {
+            httpEngine.release(false);
+        }
+    }
+
+    /**
+     * Returns an input stream from the server in the case of error such as the
+     * requested file (txt, htm, html) is not found on the remote server.
+     */
+    @Override public final InputStream getErrorStream() {
+        try {
+            HttpEngine response = getResponse();
+            if (response.hasResponseBody()
+                    && response.getResponseCode() >= HTTP_BAD_REQUEST) {
+                return response.getResponseBody();
+            }
+            return null;
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the value of the field at {@code position}. Returns null if there
+     * are fewer than {@code position} headers.
+     */
+    @Override public final String getHeaderField(int position) {
+        try {
+            return getResponse().getResponseHeaders().getHeaders().getValue(position);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the value of the field corresponding to the {@code fieldName}, or
+     * null if there is no such field. If the field has multiple values, the
+     * last value is returned.
+     */
+    @Override public final String getHeaderField(String fieldName) {
+        try {
+            RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders();
+            return fieldName == null
+                    ? rawHeaders.getStatusLine()
+                    : rawHeaders.get(fieldName);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @Override public final String getHeaderFieldKey(int position) {
+        try {
+            return getResponse().getResponseHeaders().getHeaders().getFieldName(position);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @Override public final Map<String, List<String>> getHeaderFields() {
+        try {
+            return getResponse().getResponseHeaders().getHeaders().toMultimap();
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @Override public final Map<String, List<String>> getRequestProperties() {
+        if (connected) {
+            throw new IllegalStateException(
+                    "Cannot access request header fields after connection is set");
+        }
+        return rawRequestHeaders.toMultimap();
+    }
+
+    @Override public final InputStream getInputStream() throws IOException {
+        if (!doInput) {
+            throw new ProtocolException("This protocol does not support input");
+        }
+
+        HttpEngine response = getResponse();
+
+        /*
+         * if the requested file does not exist, throw an exception formerly the
+         * Error page from the server was returned if the requested file was
+         * text/html this has changed to return FileNotFoundException for all
+         * file types
+         */
+        if (getResponseCode() >= HTTP_BAD_REQUEST) {
+            throw new FileNotFoundException(url.toString());
+        }
+
+        InputStream result = response.getResponseBody();
+        if (result == null) {
+            throw new IOException("No response body exists; responseCode=" + getResponseCode());
+        }
+        return result;
+    }
+
+    @Override public final OutputStream getOutputStream() throws IOException {
+        connect();
+
+        OutputStream result = httpEngine.getRequestBody();
+        if (result == null) {
+            throw new ProtocolException("method does not support a request body: " + method);
+        } else if (httpEngine.hasResponse()) {
+            throw new ProtocolException("cannot write request body after response has been read");
+        }
+
+        return result;
+    }
+
+    @Override public final Permission getPermission() throws IOException {
+        String connectToAddress = getConnectToHost() + ":" + getConnectToPort();
+        return new SocketPermission(connectToAddress, "connect, resolve");
+    }
+
+    private String getConnectToHost() {
+        return usingProxy()
+                ? ((InetSocketAddress) proxy.address()).getHostName()
+                : getURL().getHost();
+    }
+
+    private int getConnectToPort() {
+        int hostPort = usingProxy()
+                ? ((InetSocketAddress) proxy.address()).getPort()
+                : getURL().getPort();
+        return hostPort < 0 ? getDefaultPort() : hostPort;
+    }
+
+    @Override public final String getRequestProperty(String field) {
+        if (field == null) {
+            return null;
+        }
+        return rawRequestHeaders.get(field);
+    }
+
+    private void initHttpEngine() throws IOException {
+        if (httpEngineFailure != null) {
+            throw httpEngineFailure;
+        } else if (httpEngine != null) {
+            return;
+        }
+
+        connected = true;
+        try {
+            if (doOutput) {
+                if (method == HttpEngine.GET) {
+                    // they are requesting a stream to write to. This implies a POST method
+                    method = HttpEngine.POST;
+                } else if (method != HttpEngine.POST && method != HttpEngine.PUT) {
+                    // If the request method is neither POST nor PUT, then you're not writing
+                    throw new ProtocolException(method + " does not support writing");
+                }
+            }
+            httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
+        } catch (IOException e) {
+            httpEngineFailure = e;
+            throw e;
+        }
+    }
+
+    /**
+     * Create a new HTTP engine. This hook method is non-final so it can be
+     * overridden by HttpsURLConnectionImpl.
+     */
+    protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
+            HttpConnection connection, RetryableOutputStream requestBody) throws IOException {
+        return new HttpEngine(this, method, requestHeaders, connection, requestBody);
+    }
+
+    /**
+     * Aggressively tries to get the final HTTP response, potentially making
+     * many HTTP requests in the process in order to cope with redirects and
+     * authentication.
+     */
+    private HttpEngine getResponse() throws IOException {
+        initHttpEngine();
+
+        if (httpEngine.hasResponse()) {
+            return httpEngine;
+        }
+
+        while (true) {
+            try {
+                httpEngine.sendRequest();
+                httpEngine.readResponse();
+            } catch (IOException e) {
+                /*
+                 * If the connection was recycled, its staleness may have caused
+                 * the failure. Silently retry with a different connection.
+                 */
+                OutputStream requestBody = httpEngine.getRequestBody();
+                if (httpEngine.hasRecycledConnection()
+                        && (requestBody == null || requestBody instanceof RetryableOutputStream)) {
+                    httpEngine.release(false);
+                    httpEngine = newHttpEngine(method, rawRequestHeaders, null,
+                            (RetryableOutputStream) requestBody);
+                    continue;
+                }
+                httpEngineFailure = e;
+                throw e;
+            }
+
+            Retry retry = processResponseHeaders();
+            if (retry == Retry.NONE) {
+                httpEngine.automaticallyReleaseConnectionToPool();
+                return httpEngine;
+            }
+
+            /*
+             * The first request was insufficient. Prepare for another...
+             */
+            String retryMethod = method;
+            OutputStream requestBody = httpEngine.getRequestBody();
+
+            /*
+             * Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM
+             * redirect should keep the same method, Chrome, Firefox and the
+             * RI all issue GETs when following any redirect.
+             */
+            int responseCode = getResponseCode();
+            if (responseCode == HTTP_MULT_CHOICE || responseCode == HTTP_MOVED_PERM
+                    || responseCode == HTTP_MOVED_TEMP || responseCode == HTTP_SEE_OTHER) {
+                retryMethod = HttpEngine.GET;
+                requestBody = null;
+            }
+
+            if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
+                throw new HttpRetryException("Cannot retry streamed HTTP body",
+                        httpEngine.getResponseCode());
+            }
+
+            if (retry == Retry.DIFFERENT_CONNECTION) {
+                httpEngine.automaticallyReleaseConnectionToPool();
+            }
+
+            httpEngine.release(true);
+
+            httpEngine = newHttpEngine(retryMethod, rawRequestHeaders,
+                    httpEngine.getConnection(), (RetryableOutputStream) requestBody);
+        }
+    }
+
+    HttpEngine getHttpEngine() {
+        return httpEngine;
+    }
+
+    enum Retry {
+        NONE,
+        SAME_CONNECTION,
+        DIFFERENT_CONNECTION
+    }
+
+    /**
+     * Returns the retry action to take for the current response headers. The
+     * headers, proxy and target URL or this connection may be adjusted to
+     * prepare for a follow up request.
+     */
+    private Retry processResponseHeaders() throws IOException {
+        switch (getResponseCode()) {
+        case HTTP_PROXY_AUTH:
+            if (!usingProxy()) {
+                throw new IOException(
+                        "Received HTTP_PROXY_AUTH (407) code while not using proxy");
+            }
+            // fall-through
+        case HTTP_UNAUTHORIZED:
+            boolean credentialsFound = processAuthHeader(getResponseCode(),
+                    httpEngine.getResponseHeaders(), rawRequestHeaders);
+            return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
+
+        case HTTP_MULT_CHOICE:
+        case HTTP_MOVED_PERM:
+        case HTTP_MOVED_TEMP:
+        case HTTP_SEE_OTHER:
+            if (!getInstanceFollowRedirects()) {
+                return Retry.NONE;
+            }
+            if (++redirectionCount > MAX_REDIRECTS) {
+                throw new ProtocolException("Too many redirects");
+            }
+            String location = getHeaderField("Location");
+            if (location == null) {
+                return Retry.NONE;
+            }
+            URL previousUrl = url;
+            url = new URL(previousUrl, location);
+            if (!previousUrl.getProtocol().equals(url.getProtocol())) {
+                return Retry.NONE; // the scheme changed; don't retry.
+            }
+            if (previousUrl.getHost().equals(url.getHost())
+                    && Libcore.getEffectivePort(previousUrl) == Libcore.getEffectivePort(url)) {
+                return Retry.SAME_CONNECTION;
+            } else {
+                return Retry.DIFFERENT_CONNECTION;
+            }
+
+        default:
+            return Retry.NONE;
+        }
+    }
+
+    /**
+     * React to a failed authorization response by looking up new credentials.
+     *
+     * @return true if credentials have been added to successorRequestHeaders
+     *     and another request should be attempted.
+     */
+    final boolean processAuthHeader(int responseCode, ResponseHeaders response,
+            RawHeaders successorRequestHeaders) throws IOException {
+        if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) {
+            throw new IllegalArgumentException();
+        }
+
+        // keep asking for username/password until authorized
+        String challengeHeader = responseCode == HTTP_PROXY_AUTH
+                ? "Proxy-Authenticate"
+                : "WWW-Authenticate";
+        String credentials = getAuthorizationCredentials(response.getHeaders(), challengeHeader);
+        if (credentials == null) {
+            return false; // could not find credentials, end request cycle
+        }
+
+        // add authorization credentials, bypassing the already-connected check
+        String fieldName = responseCode == HTTP_PROXY_AUTH
+                ? "Proxy-Authorization"
+                : "Authorization";
+        successorRequestHeaders.set(fieldName, credentials);
+        return true;
+    }
+
+    /**
+     * Returns the authorization credentials on the base of provided challenge.
+     */
+    private String getAuthorizationCredentials(RawHeaders responseHeaders, String challengeHeader)
+            throws IOException {
+        List<Challenge> challenges = HeaderParser.parseChallenges(responseHeaders, challengeHeader);
+        if (challenges.isEmpty()) {
+            throw new IOException("No authentication challenges found");
+        }
+
+        for (Challenge challenge : challenges) {
+            // use the global authenticator to get the password
+            PasswordAuthentication auth = Authenticator.requestPasswordAuthentication(
+                    getConnectToInetAddress(), getConnectToPort(), url.getProtocol(),
+                    challenge.realm, challenge.scheme);
+            if (auth == null) {
+                continue;
+            }
+
+            // base64 encode the username and password
+            String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword());
+            byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
+            String encoded = Base64.encode(bytes);
+            return challenge.scheme + " " + encoded;
+        }
+
+        return null;
+    }
+
+    private InetAddress getConnectToInetAddress() throws IOException {
+        return usingProxy()
+                ? ((InetSocketAddress) proxy.address()).getAddress()
+                : InetAddress.getByName(getURL().getHost());
+    }
+
+    final int getDefaultPort() {
+        return defaultPort;
+    }
+
+    /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
+    final int getFixedContentLength() {
+        return fixedContentLength;
+    }
+
+    /** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
+    final int getChunkLength() {
+        return chunkLength;
+    }
+
+    final Proxy getProxy() {
+        return proxy;
+    }
+
+    final void setProxy(Proxy proxy) {
+        this.proxy = proxy;
+    }
+
+    @Override public final boolean usingProxy() {
+        return (proxy != null && proxy.type() != Proxy.Type.DIRECT);
+    }
+
+    @Override public String getResponseMessage() throws IOException {
+        return getResponse().getResponseHeaders().getHeaders().getResponseMessage();
+    }
+
+    @Override public final int getResponseCode() throws IOException {
+        return getResponse().getResponseCode();
+    }
+
+    @Override public final void setRequestProperty(String field, String newValue) {
+        if (connected) {
+            throw new IllegalStateException("Cannot set request property after connection is made");
+        }
+        if (field == null) {
+            throw new NullPointerException("field == null");
+        }
+        rawRequestHeaders.set(field, newValue);
+    }
+
+    @Override public final void addRequestProperty(String field, String value) {
+        if (connected) {
+            throw new IllegalStateException("Cannot add request property after connection is made");
+        }
+        if (field == null) {
+            throw new NullPointerException("field == null");
+        }
+        rawRequestHeaders.add(field, value);
+    }
+}
diff --git a/src/main/java/libcore/net/http/HttpsHandler.java b/src/main/java/libcore/net/http/HttpsHandler.java
new file mode 100644
index 0000000..ed9ba72
--- /dev/null
+++ b/src/main/java/libcore/net/http/HttpsHandler.java
@@ -0,0 +1,42 @@
+/*
+ *  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 libcore.net.http;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+public final class HttpsHandler extends URLStreamHandler {
+
+    @Override protected URLConnection openConnection(URL url) throws IOException {
+        return new HttpsURLConnectionImpl(url, getDefaultPort());
+    }
+
+    @Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
+        if (url == null || proxy == null) {
+            throw new IllegalArgumentException("url == null || proxy == null");
+        }
+        return new HttpsURLConnectionImpl(url, getDefaultPort(), proxy);
+    }
+
+    @Override protected int getDefaultPort() {
+        return 443;
+    }
+}
diff --git a/src/main/java/libcore/net/http/HttpsURLConnectionImpl.java b/src/main/java/libcore/net/http/HttpsURLConnectionImpl.java
new file mode 100644
index 0000000..0ea33f7
--- /dev/null
+++ b/src/main/java/libcore/net/http/HttpsURLConnectionImpl.java
@@ -0,0 +1,535 @@
+/*
+ *  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 libcore.net.http;
+
+import com.squareup.okhttp.OkHttpConnection;
+import com.squareup.okhttp.OkHttpsConnection;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheResponse;
+import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.SecureCacheResponse;
+import java.net.URL;
+import java.security.Permission;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+public final class HttpsURLConnectionImpl extends OkHttpsConnection {
+
+    /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
+    private final HttpUrlConnectionDelegate delegate;
+
+    public HttpsURLConnectionImpl(URL url, int port) {
+        super(url);
+        delegate = new HttpUrlConnectionDelegate(url, port);
+    }
+
+    public HttpsURLConnectionImpl(URL url, int port, Proxy proxy) {
+        super(url);
+        delegate = new HttpUrlConnectionDelegate(url, port, proxy);
+    }
+
+    private void checkConnected() {
+        if (delegate.getSSLSocket() == null) {
+            throw new IllegalStateException("Connection has not yet been established");
+        }
+    }
+
+    HttpEngine getHttpEngine() {
+        return delegate.getHttpEngine();
+    }
+
+    @Override
+    public String getCipherSuite() {
+        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
+        if (cacheResponse != null) {
+            return cacheResponse.getCipherSuite();
+        }
+        checkConnected();
+        return delegate.getSSLSocket().getSession().getCipherSuite();
+    }
+
+    @Override
+    public Certificate[] getLocalCertificates() {
+        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
+        if (cacheResponse != null) {
+            List<Certificate> result = cacheResponse.getLocalCertificateChain();
+            return result != null ? result.toArray(new Certificate[result.size()]) : null;
+        }
+        checkConnected();
+        return delegate.getSSLSocket().getSession().getLocalCertificates();
+    }
+
+    @Override
+    public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
+        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
+        if (cacheResponse != null) {
+            List<Certificate> result = cacheResponse.getServerCertificateChain();
+            return result != null ? result.toArray(new Certificate[result.size()]) : null;
+        }
+        checkConnected();
+        return delegate.getSSLSocket().getSession().getPeerCertificates();
+    }
+
+    @Override
+    public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
+        if (cacheResponse != null) {
+            return cacheResponse.getPeerPrincipal();
+        }
+        checkConnected();
+        return delegate.getSSLSocket().getSession().getPeerPrincipal();
+    }
+
+    @Override
+    public Principal getLocalPrincipal() {
+        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
+        if (cacheResponse != null) {
+            return cacheResponse.getLocalPrincipal();
+        }
+        checkConnected();
+        return delegate.getSSLSocket().getSession().getLocalPrincipal();
+    }
+
+    @Override
+    public void disconnect() {
+        delegate.disconnect();
+    }
+
+    @Override
+    public InputStream getErrorStream() {
+        return delegate.getErrorStream();
+    }
+
+    @Override
+    public String getRequestMethod() {
+        return delegate.getRequestMethod();
+    }
+
+    @Override
+    public int getResponseCode() throws IOException {
+        return delegate.getResponseCode();
+    }
+
+    @Override
+    public String getResponseMessage() throws IOException {
+        return delegate.getResponseMessage();
+    }
+
+    @Override
+    public void setRequestMethod(String method) throws ProtocolException {
+        delegate.setRequestMethod(method);
+    }
+
+    @Override
+    public boolean usingProxy() {
+        return delegate.usingProxy();
+    }
+
+    @Override
+    public boolean getInstanceFollowRedirects() {
+        return delegate.getInstanceFollowRedirects();
+    }
+
+    @Override
+    public void setInstanceFollowRedirects(boolean followRedirects) {
+        delegate.setInstanceFollowRedirects(followRedirects);
+    }
+
+    @Override
+    public void connect() throws IOException {
+        connected = true;
+        delegate.connect();
+    }
+
+    @Override
+    public boolean getAllowUserInteraction() {
+        return delegate.getAllowUserInteraction();
+    }
+
+    @Override
+    public Object getContent() throws IOException {
+        return delegate.getContent();
+    }
+
+    @SuppressWarnings("unchecked") // Spec does not generify
+    @Override
+    public Object getContent(Class[] types) throws IOException {
+        return delegate.getContent(types);
+    }
+
+    @Override
+    public String getContentEncoding() {
+        return delegate.getContentEncoding();
+    }
+
+    @Override
+    public int getContentLength() {
+        return delegate.getContentLength();
+    }
+
+    @Override
+    public String getContentType() {
+        return delegate.getContentType();
+    }
+
+    @Override
+    public long getDate() {
+        return delegate.getDate();
+    }
+
+    @Override
+    public boolean getDefaultUseCaches() {
+        return delegate.getDefaultUseCaches();
+    }
+
+    @Override
+    public boolean getDoInput() {
+        return delegate.getDoInput();
+    }
+
+    @Override
+    public boolean getDoOutput() {
+        return delegate.getDoOutput();
+    }
+
+    @Override
+    public long getExpiration() {
+        return delegate.getExpiration();
+    }
+
+    @Override
+    public String getHeaderField(int pos) {
+        return delegate.getHeaderField(pos);
+    }
+
+    @Override
+    public Map<String, List<String>> getHeaderFields() {
+        return delegate.getHeaderFields();
+    }
+
+    @Override
+    public Map<String, List<String>> getRequestProperties() {
+        return delegate.getRequestProperties();
+    }
+
+    @Override
+    public void addRequestProperty(String field, String newValue) {
+        delegate.addRequestProperty(field, newValue);
+    }
+
+    @Override
+    public String getHeaderField(String key) {
+        return delegate.getHeaderField(key);
+    }
+
+    @Override
+    public long getHeaderFieldDate(String field, long defaultValue) {
+        return delegate.getHeaderFieldDate(field, defaultValue);
+    }
+
+    @Override
+    public int getHeaderFieldInt(String field, int defaultValue) {
+        return delegate.getHeaderFieldInt(field, defaultValue);
+    }
+
+    @Override
+    public String getHeaderFieldKey(int posn) {
+        return delegate.getHeaderFieldKey(posn);
+    }
+
+    @Override
+    public long getIfModifiedSince() {
+        return delegate.getIfModifiedSince();
+    }
+
+    @Override
+    public InputStream getInputStream() throws IOException {
+        return delegate.getInputStream();
+    }
+
+    @Override
+    public long getLastModified() {
+        return delegate.getLastModified();
+    }
+
+    @Override
+    public OutputStream getOutputStream() throws IOException {
+        return delegate.getOutputStream();
+    }
+
+    @Override
+    public Permission getPermission() throws IOException {
+        return delegate.getPermission();
+    }
+
+    @Override
+    public String getRequestProperty(String field) {
+        return delegate.getRequestProperty(field);
+    }
+
+    @Override
+    public URL getURL() {
+        return delegate.getURL();
+    }
+
+    @Override
+    public boolean getUseCaches() {
+        return delegate.getUseCaches();
+    }
+
+    @Override
+    public void setAllowUserInteraction(boolean newValue) {
+        delegate.setAllowUserInteraction(newValue);
+    }
+
+    @Override
+    public void setDefaultUseCaches(boolean newValue) {
+        delegate.setDefaultUseCaches(newValue);
+    }
+
+    @Override
+    public void setDoInput(boolean newValue) {
+        delegate.setDoInput(newValue);
+    }
+
+    @Override
+    public void setDoOutput(boolean newValue) {
+        delegate.setDoOutput(newValue);
+    }
+
+    @Override
+    public void setIfModifiedSince(long newValue) {
+        delegate.setIfModifiedSince(newValue);
+    }
+
+    @Override
+    public void setRequestProperty(String field, String newValue) {
+        delegate.setRequestProperty(field, newValue);
+    }
+
+    @Override
+    public void setUseCaches(boolean newValue) {
+        delegate.setUseCaches(newValue);
+    }
+
+    @Override
+    public void setConnectTimeout(int timeoutMillis) {
+        delegate.setConnectTimeout(timeoutMillis);
+    }
+
+    @Override
+    public int getConnectTimeout() {
+        return delegate.getConnectTimeout();
+    }
+
+    @Override
+    public void setReadTimeout(int timeoutMillis) {
+        delegate.setReadTimeout(timeoutMillis);
+    }
+
+    @Override
+    public int getReadTimeout() {
+        return delegate.getReadTimeout();
+    }
+
+    @Override
+    public String toString() {
+        return delegate.toString();
+    }
+
+    @Override
+    public void setFixedLengthStreamingMode(int contentLength) {
+        delegate.setFixedLengthStreamingMode(contentLength);
+    }
+
+    @Override
+    public void setChunkedStreamingMode(int chunkLength) {
+        delegate.setChunkedStreamingMode(chunkLength);
+    }
+
+    private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
+        private HttpUrlConnectionDelegate(URL url, int port) {
+            super(url, port);
+        }
+
+        private HttpUrlConnectionDelegate(URL url, int port, Proxy proxy) {
+            super(url, port, proxy);
+        }
+
+        @Override protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
+                HttpConnection connection, RetryableOutputStream requestBody) throws IOException {
+            return new HttpsEngine(this, method, requestHeaders, connection, requestBody,
+                    HttpsURLConnectionImpl.this);
+        }
+
+        public SecureCacheResponse getCacheResponse() {
+            HttpsEngine engine = (HttpsEngine) httpEngine;
+            return engine != null ? (SecureCacheResponse) engine.getCacheResponse() : null;
+        }
+
+        public SSLSocket getSSLSocket() {
+            HttpsEngine engine = (HttpsEngine) httpEngine;
+            return engine != null ? engine.sslSocket : null;
+        }
+    }
+
+    private static final class HttpsEngine extends HttpEngine {
+
+        /**
+         * Local stash of HttpsEngine.connection.sslSocket for answering
+         * queries such as getCipherSuite even after
+         * httpsEngine.Connection has been recycled. It's presence is also
+         * used to tell if the HttpsURLConnection is considered connected,
+         * as opposed to the connected field of URLConnection or the a
+         * non-null connect in HttpURLConnectionImpl
+         */
+        private SSLSocket sslSocket;
+
+        private final HttpsURLConnectionImpl enclosing;
+
+        /**
+         * @param policy the HttpURLConnectionImpl with connection configuration
+         * @param enclosing the HttpsURLConnection with HTTPS features
+         */
+        private HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
+                HttpConnection connection, RetryableOutputStream requestBody,
+                HttpsURLConnectionImpl enclosing) throws IOException {
+            super(policy, method, requestHeaders, connection, requestBody);
+            this.sslSocket = connection != null ? connection.getSecureSocketIfConnected() : null;
+            this.enclosing = enclosing;
+        }
+
+        @Override protected void connect() throws IOException {
+            // First try an SSL connection with compression and various TLS
+            // extensions enabled, if it fails (and its not unheard of that it
+            // will) fallback to a barebones connection.
+            try {
+                makeSslConnection(true);
+            } catch (IOException e) {
+                // If the problem was a CertificateException from the X509TrustManager,
+                // do not retry, we didn't have an abrupt server initiated exception.
+                if (e instanceof SSLHandshakeException
+                        && e.getCause() instanceof CertificateException) {
+                    throw e;
+                }
+                release(false);
+                makeSslConnection(false);
+            }
+        }
+
+        /**
+         * Attempt to make an HTTPS connection.
+         *
+         * @param tlsTolerant If true, assume server can handle common
+         * TLS extensions and SSL deflate compression. If false, use
+         * an SSL3 only fallback mode without compression.
+         */
+        private void makeSslConnection(boolean tlsTolerant) throws IOException {
+            // make an SSL Tunnel on the first message pair of each SSL + proxy connection
+            if (connection == null) {
+                connection = openSocketConnection();
+                if (connection.getAddress().getProxy() != null) {
+                    makeTunnel(policy, connection, getRequestHeaders());
+                }
+            }
+
+            // if super.makeConnection returned a connection from the
+            // pool, sslSocket needs to be initialized here. If it is
+            // a new connection, it will be initialized by
+            // getSecureSocket below.
+            sslSocket = connection.getSecureSocketIfConnected();
+
+            // we already have an SSL connection,
+            if (sslSocket != null) {
+                return;
+            }
+
+            sslSocket = connection.setupSecureSocket(
+                    enclosing.getSSLSocketFactory(), enclosing.getHostnameVerifier(), tlsTolerant);
+        }
+
+        /**
+         * To make an HTTPS connection over an HTTP proxy, send an unencrypted
+         * CONNECT request to create the proxy connection. This may need to be
+         * retried if the proxy requires authorization.
+         */
+        private void makeTunnel(HttpURLConnectionImpl policy, HttpConnection connection,
+                RequestHeaders requestHeaders) throws IOException {
+            RawHeaders rawRequestHeaders = requestHeaders.getHeaders();
+            while (true) {
+                HttpEngine connect = new ProxyConnectEngine(policy, rawRequestHeaders, connection);
+                connect.sendRequest();
+                connect.readResponse();
+
+                int responseCode = connect.getResponseCode();
+                switch (connect.getResponseCode()) {
+                case HTTP_OK:
+                    return;
+                case HTTP_PROXY_AUTH:
+                    rawRequestHeaders = new RawHeaders(rawRequestHeaders);
+                    boolean credentialsFound = policy.processAuthHeader(HTTP_PROXY_AUTH,
+                            connect.getResponseHeaders(), rawRequestHeaders);
+                    if (credentialsFound) {
+                        continue;
+                    } else {
+                        throw new IOException("Failed to authenticate with proxy");
+                    }
+                default:
+                    throw new IOException("Unexpected response code for CONNECT: " + responseCode);
+                }
+            }
+        }
+
+        @Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
+            return cacheResponse instanceof SecureCacheResponse;
+        }
+
+        @Override protected boolean includeAuthorityInRequestLine() {
+            // Even if there is a proxy, it isn't involved. Always request just the file.
+            return false;
+        }
+
+        @Override protected SSLSocketFactory getSslSocketFactory() {
+            return enclosing.getSSLSocketFactory();
+        }
+
+        @Override protected OkHttpConnection getHttpConnectionToCache() {
+            return enclosing;
+        }
+    }
+
+    private static class ProxyConnectEngine extends HttpEngine {
+        public ProxyConnectEngine(HttpURLConnectionImpl policy, RawHeaders requestHeaders,
+                HttpConnection connection) throws IOException {
+            super(policy, HttpEngine.CONNECT, requestHeaders, connection, null);
+        }
+
+        @Override protected boolean requiresTunnel() {
+            return true;
+        }
+    }
+}
diff --git a/src/main/java/libcore/net/http/RawHeaders.java b/src/main/java/libcore/net/http/RawHeaders.java
new file mode 100644
index 0000000..642c61d
--- /dev/null
+++ b/src/main/java/libcore/net/http/RawHeaders.java
@@ -0,0 +1,390 @@
+/*
+ *  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 libcore.net.http;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+import libcore.util.Libcore;
+
+/**
+ * The HTTP status and unparsed header fields of a single HTTP message. Values
+ * are represented as uninterpreted strings; use {@link RequestHeaders} and
+ * {@link ResponseHeaders} for interpreted headers. This class maintains the
+ * order of the header fields within the HTTP message.
+ *
+ * <p>This class tracks fields line-by-line. A field with multiple comma-
+ * separated values on the same line will be treated as a field with a single
+ * value by this class. It is the caller's responsibility to detect and split
+ * on commas if their field permits multiple values. This simplifies use of
+ * single-valued fields whose values routinely contain commas, such as cookies
+ * or dates.
+ *
+ * <p>This class trims whitespace from values. It never returns values with
+ * leading or trailing whitespace.
+ */
+public final class RawHeaders {
+    private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
+        // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
+        @Override public int compare(String a, String b) {
+            if (a == b) {
+                return 0;
+            } else if (a == null) {
+                return -1;
+            } else if (b == null) {
+                return 1;
+            } else {
+                return String.CASE_INSENSITIVE_ORDER.compare(a, b);
+            }
+        }
+    };
+
+    private final List<String> namesAndValues = new ArrayList<String>(20);
+    private String statusLine;
+    private int httpMinorVersion = 1;
+    private int responseCode = -1;
+    private String responseMessage;
+
+    public RawHeaders() {
+    }
+
+    public RawHeaders(RawHeaders copyFrom) {
+        namesAndValues.addAll(copyFrom.namesAndValues);
+        statusLine = copyFrom.statusLine;
+        httpMinorVersion = copyFrom.httpMinorVersion;
+        responseCode = copyFrom.responseCode;
+        responseMessage = copyFrom.responseMessage;
+    }
+
+    /**
+     * Sets the response status line (like "HTTP/1.0 200 OK") or request line
+     * (like "GET / HTTP/1.1").
+     */
+    public void setStatusLine(String statusLine) {
+        statusLine = statusLine.trim();
+        this.statusLine = statusLine;
+
+        if (statusLine == null || !statusLine.startsWith("HTTP/")) {
+            return;
+        }
+        statusLine = statusLine.trim();
+        int mark = statusLine.indexOf(" ") + 1;
+        if (mark == 0) {
+            return;
+        }
+        if (statusLine.charAt(mark - 2) != '1') {
+            this.httpMinorVersion = 0;
+        }
+        int last = mark + 3;
+        if (last > statusLine.length()) {
+            last = statusLine.length();
+        }
+        this.responseCode = Integer.parseInt(statusLine.substring(mark, last));
+        if (last + 1 <= statusLine.length()) {
+            this.responseMessage = statusLine.substring(last + 1);
+        }
+    }
+
+    public void computeResponseStatusLineFromSpdyHeaders() throws IOException {
+        String status = null;
+        String version = null;
+        for (int i = 0; i < namesAndValues.size(); i += 2) {
+            String name = namesAndValues.get(i);
+            if ("status".equals(name)) {
+                status = namesAndValues.get(i + 1);
+            } else if ("version".equals(name)) {
+                version = namesAndValues.get(i + 1);
+            }
+        }
+        if (status == null || version == null) {
+            throw new IOException("Expected 'status' and 'version' headers not present");
+        }
+        setStatusLine(version + " " + status);
+    }
+
+    /**
+     * @param method like "GET", "POST", "HEAD", etc.
+     * @param scheme like "https"
+     * @param url like "/foo/bar.html"
+     * @param version like "HTTP/1.1"
+     */
+    public void addSpdyRequestHeaders(String method, String scheme, String url, String version) {
+        // TODO: populate the statusLine for the client's benefit?
+        add("method", method);
+        add("scheme", scheme);
+        add("url", url);
+        add("version", version);
+    }
+
+    public String getStatusLine() {
+        return statusLine;
+    }
+
+    /**
+     * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0
+     * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown.
+     */
+    public int getHttpMinorVersion() {
+        return httpMinorVersion != -1 ? httpMinorVersion : 1;
+    }
+
+    /**
+     * Returns the HTTP status code or -1 if it is unknown.
+     */
+    public int getResponseCode() {
+        return responseCode;
+    }
+
+    /**
+     * Returns the HTTP status message or null if it is unknown.
+     */
+    public String getResponseMessage() {
+        return responseMessage;
+    }
+
+    /**
+     * Add an HTTP header line containing a field name, a literal colon, and a
+     * value.
+     */
+    public void addLine(String line) {
+        int index = line.indexOf(":");
+        if (index == -1) {
+            add("", line);
+        } else {
+            add(line.substring(0, index), line.substring(index + 1));
+        }
+    }
+
+    /**
+     * Add a field with the specified value.
+     */
+    public void add(String fieldName, String value) {
+        if (fieldName == null) {
+            throw new IllegalArgumentException("fieldName == null");
+        }
+        if (value == null) {
+            /*
+             * Given null values, the RI sends a malformed field line like
+             * "Accept\r\n". For platform compatibility and HTTP compliance, we
+             * print a warning and ignore null values.
+             */
+            Libcore.logW("Ignoring HTTP header field '" + fieldName + "' because its value is null");
+            return;
+        }
+        namesAndValues.add(fieldName);
+        namesAndValues.add(value.trim());
+    }
+
+    public void removeAll(String fieldName) {
+        for (int i = 0; i < namesAndValues.size(); i += 2) {
+            if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
+                namesAndValues.remove(i); // field name
+                namesAndValues.remove(i); // value
+            }
+        }
+    }
+
+    public void addAll(String fieldName, List<String> headerFields) {
+        for (String value : headerFields) {
+            add(fieldName, value);
+        }
+    }
+
+    /**
+     * Set a field with the specified value. If the field is not found, it is
+     * added. If the field is found, the existing values are replaced.
+     */
+    public void set(String fieldName, String value) {
+        removeAll(fieldName);
+        add(fieldName, value);
+    }
+
+    /**
+     * Returns the number of field values.
+     */
+    public int length() {
+        return namesAndValues.size() / 2;
+    }
+
+    /**
+     * Returns the field at {@code position} or null if that is out of range.
+     */
+    public String getFieldName(int index) {
+        int fieldNameIndex = index * 2;
+        if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) {
+            return null;
+        }
+        return namesAndValues.get(fieldNameIndex);
+    }
+
+    /**
+     * Returns the value at {@code index} or null if that is out of range.
+     */
+    public String getValue(int index) {
+        int valueIndex = index * 2 + 1;
+        if (valueIndex < 0 || valueIndex >= namesAndValues.size()) {
+            return null;
+        }
+        return namesAndValues.get(valueIndex);
+    }
+
+    /**
+     * Returns the last value corresponding to the specified field, or null.
+     */
+    public String get(String fieldName) {
+        for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
+            if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
+                return namesAndValues.get(i + 1);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @param fieldNames a case-insensitive set of HTTP header field names.
+     */
+    public RawHeaders getAll(Set<String> fieldNames) {
+        RawHeaders result = new RawHeaders();
+        for (int i = 0; i < namesAndValues.size(); i += 2) {
+            String fieldName = namesAndValues.get(i);
+            if (fieldNames.contains(fieldName)) {
+                result.add(fieldName, namesAndValues.get(i + 1));
+            }
+        }
+        return result;
+    }
+
+    public String toHeaderString() {
+        StringBuilder result = new StringBuilder(256);
+        result.append(statusLine).append("\r\n");
+        for (int i = 0; i < namesAndValues.size(); i += 2) {
+            result.append(namesAndValues.get(i)).append(": ")
+                    .append(namesAndValues.get(i + 1)).append("\r\n");
+        }
+        result.append("\r\n");
+        return result.toString();
+    }
+
+    /**
+     * Returns an immutable map containing each field to its list of values. The
+     * status line is mapped to null.
+     */
+    public Map<String, List<String>> toMultimap() {
+        Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
+        for (int i = 0; i < namesAndValues.size(); i += 2) {
+            String fieldName = namesAndValues.get(i);
+            String value = namesAndValues.get(i + 1);
+
+            List<String> allValues = new ArrayList<String>();
+            List<String> otherValues = result.get(fieldName);
+            if (otherValues != null) {
+                allValues.addAll(otherValues);
+            }
+            allValues.add(value);
+            result.put(fieldName, Collections.unmodifiableList(allValues));
+        }
+        if (statusLine != null) {
+            result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine)));
+        }
+        return Collections.unmodifiableMap(result);
+    }
+
+    /**
+     * Creates a new instance from the given map of fields to values. If
+     * present, the null field's last element will be used to set the status
+     * line.
+     */
+    public static RawHeaders fromMultimap(Map<String, List<String>> map) {
+        RawHeaders result = new RawHeaders();
+        for (Entry<String, List<String>> entry : map.entrySet()) {
+            String fieldName = entry.getKey();
+            List<String> values = entry.getValue();
+            if (fieldName != null) {
+                result.addAll(fieldName, values);
+            } else if (!values.isEmpty()) {
+                result.setStatusLine(values.get(values.size() - 1));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Returns a list of alternating names and values. Names are all lower case.
+     * No names are repeated. If any name has multiple values, they are
+     * concatenated using "\0" as a delimiter.
+     */
+    public List<String> toNameValueBlock() {
+        Set<String> names = new HashSet<String>();
+        List<String> result = new ArrayList<String>();
+        for (int i = 0; i < namesAndValues.size(); i += 2) {
+            String name = namesAndValues.get(i).toLowerCase(Locale.US);
+            String value = namesAndValues.get(i + 1);
+
+            // TODO: promote this check to where names and values are created
+            if (name.length() == 0 || value.length() == 0
+                    || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
+                throw new IllegalArgumentException("Unexpected header: " + name + ": " + value);
+            }
+
+            // If we haven't seen this name before, add the pair to the end of the list...
+            if (names.add(name)) {
+                result.add(name);
+                result.add(value);
+                continue;
+            }
+
+            // ...otherwise concatenate the existing values and this value.
+            for (int j = 0; j < result.size(); j += 2) {
+                if (name.equals(result.get(j))) {
+                    result.set(j + 1, result.get(j + 1) + "\0" + value);
+                    break;
+                }
+            }
+        }
+        return result;
+    }
+
+    public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) {
+        if (nameValueBlock.size() % 2 != 0) {
+            throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock);
+        }
+        RawHeaders result = new RawHeaders();
+        for (int i = 0; i < nameValueBlock.size(); i += 2) {
+            String name = nameValueBlock.get(i);
+            String values = nameValueBlock.get(i + 1);
+            for (int start = 0; start < values.length();) {
+                int end = values.indexOf(start, '\0');
+                if (end == -1) {
+                    end = values.length();
+                }
+                result.namesAndValues.add(name);
+                result.namesAndValues.add(values.substring(start, end));
+                start = end + 1;
+            }
+        }
+        return result;
+    }
+}
diff --git a/src/main/java/libcore/net/http/RequestHeaders.java b/src/main/java/libcore/net/http/RequestHeaders.java
new file mode 100644
index 0000000..a84437e
--- /dev/null
+++ b/src/main/java/libcore/net/http/RequestHeaders.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.http;
+
+import java.net.URI;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Parsed HTTP request headers.
+ */
+public final class RequestHeaders {
+    private final URI uri;
+    private final RawHeaders headers;
+
+    /** Don't use a cache to satisfy this request. */
+    private boolean noCache;
+    private int maxAgeSeconds = -1;
+    private int maxStaleSeconds = -1;
+    private int minFreshSeconds = -1;
+
+    /**
+     * This field's name "only-if-cached" is misleading. It actually means "do
+     * not use the network". It is set by a client who only wants to make a
+     * request if it can be fully satisfied by the cache. Cached responses that
+     * would require validation (ie. conditional gets) are not permitted if this
+     * header is set.
+     */
+    private boolean onlyIfCached;
+
+    /**
+     * True if the request contains an authorization field. Although this isn't
+     * necessarily a shared cache, it follows the spec's strict requirements for
+     * shared caches.
+     */
+    private boolean hasAuthorization;
+
+    private int contentLength = -1;
+    private String transferEncoding;
+    private String userAgent;
+    private String host;
+    private String connection;
+    private String acceptEncoding;
+    private String contentType;
+    private String ifModifiedSince;
+    private String ifNoneMatch;
+    private String proxyAuthorization;
+
+    public RequestHeaders(URI uri, RawHeaders headers) {
+        this.uri = uri;
+        this.headers = headers;
+
+        HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
+            @Override public void handle(String directive, String parameter) {
+                if ("no-cache".equalsIgnoreCase(directive)) {
+                    noCache = true;
+                } else if ("max-age".equalsIgnoreCase(directive)) {
+                    maxAgeSeconds = HeaderParser.parseSeconds(parameter);
+                } else if ("max-stale".equalsIgnoreCase(directive)) {
+                    maxStaleSeconds = HeaderParser.parseSeconds(parameter);
+                } else if ("min-fresh".equalsIgnoreCase(directive)) {
+                    minFreshSeconds = HeaderParser.parseSeconds(parameter);
+                } else if ("only-if-cached".equalsIgnoreCase(directive)) {
+                    onlyIfCached = true;
+                }
+            }
+        };
+
+        for (int i = 0; i < headers.length(); i++) {
+            String fieldName = headers.getFieldName(i);
+            String value = headers.getValue(i);
+            if ("Cache-Control".equalsIgnoreCase(fieldName)) {
+                HeaderParser.parseCacheControl(value, handler);
+            } else if ("Pragma".equalsIgnoreCase(fieldName)) {
+                if ("no-cache".equalsIgnoreCase(value)) {
+                    noCache = true;
+                }
+            } else if ("If-None-Match".equalsIgnoreCase(fieldName)) {
+                ifNoneMatch = value;
+            } else if ("If-Modified-Since".equalsIgnoreCase(fieldName)) {
+                ifModifiedSince = value;
+            } else if ("Authorization".equalsIgnoreCase(fieldName)) {
+                hasAuthorization = true;
+            } else if ("Content-Length".equalsIgnoreCase(fieldName)) {
+                try {
+                    contentLength = Integer.parseInt(value);
+                } catch (NumberFormatException ignored) {
+                }
+            } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
+                transferEncoding = value;
+            } else if ("User-Agent".equalsIgnoreCase(fieldName)) {
+                userAgent = value;
+            } else if ("Host".equalsIgnoreCase(fieldName)) {
+                host = value;
+            } else if ("Connection".equalsIgnoreCase(fieldName)) {
+                connection = value;
+            } else if ("Accept-Encoding".equalsIgnoreCase(fieldName)) {
+                acceptEncoding = value;
+            } else if ("Content-Type".equalsIgnoreCase(fieldName)) {
+                contentType = value;
+            } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) {
+                proxyAuthorization = value;
+            }
+        }
+    }
+
+    public boolean isChunked() {
+        return "chunked".equalsIgnoreCase(transferEncoding);
+    }
+
+    public boolean hasConnectionClose() {
+        return "close".equalsIgnoreCase(connection);
+    }
+
+    public URI getUri() {
+        return uri;
+    }
+
+    public RawHeaders getHeaders() {
+        return headers;
+    }
+
+    public boolean isNoCache() {
+        return noCache;
+    }
+
+    public int getMaxAgeSeconds() {
+        return maxAgeSeconds;
+    }
+
+    public int getMaxStaleSeconds() {
+        return maxStaleSeconds;
+    }
+
+    public int getMinFreshSeconds() {
+        return minFreshSeconds;
+    }
+
+    public boolean isOnlyIfCached() {
+        return onlyIfCached;
+    }
+
+    public boolean hasAuthorization() {
+        return hasAuthorization;
+    }
+
+    public int getContentLength() {
+        return contentLength;
+    }
+
+    public String getTransferEncoding() {
+        return transferEncoding;
+    }
+
+    public String getUserAgent() {
+        return userAgent;
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    public String getConnection() {
+        return connection;
+    }
+
+    public String getAcceptEncoding() {
+        return acceptEncoding;
+    }
+
+    public String getContentType() {
+        return contentType;
+    }
+
+    public String getIfModifiedSince() {
+        return ifModifiedSince;
+    }
+
+    public String getIfNoneMatch() {
+        return ifNoneMatch;
+    }
+
+    public String getProxyAuthorization() {
+        return proxyAuthorization;
+    }
+
+    public void setChunked() {
+        if (this.transferEncoding != null) {
+            headers.removeAll("Transfer-Encoding");
+        }
+        headers.add("Transfer-Encoding", "chunked");
+        this.transferEncoding = "chunked";
+    }
+
+    public void setContentLength(int contentLength) {
+        if (this.contentLength != -1) {
+            headers.removeAll("Content-Length");
+        }
+        headers.add("Content-Length", Integer.toString(contentLength));
+        this.contentLength = contentLength;
+    }
+
+    public void setUserAgent(String userAgent) {
+        if (this.userAgent != null) {
+            headers.removeAll("User-Agent");
+        }
+        headers.add("User-Agent", userAgent);
+        this.userAgent = userAgent;
+    }
+
+    public void setHost(String host) {
+        if (this.host != null) {
+            headers.removeAll("Host");
+        }
+        headers.add("Host", host);
+        this.host = host;
+    }
+
+    public void setConnection(String connection) {
+        if (this.connection != null) {
+            headers.removeAll("Connection");
+        }
+        headers.add("Connection", connection);
+        this.connection = connection;
+    }
+
+    public void setAcceptEncoding(String acceptEncoding) {
+        if (this.acceptEncoding != null) {
+            headers.removeAll("Accept-Encoding");
+        }
+        headers.add("Accept-Encoding", acceptEncoding);
+        this.acceptEncoding = acceptEncoding;
+    }
+
+    public void setContentType(String contentType) {
+        if (this.contentType != null) {
+            headers.removeAll("Content-Type");
+        }
+        headers.add("Content-Type", contentType);
+        this.contentType = contentType;
+    }
+
+    public void setIfModifiedSince(Date date) {
+        if (ifModifiedSince != null) {
+            headers.removeAll("If-Modified-Since");
+        }
+        String formattedDate = HttpDate.format(date);
+        headers.add("If-Modified-Since", formattedDate);
+        ifModifiedSince = formattedDate;
+    }
+
+    public void setIfNoneMatch(String ifNoneMatch) {
+        if (this.ifNoneMatch != null) {
+            headers.removeAll("If-None-Match");
+        }
+        headers.add("If-None-Match", ifNoneMatch);
+        this.ifNoneMatch = ifNoneMatch;
+    }
+
+    /**
+     * Returns true if the request contains conditions that save the server from
+     * sending a response that the client has locally. When the caller adds
+     * conditions, this cache won't participate in the request.
+     */
+    public boolean hasConditions() {
+        return ifModifiedSince != null || ifNoneMatch != null;
+    }
+
+    public void addCookies(Map<String, List<String>> allCookieHeaders) {
+        for (Map.Entry<String, List<String>> entry : allCookieHeaders.entrySet()) {
+            String key = entry.getKey();
+            if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) {
+                headers.addAll(key, entry.getValue());
+            }
+        }
+    }
+}
diff --git a/src/main/java/libcore/net/http/ResponseHeaders.java b/src/main/java/libcore/net/http/ResponseHeaders.java
new file mode 100644
index 0000000..236ce87
--- /dev/null
+++ b/src/main/java/libcore/net/http/ResponseHeaders.java
@@ -0,0 +1,503 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.http;
+
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import libcore.util.Objects;
+import libcore.util.ResponseSource;
+
+/**
+ * Parsed HTTP response headers.
+ */
+public final class ResponseHeaders {
+
+    /** HTTP header name for the local time when the request was sent. */
+    private static final String SENT_MILLIS = "X-Android-Sent-Millis";
+
+    /** HTTP header name for the local time when the response was received. */
+    private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
+
+    private final URI uri;
+    private final RawHeaders headers;
+
+    /** The server's time when this response was served, if known. */
+    private Date servedDate;
+
+    /** The last modified date of the response, if known. */
+    private Date lastModified;
+
+    /**
+     * The expiration date of the response, if known. If both this field and the
+     * max age are set, the max age is preferred.
+     */
+    private Date expires;
+
+    /**
+     * Extension header set by HttpURLConnectionImpl specifying the timestamp
+     * when the HTTP request was first initiated.
+     */
+    private long sentRequestMillis;
+
+    /**
+     * Extension header set by HttpURLConnectionImpl specifying the timestamp
+     * when the HTTP response was first received.
+     */
+    private long receivedResponseMillis;
+
+    /**
+     * In the response, this field's name "no-cache" is misleading. It doesn't
+     * prevent us from caching the response; it only means we have to validate
+     * the response with the origin server before returning it. We can do this
+     * with a conditional get.
+     */
+    private boolean noCache;
+
+    /** If true, this response should not be cached. */
+    private boolean noStore;
+
+    /**
+     * The duration past the response's served date that it can be served
+     * without validation.
+     */
+    private int maxAgeSeconds = -1;
+
+    /**
+     * The "s-maxage" directive is the max age for shared caches. Not to be
+     * confused with "max-age" for non-shared caches, As in Firefox and Chrome,
+     * this directive is not honored by this cache.
+     */
+    private int sMaxAgeSeconds = -1;
+
+    /**
+     * This request header field's name "only-if-cached" is misleading. It
+     * actually means "do not use the network". It is set by a client who only
+     * wants to make a request if it can be fully satisfied by the cache.
+     * Cached responses that would require validation (ie. conditional gets) are
+     * not permitted if this header is set.
+     */
+    private boolean isPublic;
+    private boolean mustRevalidate;
+    private String etag;
+    private int ageSeconds = -1;
+
+    /** Case-insensitive set of field names. */
+    private Set<String> varyFields = Collections.emptySet();
+
+    private String contentEncoding;
+    private String transferEncoding;
+    private int contentLength = -1;
+    private String connection;
+
+    public ResponseHeaders(URI uri, RawHeaders headers) {
+        this.uri = uri;
+        this.headers = headers;
+
+        HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
+            @Override public void handle(String directive, String parameter) {
+                if ("no-cache".equalsIgnoreCase(directive)) {
+                    noCache = true;
+                } else if ("no-store".equalsIgnoreCase(directive)) {
+                    noStore = true;
+                } else if ("max-age".equalsIgnoreCase(directive)) {
+                    maxAgeSeconds = HeaderParser.parseSeconds(parameter);
+                } else if ("s-maxage".equalsIgnoreCase(directive)) {
+                    sMaxAgeSeconds = HeaderParser.parseSeconds(parameter);
+                } else if ("public".equalsIgnoreCase(directive)) {
+                    isPublic = true;
+                } else if ("must-revalidate".equalsIgnoreCase(directive)) {
+                    mustRevalidate = true;
+                }
+            }
+        };
+
+        for (int i = 0; i < headers.length(); i++) {
+            String fieldName = headers.getFieldName(i);
+            String value = headers.getValue(i);
+            if ("Cache-Control".equalsIgnoreCase(fieldName)) {
+                HeaderParser.parseCacheControl(value, handler);
+            } else if ("Date".equalsIgnoreCase(fieldName)) {
+                servedDate = HttpDate.parse(value);
+            } else if ("Expires".equalsIgnoreCase(fieldName)) {
+                expires = HttpDate.parse(value);
+            } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
+                lastModified = HttpDate.parse(value);
+            } else if ("ETag".equalsIgnoreCase(fieldName)) {
+                etag = value;
+            } else if ("Pragma".equalsIgnoreCase(fieldName)) {
+                if ("no-cache".equalsIgnoreCase(value)) {
+                    noCache = true;
+                }
+            } else if ("Age".equalsIgnoreCase(fieldName)) {
+                ageSeconds = HeaderParser.parseSeconds(value);
+            } else if ("Vary".equalsIgnoreCase(fieldName)) {
+                // Replace the immutable empty set with something we can mutate.
+                if (varyFields.isEmpty()) {
+                    varyFields = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+                }
+                for (String varyField : value.split(",")) {
+                    varyFields.add(varyField.trim());
+                }
+            } else if ("Content-Encoding".equalsIgnoreCase(fieldName)) {
+                contentEncoding = value;
+            } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
+                transferEncoding = value;
+            } else if ("Content-Length".equalsIgnoreCase(fieldName)) {
+                try {
+                    contentLength = Integer.parseInt(value);
+                } catch (NumberFormatException ignored) {
+                }
+            } else if ("Connection".equalsIgnoreCase(fieldName)) {
+                connection = value;
+            } else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) {
+                sentRequestMillis = Long.parseLong(value);
+            } else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
+                receivedResponseMillis = Long.parseLong(value);
+            }
+        }
+    }
+
+    public boolean isContentEncodingGzip() {
+        return "gzip".equalsIgnoreCase(contentEncoding);
+    }
+
+    public void stripContentEncoding() {
+        contentEncoding = null;
+        headers.removeAll("Content-Encoding");
+    }
+
+    public boolean isChunked() {
+        return "chunked".equalsIgnoreCase(transferEncoding);
+    }
+
+    public boolean hasConnectionClose() {
+        return "close".equalsIgnoreCase(connection);
+    }
+
+    public URI getUri() {
+        return uri;
+    }
+
+    public RawHeaders getHeaders() {
+        return headers;
+    }
+
+    public Date getServedDate() {
+        return servedDate;
+    }
+
+    public Date getLastModified() {
+        return lastModified;
+    }
+
+    public Date getExpires() {
+        return expires;
+    }
+
+    public boolean isNoCache() {
+        return noCache;
+    }
+
+    public boolean isNoStore() {
+        return noStore;
+    }
+
+    public int getMaxAgeSeconds() {
+        return maxAgeSeconds;
+    }
+
+    public int getSMaxAgeSeconds() {
+        return sMaxAgeSeconds;
+    }
+
+    public boolean isPublic() {
+        return isPublic;
+    }
+
+    public boolean isMustRevalidate() {
+        return mustRevalidate;
+    }
+
+    public String getEtag() {
+        return etag;
+    }
+
+    public Set<String> getVaryFields() {
+        return varyFields;
+    }
+
+    public String getContentEncoding() {
+        return contentEncoding;
+    }
+
+    public int getContentLength() {
+        return contentLength;
+    }
+
+    public String getConnection() {
+        return connection;
+    }
+
+    public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) {
+        this.sentRequestMillis = sentRequestMillis;
+        headers.add(SENT_MILLIS, Long.toString(sentRequestMillis));
+        this.receivedResponseMillis = receivedResponseMillis;
+        headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
+    }
+
+    /**
+     * Returns the current age of the response, in milliseconds. The calculation
+     * is specified by RFC 2616, 13.2.3 Age Calculations.
+     */
+    private long computeAge(long nowMillis) {
+        long apparentReceivedAge = servedDate != null
+                ? Math.max(0, receivedResponseMillis - servedDate.getTime())
+                : 0;
+        long receivedAge = ageSeconds != -1
+                ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds))
+                : apparentReceivedAge;
+        long responseDuration = receivedResponseMillis - sentRequestMillis;
+        long residentDuration = nowMillis - receivedResponseMillis;
+        return receivedAge + responseDuration + residentDuration;
+    }
+
+    /**
+     * Returns the number of milliseconds that the response was fresh for,
+     * starting from the served date.
+     */
+    private long computeFreshnessLifetime() {
+        if (maxAgeSeconds != -1) {
+            return TimeUnit.SECONDS.toMillis(maxAgeSeconds);
+        } else if (expires != null) {
+            long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;
+            long delta = expires.getTime() - servedMillis;
+            return delta > 0 ? delta : 0;
+        } else if (lastModified != null && uri.getRawQuery() == null) {
+            /*
+             * As recommended by the HTTP RFC and implemented in Firefox, the
+             * max age of a document should be defaulted to 10% of the
+             * document's age at the time it was served. Default expiration
+             * dates aren't used for URIs containing a query.
+             */
+            long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
+            long delta = servedMillis - lastModified.getTime();
+            return delta > 0 ? (delta / 10) : 0;
+        }
+        return 0;
+    }
+
+    /**
+     * Returns true if computeFreshnessLifetime used a heuristic. If we used a
+     * heuristic to serve a cached response older than 24 hours, we are required
+     * to attach a warning.
+     */
+    private boolean isFreshnessLifetimeHeuristic() {
+        return maxAgeSeconds == -1 && expires == null;
+    }
+
+    /**
+     * Returns true if this response can be stored to later serve another
+     * request.
+     */
+    public boolean isCacheable(RequestHeaders request) {
+        /*
+         * Always go to network for uncacheable response codes (RFC 2616, 13.4),
+         * This implementation doesn't support caching partial content.
+         */
+        int responseCode = headers.getResponseCode();
+        if (responseCode != HttpURLConnection.HTTP_OK
+                && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
+                && responseCode != HttpURLConnection.HTTP_MULT_CHOICE
+                && responseCode != HttpURLConnection.HTTP_MOVED_PERM
+                && responseCode != HttpURLConnection.HTTP_GONE) {
+            return false;
+        }
+
+        /*
+         * Responses to authorized requests aren't cacheable unless they include
+         * a 'public', 'must-revalidate' or 's-maxage' directive.
+         */
+        if (request.hasAuthorization()
+                && !isPublic
+                && !mustRevalidate
+                && sMaxAgeSeconds == -1) {
+            return false;
+        }
+
+        if (noStore) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns true if a Vary header contains an asterisk. Such responses cannot
+     * be cached.
+     */
+    public boolean hasVaryAll() {
+        return varyFields.contains("*");
+    }
+
+    /**
+     * Returns true if none of the Vary headers on this response have changed
+     * between {@code cachedRequest} and {@code newRequest}.
+     */
+    public boolean varyMatches(Map<String, List<String>> cachedRequest,
+            Map<String, List<String>> newRequest) {
+        for (String field : varyFields) {
+            if (!Objects.equal(cachedRequest.get(field), newRequest.get(field))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns the source to satisfy {@code request} given this cached response.
+     */
+    public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) {
+        /*
+         * If this response shouldn't have been stored, it should never be used
+         * as a response source. This check should be redundant as long as the
+         * persistence store is well-behaved and the rules are constant.
+         */
+        if (!isCacheable(request)) {
+            return ResponseSource.NETWORK;
+        }
+
+        if (request.isNoCache() || request.hasConditions()) {
+            return ResponseSource.NETWORK;
+        }
+
+        long ageMillis = computeAge(nowMillis);
+        long freshMillis = computeFreshnessLifetime();
+
+        if (request.getMaxAgeSeconds() != -1) {
+            freshMillis = Math.min(freshMillis,
+                    TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds()));
+        }
+
+        long minFreshMillis = 0;
+        if (request.getMinFreshSeconds() != -1) {
+            minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds());
+        }
+
+        long maxStaleMillis = 0;
+        if (!mustRevalidate && request.getMaxStaleSeconds() != -1) {
+            maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds());
+        }
+
+        if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
+            if (ageMillis + minFreshMillis >= freshMillis) {
+                headers.add("Warning", "110 HttpURLConnection \"Response is stale\"");
+            }
+            if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) {
+                headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
+            }
+            return ResponseSource.CACHE;
+        }
+
+        if (lastModified != null) {
+            request.setIfModifiedSince(lastModified);
+        } else if (servedDate != null) {
+            request.setIfModifiedSince(servedDate);
+        }
+
+        if (etag != null) {
+            request.setIfNoneMatch(etag);
+        }
+
+        return request.hasConditions()
+                ? ResponseSource.CONDITIONAL_CACHE
+                : ResponseSource.NETWORK;
+    }
+
+    /**
+     * Returns true if this cached response should be used; false if the
+     * network response should be used.
+     */
+    public boolean validate(ResponseHeaders networkResponse) {
+        if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
+            return true;
+        }
+
+        /*
+         * The HTTP spec says that if the network's response is older than our
+         * cached response, we may return the cache's response. Like Chrome (but
+         * unlike Firefox), this client prefers to return the newer response.
+         */
+        if (lastModified != null
+                && networkResponse.lastModified != null
+                && networkResponse.lastModified.getTime() < lastModified.getTime()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Combines this cached header with a network header as defined by RFC 2616,
+     * 13.5.3.
+     */
+    public ResponseHeaders combine(ResponseHeaders network) {
+        RawHeaders result = new RawHeaders();
+        result.setStatusLine(headers.getStatusLine());
+
+        for (int i = 0; i < headers.length(); i++) {
+            String fieldName = headers.getFieldName(i);
+            String value = headers.getValue(i);
+            if ("Warning".equals(fieldName) && value.startsWith("1")) {
+                continue; // drop 100-level freshness warnings
+            }
+            if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) {
+                result.add(fieldName, value);
+            }
+        }
+
+        for (int i = 0; i < network.headers.length(); i++) {
+            String fieldName = network.headers.getFieldName(i);
+            if (isEndToEnd(fieldName)) {
+                result.add(fieldName, network.headers.getValue(i));
+            }
+        }
+
+        return new ResponseHeaders(uri, result);
+    }
+
+    /**
+     * Returns true if {@code fieldName} is an end-to-end HTTP header, as
+     * defined by RFC 2616, 13.5.1.
+     */
+    private static boolean isEndToEnd(String fieldName) {
+        return !"Connection".equalsIgnoreCase(fieldName)
+                && !"Keep-Alive".equalsIgnoreCase(fieldName)
+                && !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
+                && !"Proxy-Authorization".equalsIgnoreCase(fieldName)
+                && !"TE".equalsIgnoreCase(fieldName)
+                && !"Trailers".equalsIgnoreCase(fieldName)
+                && !"Transfer-Encoding".equalsIgnoreCase(fieldName)
+                && !"Upgrade".equalsIgnoreCase(fieldName);
+    }
+}
diff --git a/src/main/java/libcore/net/http/RetryableOutputStream.java b/src/main/java/libcore/net/http/RetryableOutputStream.java
new file mode 100644
index 0000000..c8110be
--- /dev/null
+++ b/src/main/java/libcore/net/http/RetryableOutputStream.java
@@ -0,0 +1,72 @@
+/*
+ * 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 libcore.net.http;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import libcore.util.Libcore;
+
+/**
+ * An HTTP request body that's completely buffered in memory. This allows
+ * the post body to be transparently re-sent if the HTTP request must be
+ * sent multiple times.
+ */
+final class RetryableOutputStream extends AbstractHttpOutputStream {
+    private final int limit;
+    private final ByteArrayOutputStream content;
+
+    public RetryableOutputStream(int limit) {
+        this.limit = limit;
+        this.content = new ByteArrayOutputStream(limit);
+    }
+
+    public RetryableOutputStream() {
+        this.limit = -1;
+        this.content = new ByteArrayOutputStream();
+    }
+
+    @Override public synchronized void close() {
+        if (closed) {
+            return;
+        }
+        closed = true;
+        if (content.size() < limit) {
+            throw new IllegalStateException("content-length promised "
+                    + limit + " bytes, but received " + content.size());
+        }
+    }
+
+    @Override public synchronized void write(byte[] buffer, int offset, int count)
+            throws IOException {
+        checkNotClosed();
+        Libcore.checkOffsetAndCount(buffer.length, offset, count);
+        if (limit != -1 && content.size() > limit - count) {
+            throw new IOException("exceeded content-length limit of " + limit + " bytes");
+        }
+        content.write(buffer, offset, count);
+    }
+
+    public synchronized int contentLength() {
+        close();
+        return content.size();
+    }
+
+    public void writeToSocket(OutputStream socketOut) throws IOException  {
+        content.writeTo(socketOut);
+    }
+}
diff --git a/src/main/java/libcore/net/http/SpdyTransport.java b/src/main/java/libcore/net/http/SpdyTransport.java
new file mode 100644
index 0000000..d550ef7
--- /dev/null
+++ b/src/main/java/libcore/net/http/SpdyTransport.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2012 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 libcore.net.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.net.CacheRequest;
+import java.util.List;
+import libcore.net.spdy.SpdyConnection;
+import libcore.net.spdy.SpdyStream;
+
+final class SpdyTransport implements Transport {
+    private final HttpEngine httpEngine;
+    private final SpdyConnection spdyConnection;
+    private SpdyStream stream;
+
+    // TODO: set sentMillis
+    // TODO: set cookie stuff
+
+    SpdyTransport(HttpEngine httpEngine, SpdyConnection spdyConnection) {
+        this.httpEngine = httpEngine;
+        this.spdyConnection = spdyConnection;
+    }
+
+    @Override public OutputStream createRequestBody() throws IOException {
+        // TODO: if we aren't streaming up to the server, we should buffer the whole request
+        writeRequestHeaders();
+        return stream.getOutputStream();
+    }
+
+    @Override public void writeRequestHeaders() throws IOException {
+        if (stream != null) {
+            return;
+        }
+        RawHeaders requestHeaders = httpEngine.requestHeaders.getHeaders();
+        String version = httpEngine.connection.httpMinorVersion == 1 ? "HTTP/1.1" : "HTTP/1.0";
+        requestHeaders.addSpdyRequestHeaders(httpEngine.method, httpEngine.uri.getScheme(),
+                httpEngine.uri.getPath(), version);
+        boolean hasRequestBody = httpEngine.hasRequestBody();
+        boolean hasResponseBody = true;
+        stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(),
+                hasRequestBody, hasResponseBody);
+    }
+
+    @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override public void flushRequest() throws IOException {
+        stream.getOutputStream().close();
+    }
+
+    @Override public ResponseHeaders readResponseHeaders() throws IOException {
+        // TODO: fix the SPDY implementation so this throws a (buffered) IOException
+        try {
+            List<String> nameValueBlock = stream.getResponseHeaders();
+            RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
+            rawHeaders.computeResponseStatusLineFromSpdyHeaders();
+            return new ResponseHeaders(httpEngine.uri, rawHeaders);
+        } catch (InterruptedException e) {
+            InterruptedIOException rethrow = new InterruptedIOException();
+            rethrow.initCause(e);
+            throw rethrow;
+        }
+    }
+
+    @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
+        // TODO: handle HTTP responses that don't have a response body
+        return stream.getInputStream();
+    }
+
+    @Override public boolean makeReusable(OutputStream requestBodyOut, InputStream responseBodyIn) {
+        return true;
+    }
+}
diff --git a/src/main/java/libcore/net/http/Transport.java b/src/main/java/libcore/net/http/Transport.java
new file mode 100644
index 0000000..3d4c8dd
--- /dev/null
+++ b/src/main/java/libcore/net/http/Transport.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2012 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 libcore.net.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheRequest;
+
+interface Transport {
+    /**
+     * Returns an output stream where the request body can be written. The
+     * returned stream will of one of two types:
+     * <ul>
+     *     <li><strong>Direct.</strong> Bytes are written to the socket and
+     *     forgotten. This is most efficient, particularly for large request
+     *     bodies. The returned stream may be buffered; the caller must call
+     *     {@link #flushRequest} before reading the response.</li>
+     *     <li><strong>Buffered.</strong> Bytes are written to an in memory
+     *     buffer, and must be explicitly flushed with a call to {@link
+     *     #writeRequestBody}. This allows HTTP authorization (401, 407)
+     *     responses to be retransmitted transparently.</li>
+     * </ul>
+     */
+    // TODO: don't bother retransmitting the request body? It's quite a corner
+    // case and there's uncertainty whether Firefox or Chrome do this
+    OutputStream createRequestBody() throws IOException;
+
+    /**
+     * This should update the HTTP engine's sentRequestMillis field.
+     */
+    void writeRequestHeaders() throws IOException;
+
+    /**
+     * Sends the request body returned by {@link #createRequestBody} to the
+     * remote peer.
+     */
+    void writeRequestBody(RetryableOutputStream requestBody) throws IOException;
+
+    /**
+     * Flush the request body to the underlying socket.
+     */
+    void flushRequest() throws IOException;
+
+    /**
+     * Read response headers and update the cookie manager.
+     */
+    ResponseHeaders readResponseHeaders() throws IOException;
+
+    // TODO: make this the content stream?
+    InputStream getTransferStream(CacheRequest cacheRequest) throws IOException;
+
+    /**
+     * Returns true if the underlying connection can be recycled.
+     */
+    boolean makeReusable(OutputStream requestBodyOut, InputStream responseBodyIn);
+}
diff --git a/src/main/java/libcore/net/spdy/IncomingStreamHandler.java b/src/main/java/libcore/net/spdy/IncomingStreamHandler.java
new file mode 100644
index 0000000..69cc8e1
--- /dev/null
+++ b/src/main/java/libcore/net/spdy/IncomingStreamHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.spdy;
+
+import java.io.IOException;
+
+/**
+ * Listener to be notified when a connected peer creates a new stream.
+ */
+public interface IncomingStreamHandler {
+    IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() {
+        @Override public void receive(SpdyStream stream) throws IOException {
+            stream.close(SpdyStream.RST_REFUSED_STREAM);
+        }
+    };
+
+    /**
+     * Handle a new stream from this connection's peer. Implementations should
+     * respond by either {@link SpdyStream#reply(java.util.List) replying to the
+     * stream} or {@link SpdyStream#close(int) closing it}. This response does
+     * not need to be synchronous.
+     */
+    void receive(SpdyStream stream) throws IOException;
+}
diff --git a/src/main/java/libcore/net/spdy/SpdyConnection.java b/src/main/java/libcore/net/spdy/SpdyConnection.java
new file mode 100644
index 0000000..44194f1
--- /dev/null
+++ b/src/main/java/libcore/net/spdy/SpdyConnection.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.spdy;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * A socket connection to a remote peer. A connection hosts streams which can
+ * send and receive data.
+ */
+public final class SpdyConnection implements Closeable {
+
+    /*
+     * Socket writes are guarded by this. Socket reads are unguarded but are
+     * only made by the reader thread.
+     */
+
+    static final int FLAG_FIN = 0x01;
+    static final int FLAG_UNIDIRECTIONAL = 0x02;
+
+    static final int TYPE_EOF = -1;
+    static final int TYPE_DATA = 0x00;
+    static final int TYPE_SYN_STREAM = 0x01;
+    static final int TYPE_SYN_REPLY = 0x02;
+    static final int TYPE_RST_STREAM = 0x03;
+    static final int TYPE_SETTINGS = 0x04;
+    static final int TYPE_NOOP = 0x05;
+    static final int TYPE_PING = 0x06;
+    static final int TYPE_GOAWAY = 0x07;
+    static final int TYPE_HEADERS = 0x08;
+    static final int VERSION = 2;
+
+    /** Guarded by this. */
+    private int nextStreamId;
+    private final SpdyReader spdyReader;
+    private final SpdyWriter spdyWriter;
+    private final Executor executor;
+
+    /**
+     * User code to run in response to an incoming stream. This must not be run
+     * on the read thread, otherwise a deadlock is possible.
+     */
+    private final IncomingStreamHandler handler;
+
+    private final Map<Integer, SpdyStream> streams = Collections.synchronizedMap(
+            new HashMap<Integer, SpdyStream>());
+
+    private SpdyConnection(Builder builder) {
+        nextStreamId = builder.client ? 1 : 2;
+        spdyReader = new SpdyReader(builder.in);
+        spdyWriter = new SpdyWriter(builder.out);
+        handler = builder.handler;
+
+        String name = isClient() ? "ClientReader" : "ServerReader";
+        executor = builder.executor != null
+                ? builder.executor
+                : Executors.newCachedThreadPool(Threads.newThreadFactory(name));
+        executor.execute(new Reader());
+    }
+
+    /**
+     * Returns true if this peer initiated the connection.
+     */
+    public boolean isClient() {
+        return nextStreamId % 2 == 1;
+    }
+
+    private SpdyStream getStream(int id) {
+        SpdyStream stream = streams.get(id);
+        if (stream == null) {
+            throw new UnsupportedOperationException("TODO " + id + "; " + streams); // TODO: rst stream
+        }
+        return stream;
+    }
+
+    void removeStream(int streamId) {
+        streams.remove(streamId);
+    }
+
+    /**
+     * Returns a new locally-initiated stream.
+     *
+     * @param out true to create an output stream that we can use to send data
+     *     to the remote peer. Corresponds to {@code FLAG_FIN}.
+     * @param in true to create an input stream that the remote peer can use to
+     *     send data to us. Corresponds to {@code FLAG_UNIDIRECTIONAL}.
+     */
+    public synchronized SpdyStream newStream(List<String> requestHeaders, boolean out, boolean in)
+            throws IOException {
+        int streamId = nextStreamId; // TODO
+        nextStreamId += 2;
+        int flags = (out ? 0 : FLAG_FIN) | (in ? 0 : FLAG_UNIDIRECTIONAL);
+        int associatedStreamId = 0;  // TODO
+        int priority = 0; // TODO
+
+        SpdyStream result = new SpdyStream(streamId, this, requestHeaders, flags);
+        streams.put(streamId, result);
+
+        spdyWriter.flags = flags;
+        spdyWriter.streamId = streamId;
+        spdyWriter.associatedStreamId = associatedStreamId;
+        spdyWriter.priority = priority;
+        spdyWriter.nameValueBlock = requestHeaders;
+        spdyWriter.synStream();
+
+        return result;
+    }
+
+    synchronized void writeSynReply(int streamId, List<String> alternating) throws IOException {
+        int flags = 0; // TODO
+        spdyWriter.flags = flags;
+        spdyWriter.streamId = streamId;
+        spdyWriter.nameValueBlock = alternating;
+        spdyWriter.synReply();
+    }
+
+    /** Writes a complete data frame. */
+    synchronized void writeFrame(byte[] bytes, int offset, int length) throws IOException {
+        spdyWriter.out.write(bytes, offset, length);
+    }
+
+    void writeSynResetLater(final int streamId, final int statusCode) {
+        executor.execute(new Runnable() {
+            @Override public void run() {
+                try {
+                    writeSynReset(streamId, statusCode);
+                } catch (IOException ignored) {
+                }
+            }
+        });
+    }
+
+    synchronized void writeSynReset(int streamId, int statusCode) throws IOException {
+        int flags = 0; // TODO
+        spdyWriter.flags = flags;
+        spdyWriter.streamId = streamId;
+        spdyWriter.statusCode = statusCode;
+        spdyWriter.synReset();
+    }
+
+    public synchronized void flush() throws IOException {
+        spdyWriter.out.flush();
+    }
+
+    @Override public synchronized void close() throws IOException {
+        // TODO: graceful close; send RST frames
+        // TODO: close all streams to release waiting readers
+        if (executor instanceof ExecutorService) {
+            ((ExecutorService) executor).shutdown();
+        }
+    }
+
+    public static class Builder {
+        private InputStream in;
+        private OutputStream out;
+        private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS;
+        private Executor executor;
+        public boolean client;
+
+        /**
+         * @param client true if this peer initiated the connection; false if
+         *     this peer accepted the connection.
+         */
+        public Builder(boolean client, Socket socket) throws IOException {
+            this(client, socket.getInputStream(), socket.getOutputStream());
+        }
+
+        /**
+         * @param client true if this peer initiated the connection; false if this
+         *     peer accepted the connection.
+         */
+        public Builder(boolean client, InputStream in, OutputStream out) {
+            this.client = client;
+            this.in = in;
+            this.out = out;
+        }
+
+        public Builder executor(Executor executor) {
+            this.executor = executor;
+            return this;
+        }
+
+        public Builder handler(IncomingStreamHandler handler) {
+            this.handler = handler;
+            return this;
+        }
+
+        public SpdyConnection build() {
+            return new SpdyConnection(this);
+        }
+    }
+
+    private class Reader implements Runnable {
+        @Override public void run() {
+            try {
+                while (readFrame()) {
+                }
+                close();
+            } catch (Throwable e) {
+                e.printStackTrace(); // TODO
+            }
+        }
+
+        private boolean readFrame() throws IOException {
+            switch (spdyReader.nextFrame()) {
+            case TYPE_EOF:
+                return false;
+
+            case TYPE_DATA:
+                getStream(spdyReader.streamId)
+                        .receiveData(spdyReader.in, spdyReader.flags, spdyReader.length);
+                return true;
+
+            case TYPE_SYN_STREAM:
+                final SpdyStream stream = new SpdyStream(spdyReader.streamId, SpdyConnection.this,
+                        spdyReader.nameValueBlock, spdyReader.flags);
+                SpdyStream previous = streams.put(spdyReader.streamId, stream);
+                if (previous != null) {
+                    previous.close(SpdyStream.RST_PROTOCOL_ERROR);
+                }
+                executor.execute(new Runnable() {
+                    @Override public void run() {
+                        try {
+                            handler.receive(stream);
+                        } catch (IOException e) {
+                            throw new RuntimeException(e);
+                        }
+                    }
+                });
+                return true;
+
+            case TYPE_SYN_REPLY:
+                // TODO: honor flags
+                getStream(spdyReader.streamId).receiveReply(spdyReader.nameValueBlock);
+                return true;
+
+            case TYPE_RST_STREAM:
+                getStream(spdyReader.streamId).receiveRstStream(spdyReader.statusCode);
+                return true;
+
+            case SpdyConnection.TYPE_SETTINGS:
+                // TODO: implement
+                System.out.println("Unimplemented TYPE_SETTINGS frame discarded");
+                return true;
+
+            case SpdyConnection.TYPE_NOOP:
+            case SpdyConnection.TYPE_PING:
+            case SpdyConnection.TYPE_GOAWAY:
+            case SpdyConnection.TYPE_HEADERS:
+                throw new UnsupportedOperationException();
+
+            default:
+                // TODO: throw IOException here?
+                return false;
+            }
+        }
+    }
+}
diff --git a/src/main/java/libcore/net/spdy/SpdyReader.java b/src/main/java/libcore/net/spdy/SpdyReader.java
new file mode 100644
index 0000000..38557c9
--- /dev/null
+++ b/src/main/java/libcore/net/spdy/SpdyReader.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.spdy;
+
+import java.io.DataInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+import libcore.io.Streams;
+
+/**
+ * Read version 2 SPDY frames.
+ */
+final class SpdyReader {
+    public static final Charset UTF_8 = Charset.forName("UTF-8");
+    private static final String DICTIONARY_STRING = ""
+            + "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-"
+            + "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi"
+            + "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser"
+            + "-agent10010120020120220320420520630030130230330430530630740040140240340440"
+            + "5406407408409410411412413414415416417500501502503504505accept-rangesageeta"
+            + "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic"
+            + "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran"
+            + "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati"
+            + "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo"
+            + "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe"
+            + "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic"
+            + "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1"
+            + ".1statusversionurl\0";
+    public static final byte[] DICTIONARY;
+    static {
+        try {
+            DICTIONARY = DICTIONARY_STRING.getBytes("UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    public final DataInputStream in;
+    public int flags;
+    public int length;
+    public int streamId;
+    public int associatedStreamId;
+    public int version;
+    public int type;
+    public int priority;
+    public int statusCode;
+
+    public List<String> nameValueBlock;
+    private final DataInputStream nameValueBlockIn;
+    private int compressedLimit;
+
+    SpdyReader(InputStream in) {
+        this.in = new DataInputStream(in);
+        this.nameValueBlockIn = newNameValueBlockStream();
+    }
+
+    /**
+     * Advance to the next frame in the source data. If the frame is of
+     * TYPE_DATA, it's the caller's responsibility to read length bytes from
+     * the input stream before the next call to nextFrame().
+     */
+    public int nextFrame() throws IOException {
+        int w1;
+        try {
+            w1 = in.readInt();
+        } catch (EOFException e) {
+            return SpdyConnection.TYPE_EOF;
+        }
+        int w2 = in.readInt();
+
+        boolean control = (w1 & 0x80000000) != 0;
+        flags = (w2 & 0xff000000) >>> 24;
+        length = (w2 & 0xffffff);
+
+        if (control) {
+            version = (w1 & 0x7fff0000) >>> 16;
+            type = (w1 & 0xffff);
+
+            switch (type) {
+            case SpdyConnection.TYPE_SYN_STREAM:
+                readSynStream();
+                return SpdyConnection.TYPE_SYN_STREAM;
+
+            case SpdyConnection.TYPE_SYN_REPLY:
+                readSynReply();
+                return SpdyConnection.TYPE_SYN_REPLY;
+
+            case SpdyConnection.TYPE_RST_STREAM:
+                readSynReset();
+                return SpdyConnection.TYPE_RST_STREAM;
+
+            default:
+                readControlFrame();
+                return type;
+            }
+        } else {
+            streamId = w1 & 0x7fffffff;
+            return SpdyConnection.TYPE_DATA;
+        }
+    }
+
+    private void readSynStream() throws IOException {
+        int w1 = in.readInt();
+        int w2 = in.readInt();
+        int s3 = in.readShort();
+        streamId = w1 & 0x7fffffff;
+        associatedStreamId = w2 & 0x7fffffff;
+        priority = s3 & 0xc000 >> 14;
+        // int unused = s3 & 0x3fff;
+        nameValueBlock = readNameValueBlock(length - 10);
+    }
+
+    private void readSynReply() throws IOException {
+        int w1 = in.readInt();
+        in.readShort(); // unused
+        streamId = w1 & 0x7fffffff;
+        nameValueBlock = readNameValueBlock(length - 6);
+    }
+
+    private void readSynReset() throws IOException {
+        streamId = in.readInt() & 0x7fffffff;
+        statusCode = in.readInt();
+    }
+
+    private void readControlFrame() throws IOException {
+        Streams.skipByReading(in, length);
+    }
+
+    private DataInputStream newNameValueBlockStream() {
+        // Limit the inflater input stream to only those bytes in the Name/Value block.
+        final InputStream throttleStream = new InputStream() {
+            @Override public int read() throws IOException {
+                return Streams.readSingleByte(this);
+            }
+
+            @Override public int read(byte[] buffer, int offset, int byteCount) throws IOException {
+                byteCount = Math.min(byteCount, compressedLimit);
+                int consumed = in.read(buffer, offset, byteCount);
+                compressedLimit -= consumed;
+                return consumed;
+            }
+
+            @Override public void close() throws IOException {
+                in.close();
+            }
+        };
+
+        // Subclass inflater to install a dictionary when it's needed.
+        Inflater inflater = new Inflater() {
+            @Override
+            public int inflate(byte[] buffer, int offset, int count) throws DataFormatException {
+                int result = super.inflate(buffer, offset, count);
+                if (result == 0 && needsDictionary()) {
+                    setDictionary(DICTIONARY);
+                    result = super.inflate(buffer, offset, count);
+                }
+                return result;
+            }
+        };
+
+        return new DataInputStream(new InflaterInputStream(throttleStream, inflater));
+    }
+
+    private List<String> readNameValueBlock(int length) throws IOException {
+        this.compressedLimit += length;
+        try {
+            List<String> entries = new ArrayList<String>();
+
+            int numberOfPairs = nameValueBlockIn.readShort();
+            for (int i = 0; i < numberOfPairs; i++) {
+                String name = readString();
+                String values = readString();
+                if (name.length() == 0 || values.length() == 0) {
+                    throw new IOException(); // TODO: PROTOCOL ERROR
+                }
+                entries.add(name);
+                entries.add(values);
+            }
+
+            if (compressedLimit != 0) {
+                Logger.getLogger(getClass().getName())
+                        .warning("compressedLimit > 0" + compressedLimit);
+            }
+
+            return entries;
+        } catch (DataFormatException e) {
+            throw new IOException(e);
+        }
+    }
+
+    private String readString() throws DataFormatException, IOException {
+        int length = nameValueBlockIn.readShort();
+        byte[] bytes = new byte[length];
+        Streams.readFully(nameValueBlockIn, bytes);
+        return new String(bytes, 0, length, "UTF-8");
+    }
+}
diff --git a/src/main/java/libcore/net/spdy/SpdyServer.java b/src/main/java/libcore/net/spdy/SpdyServer.java
new file mode 100644
index 0000000..52af83c
--- /dev/null
+++ b/src/main/java/libcore/net/spdy/SpdyServer.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.spdy;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A basic SPDY server that serves the contents of a local directory. This
+ * server will service a single SPDY connection.
+ */
+public final class SpdyServer implements IncomingStreamHandler {
+    private final File baseDirectory;
+
+    public SpdyServer(File baseDirectory) {
+        this.baseDirectory = baseDirectory;
+    }
+
+    private void run() throws IOException {
+        ServerSocket serverSocket = new ServerSocket(8888);
+        serverSocket.setReuseAddress(true);
+
+        Socket socket = serverSocket.accept();
+        new SpdyConnection.Builder(false, socket)
+                .handler(this)
+                .build();
+    }
+
+    @Override public void receive(final SpdyStream stream) throws IOException {
+        List<String> requestHeaders = stream.getRequestHeaders();
+        String path = null;
+        for (int i = 0; i < requestHeaders.size(); i += 2) {
+            String s = requestHeaders.get(i);
+            if ("url".equals(s)) {
+                path = requestHeaders.get(i + 1);
+                break;
+            }
+        }
+
+        if (path == null) {
+            // TODO: send bad request error
+            throw new AssertionError();
+        }
+
+        File file = new File(baseDirectory + path);
+
+        if (file.exists() && !file.isDirectory()) {
+            serveFile(stream, file);
+        } else {
+            send404(stream, path);
+        }
+    }
+
+    private void send404(SpdyStream stream, String path) throws IOException {
+        List<String> responseHeaders = Arrays.asList(
+                "status", "404",
+                "version", "HTTP/1.1",
+                "content-type", "text/plain"
+        );
+        OutputStream out = stream.reply(responseHeaders);
+        String text = "Not found: " + path;
+        out.write(text.getBytes("UTF-8"));
+        out.close();
+    }
+
+    private void serveFile(SpdyStream stream, File file) throws IOException {
+        InputStream in = new FileInputStream(file);
+        byte[] buffer = new byte[8192];
+        OutputStream out = stream.reply(Arrays.asList(
+                "status", "200",
+                "version", "HTTP/1.1",
+                "content-type", contentType(file)
+        ));
+        int count;
+        while ((count = in.read(buffer)) != -1) {
+            out.write(buffer, 0, count);
+        }
+        out.close();
+    }
+
+    private String contentType(File file) {
+        return file.getName().endsWith(".html") ? "text/html" : "text/plain";
+    }
+
+    public static void main(String... args) throws IOException {
+        if (args.length != 1 || args[0].startsWith("-")) {
+            System.out.println("Usage: SpdyServer <base directory>");
+            return;
+        }
+
+        new SpdyServer(new File(args[0])).run();
+    }
+}
diff --git a/src/main/java/libcore/net/spdy/SpdyStream.java b/src/main/java/libcore/net/spdy/SpdyStream.java
new file mode 100644
index 0000000..4588a4a
--- /dev/null
+++ b/src/main/java/libcore/net/spdy/SpdyStream.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.spdy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.util.List;
+import libcore.io.Streams;
+import libcore.util.Libcore;
+
+import static java.nio.ByteOrder.BIG_ENDIAN;
+
+/**
+ * A logical bidirectional stream.
+ */
+public final class SpdyStream {
+
+    /*
+     * Internal state is guarded by this. No long-running or potentially
+     * blocking operations are performed while the lock is held.
+     */
+
+    private static final int DATA_FRAME_HEADER_LENGTH = 8;
+
+    public static final int RST_PROTOCOL_ERROR = 1;
+    public static final int RST_INVALID_STREAM = 2;
+    public static final int RST_REFUSED_STREAM = 3;
+    public static final int RST_UNSUPPORTED_VERSION = 4;
+    public static final int RST_CANCEL = 5;
+    public static final int RST_INTERNAL_ERROR = 6;
+    public static final int RST_FLOW_CONTROL_ERROR = 7;
+
+    private final int id;
+    private final SpdyConnection connection;
+
+    /** Headers sent by the stream initiator. Immutable and non null. */
+    private final List<String> requestHeaders;
+
+    /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */
+    private List<String> responseHeaders;
+
+    private final SpdyDataInputStream in = new SpdyDataInputStream();
+    private final SpdyDataOutputStream out = new SpdyDataOutputStream();
+
+    /**
+     * The reason why this stream was abnormally closed. If there are multiple
+     * reasons to abnormally close this stream (such as both peers closing it
+     * near-simultaneously) then this is the first reason known to this peer.
+     */
+    private int rstStatusCode = -1;
+
+    /**
+     * True if either side has shut down the input stream. We will receive no
+     * more bytes beyond those already in the buffer. Guarded by this.
+     */
+    private boolean inFinished;
+
+    /**
+     * True if either side has shut down the output stream. We will write no
+     * more bytes to the output stream. Guarded by this.
+     */
+    private boolean outFinished;
+
+    SpdyStream(int id, SpdyConnection connection, List<String> requestHeaders, int flags) {
+        this.id = id;
+        this.connection = connection;
+        this.requestHeaders = requestHeaders;
+
+        if (isLocallyInitiated()) {
+            // I am the sender
+            inFinished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0;
+            outFinished = (flags & SpdyConnection.FLAG_FIN) != 0;
+        } else {
+            // I am the receiver
+            inFinished = (flags & SpdyConnection.FLAG_FIN) != 0;
+            outFinished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0;
+        }
+    }
+
+    /**
+     * Returns true if this stream was created by this peer.
+     */
+    public boolean isLocallyInitiated() {
+        boolean streamIsClient = (id % 2 == 1);
+        return connection.isClient() == streamIsClient;
+    }
+
+    public SpdyConnection getConnection() {
+        return connection;
+    }
+
+    public List<String> getRequestHeaders() {
+        return requestHeaders;
+    }
+
+    public synchronized List<String> getResponseHeaders() throws InterruptedException {
+        while (responseHeaders == null && rstStatusCode == -1) {
+            wait();
+        }
+        return responseHeaders;
+    }
+
+    /**
+     * Returns the reason why this stream was closed, or -1 if it closed
+     * normally or has not yet been closed.
+     */
+    public synchronized int getRstStatusCode() { // TODO: rename this?
+        return rstStatusCode;
+    }
+
+    public InputStream getInputStream() {
+        return in;
+    }
+
+    public OutputStream getOutputStream() {
+        if (!isLocallyInitiated()) {
+            throw new IllegalStateException("use reply for a remotely initiated stream");
+        }
+        return out;
+    }
+
+    /**
+     * Sends a reply.
+     */
+    // TODO: support reply with FIN
+    public synchronized OutputStream reply(List<String> responseHeaders) throws IOException {
+        if (responseHeaders == null) {
+            throw new NullPointerException("responseHeaders == null");
+        }
+        if (isLocallyInitiated()) {
+            throw new IllegalStateException("cannot reply to a locally initiated stream");
+        }
+        synchronized (this) {
+            if (this.responseHeaders != null) {
+                throw new IllegalStateException("reply already sent");
+            }
+            this.responseHeaders = responseHeaders;
+        }
+        connection.writeSynReply(id, responseHeaders);
+        return out;
+    }
+
+    /**
+     * Abnormally terminate this stream.
+     */
+    public synchronized void close(int rstStatusCode) {
+        // TODO: no-op if inFinished == true and outFinished == true ?
+        if (this.rstStatusCode != -1) {
+            this.rstStatusCode = rstStatusCode;
+            inFinished = true;
+            outFinished = true;
+            connection.removeStream(id);
+            notifyAll();
+            connection.writeSynResetLater(id, rstStatusCode);
+        }
+    }
+
+    synchronized void receiveReply(List<String> strings) throws IOException {
+        if (!isLocallyInitiated() || responseHeaders != null) {
+            throw new IOException(); // TODO: send RST
+        }
+        responseHeaders = strings;
+        notifyAll();
+    }
+
+    synchronized void receiveData(InputStream in, int flags, int length) throws IOException {
+        this.in.receive(in, length);
+        if ((flags & SpdyConnection.FLAG_FIN) != 0) {
+            inFinished = true;
+            notifyAll();
+        }
+    }
+
+    synchronized void receiveRstStream(int statusCode) {
+        if (rstStatusCode != -1) {
+            rstStatusCode = statusCode;
+            inFinished = true;
+            outFinished = true;
+            notifyAll();
+        }
+    }
+
+    /**
+     * An input stream that reads the incoming data frames of a stream. Although
+     * this class uses synchronization to safely receive incoming data frames,
+     * it is not intended for use by multiple readers.
+     */
+    private final class SpdyDataInputStream extends InputStream {
+        /*
+         * Store incoming data bytes in a circular buffer. When the buffer is
+         * empty, pos == -1. Otherwise pos is the first byte to read and limit
+         * is the first byte to write.
+         *
+         * { - - - X X X X - - - }
+         *         ^       ^
+         *        pos    limit
+         *
+         * { X X X - - - - X X X }
+         *         ^       ^
+         *       limit    pos
+         */
+
+        private final byte[] buffer = new byte[64 * 1024]; // 64KiB specified by TODO
+        /** the next byte to be read, or -1 if the buffer is empty. Never buffer.length */
+        private int pos = -1;
+        /** the last byte to be read. Never buffer.length */
+        private int limit;
+        /** True if the caller has closed this stream. */
+        private boolean closed;
+
+        @Override public int available() throws IOException {
+            synchronized (SpdyStream.this) {
+                checkNotClosed();
+                if (pos == -1) {
+                    return 0;
+                } else if (limit > pos) {
+                    return limit - pos;
+                } else {
+                    return limit + (buffer.length - pos);
+                }
+            }
+        }
+
+        @Override public int read() throws IOException {
+            return Streams.readSingleByte(this);
+        }
+
+        @Override public int read(byte[] b, int offset, int count) throws IOException {
+            synchronized (SpdyStream.this) {
+                checkNotClosed();
+                Libcore.checkOffsetAndCount(b.length, offset, count);
+
+                while (pos == -1 && !inFinished) {
+                    try {
+                        SpdyStream.this.wait();
+                    } catch (InterruptedException e) {
+                        throw new InterruptedIOException();
+                    }
+                }
+
+                if (pos == -1) {
+                    return -1;
+                }
+
+                int copied = 0;
+
+                // drain from [pos..buffer.length)
+                if (limit <= pos) {
+                    int bytesToCopy = Math.min(count, buffer.length - pos);
+                    System.arraycopy(buffer, pos, b, offset, bytesToCopy);
+                    pos += bytesToCopy;
+                    copied += bytesToCopy;
+                    if (pos == buffer.length) {
+                        pos = 0;
+                    }
+                }
+
+                // drain from [pos..limit)
+                if (copied < count) {
+                    int bytesToCopy = Math.min(limit - pos, count - copied);
+                    System.arraycopy(buffer, pos, b, offset + copied, bytesToCopy);
+                    pos += bytesToCopy;
+                    copied += bytesToCopy;
+                }
+
+                // TODO: notify peer of flow-control
+
+                if (pos == limit) {
+                    pos = -1;
+                    limit = 0;
+                }
+
+                return copied;
+            }
+        }
+
+        void receive(InputStream in, int byteCount) throws IOException {
+            if (inFinished) {
+                return; // ignore this; probably a benign race
+            }
+            if (byteCount == 0) {
+                return;
+            }
+
+            if (byteCount > buffer.length - available()) {
+                throw new IOException(); // TODO: RST the stream
+            }
+
+            // fill [limit..buffer.length)
+            if (pos < limit) {
+                int firstCopyCount = Math.min(byteCount, buffer.length - limit);
+                Streams.readFully(in, buffer, limit, firstCopyCount);
+                limit += firstCopyCount;
+                byteCount -= firstCopyCount;
+                if (limit == buffer.length) {
+                    limit = 0;
+                }
+            }
+
+            // fill [limit..pos)
+            if (byteCount > 0) {
+                Streams.readFully(in, buffer, limit, byteCount);
+                limit += byteCount;
+            }
+
+            if (pos == -1) {
+                pos = 0;
+                SpdyStream.this.notifyAll();
+            }
+        }
+
+        @Override public void close() throws IOException {
+            closed = true;
+            // TODO: send RST to peer if !inFinished
+        }
+
+        private void checkNotClosed() throws IOException {
+            if (closed) {
+                throw new IOException("stream closed");
+            }
+        }
+    }
+
+    /**
+     * An output stream that writes outgoing data frames of a stream. This class
+     * is not thread safe.
+     */
+    private final class SpdyDataOutputStream extends OutputStream {
+        private final byte[] buffer = new byte[8192];
+        private int pos = DATA_FRAME_HEADER_LENGTH;
+
+        /** True if the caller has closed this stream. */
+        private boolean closed;
+
+        @Override public void write(int b) throws IOException {
+            Streams.writeSingleByte(this, b);
+        }
+
+        @Override public void write(byte[] bytes, int offset, int count) throws IOException {
+            Libcore.checkOffsetAndCount(bytes.length, offset, count);
+            checkNotClosed();
+
+            while (count > 0) {
+                if (pos == buffer.length) {
+                    writeFrame(false);
+                }
+                int bytesToCopy = Math.min(count, buffer.length - pos);
+                System.arraycopy(bytes, offset, buffer, pos, bytesToCopy);
+                pos += bytesToCopy;
+                offset += bytesToCopy;
+                count -= bytesToCopy;
+            }
+        }
+
+        @Override public void flush() throws IOException {
+            checkNotClosed();
+            if (pos > DATA_FRAME_HEADER_LENGTH) {
+                writeFrame(false);
+                connection.flush();
+            }
+        }
+
+        @Override public void close() throws IOException {
+            if (!closed) {
+                closed = true;
+                writeFrame(true);
+                connection.flush();
+            }
+        }
+
+        private void writeFrame(boolean last) throws IOException {
+            int flags = 0;
+            if (last) {
+                flags |= SpdyConnection.FLAG_FIN;
+            }
+            int length = pos - DATA_FRAME_HEADER_LENGTH;
+            Libcore.pokeInt(buffer, 0, id & 0x7fffffff, BIG_ENDIAN);
+            Libcore.pokeInt(buffer, 4, (flags & 0xff) << 24 | length & 0xffffff, BIG_ENDIAN);
+            connection.writeFrame(buffer, 0, pos);
+            pos = DATA_FRAME_HEADER_LENGTH;
+        }
+
+        private void checkNotClosed() throws IOException {
+            synchronized (SpdyStream.this) {
+                if (closed) {
+                    throw new IOException("stream closed");
+                }
+                if (outFinished) {
+                    throw new IOException("output stream finished "
+                            + "(RST status code=" + rstStatusCode + ")");
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/libcore/net/spdy/SpdyWriter.java b/src/main/java/libcore/net/spdy/SpdyWriter.java
new file mode 100644
index 0000000..5bc4644
--- /dev/null
+++ b/src/main/java/libcore/net/spdy/SpdyWriter.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.spdy;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+/**
+ * Write version 2 SPDY frames.
+ */
+final class SpdyWriter {
+    final DataOutputStream out;
+    public int flags;
+    public int streamId;
+    public int associatedStreamId;
+    public int priority;
+    public int statusCode;
+
+    public List<String> nameValueBlock;
+    private final ByteArrayOutputStream nameValueBlockBuffer;
+    private final DataOutputStream nameValueBlockOut;
+
+    SpdyWriter(OutputStream out) {
+        this.out = new DataOutputStream(out);
+
+        Deflater deflater = new Deflater();
+        deflater.setDictionary(SpdyReader.DICTIONARY);
+        nameValueBlockBuffer = new ByteArrayOutputStream();
+        nameValueBlockOut = new DataOutputStream(
+                new DeflaterOutputStream(nameValueBlockBuffer, deflater, true));
+    }
+
+    public void synStream() throws IOException {
+        writeNameValueBlockToBuffer();
+        int length = 10 + nameValueBlockBuffer.size();
+        int type = SpdyConnection.TYPE_SYN_STREAM;
+
+        int unused = 0;
+        out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+        out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+        out.writeInt(streamId & 0x7fffffff);
+        out.writeInt(associatedStreamId & 0x7fffffff);
+        out.writeShort((priority & 0x3) << 30 | (unused & 0x3FFF) << 16);
+        nameValueBlockBuffer.writeTo(out);
+        out.flush();
+    }
+
+    public void synReply() throws IOException {
+        writeNameValueBlockToBuffer();
+        int type = SpdyConnection.TYPE_SYN_REPLY;
+        int length = nameValueBlockBuffer.size() + 6;
+        int unused = 0;
+
+        out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+        out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+        out.writeInt(streamId & 0x7fffffff);
+        out.writeShort(unused);
+        nameValueBlockBuffer.writeTo(out);
+        out.flush();
+    }
+
+    public void synReset() throws IOException {
+        int type = SpdyConnection.TYPE_RST_STREAM;
+        int length = 8;
+        out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+        out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+        out.writeInt(streamId & 0x7fffffff);
+        out.writeInt(statusCode);
+    }
+
+    public void data(byte[] data) throws IOException {
+        int length = data.length;
+        out.writeInt(streamId & 0x7fffffff);
+        out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+        out.write(data);
+        out.flush();
+    }
+
+    private void writeNameValueBlockToBuffer() throws IOException {
+        nameValueBlockBuffer.reset();
+        int numberOfPairs = nameValueBlock.size() / 2;
+        nameValueBlockOut.writeShort(numberOfPairs);
+        for (String s : nameValueBlock) {
+            nameValueBlockOut.writeShort(s.length());
+            nameValueBlockOut.write(s.getBytes("UTF-8"));
+        }
+        nameValueBlockOut.flush();
+    }
+}
diff --git a/src/main/java/libcore/net/spdy/Threads.java b/src/main/java/libcore/net/spdy/Threads.java
new file mode 100644
index 0000000..a1fbf67
--- /dev/null
+++ b/src/main/java/libcore/net/spdy/Threads.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.spdy;
+
+import java.util.concurrent.ThreadFactory;
+
+final class Threads {
+    public static ThreadFactory newThreadFactory(final String name) {
+        return new ThreadFactory() {
+            @Override public Thread newThread(Runnable r) {
+                return new Thread(r, name);
+            }
+        };
+    }
+
+    private Threads() {
+    }
+}
diff --git a/src/main/java/libcore/util/BasicLruCache.java b/src/main/java/libcore/util/BasicLruCache.java
new file mode 100644
index 0000000..b5f6fdf
--- /dev/null
+++ b/src/main/java/libcore/util/BasicLruCache.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2011 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 libcore.util;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A minimal least-recently-used cache for libcore. Prefer {@code
+ * android.util.LruCache} where that is available.
+ */
+public class BasicLruCache<K, V> {
+    private final LinkedHashMap<K, V> map;
+    private final int maxSize;
+
+    public BasicLruCache(int maxSize) {
+        if (maxSize <= 0) {
+            throw new IllegalArgumentException("maxSize <= 0");
+        }
+        this.maxSize = maxSize;
+        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
+    }
+
+    /**
+     * Returns the value for {@code key} if it exists in the cache or can be
+     * created by {@code #create}. If a value was returned, it is moved to the
+     * head of the queue. This returns null if a value is not cached and cannot
+     * be created.
+     */
+    public synchronized final V get(K key) {
+        if (key == null) {
+            throw new NullPointerException();
+        }
+
+        V result = map.get(key);
+        if (result != null) {
+            return result;
+        }
+
+        result = create(key);
+
+        if (result != null) {
+            map.put(key, result);
+            trimToSize(maxSize);
+        }
+        return result;
+    }
+
+    /**
+     * Caches {@code value} for {@code key}. The value is moved to the head of
+     * the queue.
+     *
+     * @return the previous value mapped by {@code key}. Although that entry is
+     *     no longer cached, it has not been passed to {@link #entryEvicted}.
+     */
+    public synchronized final V put(K key, V value) {
+        if (key == null || value == null) {
+            throw new NullPointerException();
+        }
+
+        V previous = map.put(key, value);
+        trimToSize(maxSize);
+        return previous;
+    }
+
+    private void trimToSize(int maxSize) {
+        while (map.size() > maxSize) {
+            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
+
+            K key = toEvict.getKey();
+            V value = toEvict.getValue();
+            map.remove(key);
+
+            entryEvicted(key, value);
+        }
+    }
+
+    /**
+     * Called for entries that have reached the tail of the least recently used
+     * queue and are be removed. The default implementation does nothing.
+     */
+    protected void entryEvicted(K key, V value) {
+    }
+
+    /**
+     * Called after a cache miss to compute a value for the corresponding key.
+     * Returns the computed value or null if no value can be computed. The
+     * default implementation returns null.
+     */
+    protected V create(K key) {
+        return null;
+    }
+
+    /**
+     * Returns a copy of the current contents of the cache, ordered from least
+     * recently accessed to most recently accessed.
+     */
+    public synchronized final Map<K, V> snapshot() {
+        return new LinkedHashMap<K, V>(map);
+    }
+
+    /**
+     * Clear the cache, calling {@link #entryEvicted} on each removed entry.
+     */
+    public synchronized final void evictAll() {
+        trimToSize(0);
+    }
+}
diff --git a/src/main/java/libcore/util/Charsets.java b/src/main/java/libcore/util/Charsets.java
new file mode 100644
index 0000000..95848ee
--- /dev/null
+++ b/src/main/java/libcore/util/Charsets.java
@@ -0,0 +1,48 @@
+/*
+ * 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 libcore.util;
+
+import java.nio.charset.Charset;
+
+/**
+ * Provides convenient access to the most important built-in charsets. Saves a hash lookup and
+ * unnecessary handling of UnsupportedEncodingException at call sites, compared to using the
+ * charset's name.
+ *
+ * Also various special-case charset conversions (for performance).
+ *
+ * @hide internal use only
+ */
+public final class Charsets {
+    /**
+     * A cheap and type-safe constant for the ISO-8859-1 Charset.
+     */
+    public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
+
+    /**
+     * A cheap and type-safe constant for the US-ASCII Charset.
+     */
+    public static final Charset US_ASCII = Charset.forName("US-ASCII");
+
+    /**
+     * A cheap and type-safe constant for the UTF-8 Charset.
+     */
+    public static final Charset UTF_8 = Charset.forName("UTF-8");
+
+    private Charsets() {
+    }
+}
diff --git a/src/main/java/libcore/util/CollectionUtils.java b/src/main/java/libcore/util/CollectionUtils.java
new file mode 100644
index 0000000..45edf4f
--- /dev/null
+++ b/src/main/java/libcore/util/CollectionUtils.java
@@ -0,0 +1,98 @@
+/*
+ * 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 libcore.util;
+
+import java.lang.ref.Reference;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+public final class CollectionUtils {
+    private CollectionUtils() {
+    }
+
+    /**
+     * Returns an iterator over the values referenced by the elements of {@code
+     * iterable}.
+     *
+     * @param trim true to remove reference objects from the iterable after
+     *     their referenced values have been cleared.
+     */
+    public static <T> Iterable<T> dereferenceIterable(
+            final Iterable<? extends Reference<T>> iterable, final boolean trim) {
+        return new Iterable<T>() {
+            public Iterator<T> iterator() {
+                return new Iterator<T>() {
+                    private final Iterator<? extends Reference<T>> delegate = iterable.iterator();
+                    private boolean removeIsOkay;
+                    private T next;
+
+                    private void computeNext() {
+                        removeIsOkay = false;
+                        while (next == null && delegate.hasNext()) {
+                            next = delegate.next().get();
+                            if (trim && next == null) {
+                                delegate.remove();
+                            }
+                        }
+                    }
+
+                    @Override public boolean hasNext() {
+                        computeNext();
+                        return next != null;
+                    }
+
+                    @Override public T next() {
+                        if (!hasNext()) {
+                            throw new IllegalStateException();
+                        }
+                        T result = next;
+                        removeIsOkay = true;
+                        next = null;
+                        return result;
+                    }
+
+                    public void remove() {
+                        if (!removeIsOkay) {
+                            throw new IllegalStateException();
+                        }
+                        delegate.remove();
+                    }
+                };
+            }
+        };
+    }
+
+    /**
+     * Sorts and removes duplicate elements from {@code list}. This method does
+     * not use {@link Object#equals}: only the comparator defines equality.
+     */
+    public static <T> void removeDuplicates(List<T> list, Comparator<? super T> comparator) {
+        Collections.sort(list, comparator);
+        int j = 1;
+        for (int i = 1; i < list.size(); i++) {
+            if (comparator.compare(list.get(j - 1), list.get(i)) != 0) {
+                T object = list.get(i);
+                list.set(j++, object);
+            }
+        }
+        if (j < list.size()) {
+            list.subList(j, list.size()).clear();
+        }
+    }
+}
diff --git a/src/main/java/libcore/util/DefaultFileNameMap.java b/src/main/java/libcore/util/DefaultFileNameMap.java
new file mode 100644
index 0000000..e817a72
--- /dev/null
+++ b/src/main/java/libcore/util/DefaultFileNameMap.java
@@ -0,0 +1,43 @@
+/*
+ * 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 libcore.util;
+
+import java.net.FileNameMap;
+import java.util.Locale;
+import libcore.net.MimeUtils;
+
+/**
+ * Implements {@link java.net.FileNameMap} in terms of {@link libcore.net.MimeUtils}.
+ */
+class DefaultFileNameMap implements FileNameMap {
+    public String getContentTypeFor(String filename) {
+        if (filename.endsWith("/")) {
+            // a directory, return html
+            return MimeUtils.guessMimeTypeFromExtension("html");
+        }
+        int lastCharInExtension = filename.lastIndexOf('#');
+        if (lastCharInExtension < 0) {
+            lastCharInExtension = filename.length();
+        }
+        int firstCharInExtension = filename.lastIndexOf('.') + 1;
+        String ext = "";
+        if (firstCharInExtension > filename.lastIndexOf('/')) {
+            ext = filename.substring(firstCharInExtension, lastCharInExtension);
+        }
+        return MimeUtils.guessMimeTypeFromExtension(ext.toLowerCase(Locale.US));
+    }
+}
diff --git a/src/main/java/libcore/util/EmptyArray.java b/src/main/java/libcore/util/EmptyArray.java
new file mode 100644
index 0000000..0f919c5
--- /dev/null
+++ b/src/main/java/libcore/util/EmptyArray.java
@@ -0,0 +1,34 @@
+/*
+ * 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 libcore.util;
+
+public final class EmptyArray {
+    private EmptyArray() {
+    }
+
+    public static final boolean[] BOOLEAN = new boolean[0];
+    public static final byte[] BYTE = new byte[0];
+    public static final char[] CHAR = new char[0];
+    public static final double[] DOUBLE = new double[0];
+    public static final int[] INT = new int[0];
+
+    public static final Class<?>[] CLASS = new Class[0];
+    public static final Object[] OBJECT = new Object[0];
+    public static final String[] STRING = new String[0];
+    public static final Throwable[] THROWABLE = new Throwable[0];
+    public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];
+}
diff --git a/src/main/java/libcore/util/ExtendedResponseCache.java b/src/main/java/libcore/util/ExtendedResponseCache.java
new file mode 100644
index 0000000..b3f9191
--- /dev/null
+++ b/src/main/java/libcore/util/ExtendedResponseCache.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2012 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 libcore.util;
+
+import com.squareup.okhttp.OkHttpConnection;
+import java.net.CacheResponse;
+
+/**
+ * A response cache that supports statistics tracking and updating stored
+ * responses. Implementations of {@link java.net.ResponseCache} should implement this
+ * interface to receive additional support from the HTTP engine.
+ *
+ * @hide
+ */
+public interface ExtendedResponseCache {
+
+    /*
+     * This hidden interface is defined in a non-hidden package (java.net) so
+     * its @hide tag will be parsed by Doclava. This hides this interface from
+     * implementing classes' documentation.
+     */
+
+    /**
+     * Track an HTTP response being satisfied by {@code source}.
+     * @hide
+     */
+    void trackResponse(ResponseSource source);
+
+    /**
+     * Track an conditional GET that was satisfied by this cache.
+     * @hide
+     */
+    void trackConditionalCacheHit();
+
+    /**
+     * Updates stored HTTP headers using a hit on a conditional GET.
+     * @hide
+     */
+    void update(CacheResponse conditionalCacheHit, OkHttpConnection httpConnection);
+}
diff --git a/src/main/java/libcore/util/IntegralToString.java b/src/main/java/libcore/util/IntegralToString.java
new file mode 100644
index 0000000..1b66e51
--- /dev/null
+++ b/src/main/java/libcore/util/IntegralToString.java
@@ -0,0 +1,68 @@
+/*
+ * 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 libcore.util;
+
+/**
+ * Converts integral types to strings. This class is public but hidden so that it can also be
+ * used by java.util.Formatter to speed up %d. This class is in java.lang so that it can take
+ * advantage of the package-private String constructor.
+ *
+ * The most important methods are appendInt/appendLong and intToString(int)/longToString(int).
+ * The former are used in the implementation of StringBuilder, StringBuffer, and Formatter, while
+ * the latter are used by Integer.toString and Long.toString.
+ *
+ * The append methods take AbstractStringBuilder rather than Appendable because the latter requires
+ * CharSequences, while we only have raw char[]s. Since much of the savings come from not creating
+ * any garbage, we can't afford temporary CharSequence instances.
+ *
+ * One day the performance advantage of the binary/hex/octal specializations will be small enough
+ * that we can lose the duplication, but until then this class offers the full set.
+ *
+ * @hide
+ */
+public final class IntegralToString {
+    /**
+     * The digits for every supported radix.
+     */
+    private static final char[] DIGITS = {
+        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+        'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+        'u', 'v', 'w', 'x', 'y', 'z'
+    };
+
+    private static final char[] UPPER_CASE_DIGITS = {
+        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+        'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
+        'U', 'V', 'W', 'X', 'Y', 'Z'
+    };
+
+    private IntegralToString() {
+    }
+
+    public static String bytesToHexString(byte[] bytes, boolean upperCase) {
+        char[] digits = upperCase ? UPPER_CASE_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);
+    }
+}
diff --git a/src/main/java/libcore/util/Libcore.java b/src/main/java/libcore/util/Libcore.java
new file mode 100644
index 0000000..740d678
--- /dev/null
+++ b/src/main/java/libcore/util/Libcore.java
@@ -0,0 +1,232 @@
+package libcore.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.SSLSocket;
+import org.eclipse.jetty.npn.NextProtoNego;
+
+/**
+ * APIs for interacting with Android's core library. This mostly emulates the
+ * Android core library for interoperability with other runtimes.
+ */
+public final class Libcore {
+
+    private Libcore() {
+    }
+
+    private static boolean useAndroidTlsApis;
+    private static Class<?> openSslSocketClass;
+    private static Method setEnabledCompressionMethods;
+    private static Method setUseSessionTickets;
+    private static Method setHostname;
+    private static boolean android23TlsOptionsAvailable;
+    private static Method setNpnProtocols;
+    private static Method getNpnSelectedProtocol;
+    private static boolean android41TlsOptionsAvailable;
+
+    static {
+        try {
+            openSslSocketClass = Class.forName(
+                    "org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
+            useAndroidTlsApis = true;
+            setEnabledCompressionMethods = openSslSocketClass.getMethod(
+                    "setEnabledCompressionMethods", String[].class);
+            setUseSessionTickets = openSslSocketClass.getMethod(
+                    "setUseSessionTickets", boolean.class);
+            setHostname = openSslSocketClass.getMethod("setHostname", String.class);
+            android23TlsOptionsAvailable = true;
+            setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
+            getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
+            android41TlsOptionsAvailable = true;
+        } catch (ClassNotFoundException ignored) {
+            // This isn't an Android runtime.
+        } catch (NoSuchMethodException ignored) {
+            // This Android runtime is missing some optional TLS options.
+        }
+    }
+
+    public static void makeTlsTolerant(SSLSocket socket, String socketHost, boolean tlsTolerant) {
+        if (!tlsTolerant) {
+            socket.setEnabledProtocols(new String[] {"SSLv3"});
+            return;
+        }
+
+        if (android23TlsOptionsAvailable && openSslSocketClass.isInstance(socket)) {
+            // This is Android: use reflection on OpenSslSocketImpl.
+            try {
+                String[] compressionMethods = {"ZLIB"};
+                setEnabledCompressionMethods.invoke(socket,
+                        new Object[] {compressionMethods});
+                setUseSessionTickets.invoke(socket, true);
+                setHostname.invoke(socket, socketHost);
+            } catch (InvocationTargetException e) {
+                throw new RuntimeException(e);
+            } catch (IllegalAccessException e) {
+                throw new AssertionError(e);
+            }
+        }
+    }
+
+    /**
+     * Returns the negotiated protocol, or null if no protocol was negotiated.
+     */
+    public static byte[] getNpnSelectedProtocol(SSLSocket socket) {
+        if (useAndroidTlsApis) {
+            // This is Android: use reflection on OpenSslSocketImpl.
+            if (android41TlsOptionsAvailable && openSslSocketClass.isInstance(socket)) {
+                try {
+                    return (byte[]) getNpnSelectedProtocol.invoke(socket);
+                } catch (InvocationTargetException e) {
+                    throw new RuntimeException(e);
+                } catch (IllegalAccessException e) {
+                    throw new AssertionError(e);
+                }
+            }
+            return null;
+        } else {
+            // This is OpenJDK: use JettyNpnProvider.
+            JettyNpnProvider provider = (JettyNpnProvider) NextProtoNego.get(socket);
+            if (!provider.unsupported && provider.selected == null) {
+                throw new IllegalStateException(
+                        "No callback received. Is NPN configured properly?");
+            }
+            try {
+                return provider.unsupported
+                        ? null
+                        : provider.selected.getBytes("US-ASCII");
+            } catch (UnsupportedEncodingException e) {
+                throw new AssertionError(e);
+            }
+        }
+    }
+
+    public static void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
+        if (useAndroidTlsApis) {
+            // This is Android: use reflection on OpenSslSocketImpl.
+            if (android41TlsOptionsAvailable && openSslSocketClass.isInstance(socket)) {
+                try {
+                    setNpnProtocols.invoke(socket, new Object[] {npnProtocols});
+                } catch (IllegalAccessException e) {
+                    throw new AssertionError(e);
+                } catch (InvocationTargetException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        } else {
+            // This is OpenJDK: use JettyNpnProvider.
+            try {
+                List<String> strings = new ArrayList<String>();
+                for (int i = 0; i < npnProtocols.length;) {
+                    int length = npnProtocols[i++];
+                    strings.add(new String(npnProtocols, i, length, "US-ASCII"));
+                    i += length;
+                }
+                JettyNpnProvider provider = new JettyNpnProvider();
+                provider.protocols = strings;
+                NextProtoNego.put(socket, provider);
+            } catch (UnsupportedEncodingException e) {
+                throw new AssertionError(e);
+            }
+        }
+    }
+
+    private static class JettyNpnProvider
+            implements NextProtoNego.ClientProvider, NextProtoNego.ServerProvider {
+        List<String> protocols;
+        boolean unsupported;
+        String selected;
+
+        @Override public boolean supports() {
+            return true;
+        }
+        @Override public List<String> protocols() {
+            return protocols;
+        }
+        @Override public void unsupported() {
+            this.unsupported = true;
+        }
+        @Override public void protocolSelected(String selected) {
+            this.selected = selected;
+        }
+        @Override public String selectProtocol(List<String> strings) {
+            // TODO: use OpenSSL's algorithm which uses 2 lists
+            System.out.println("CLIENT PROTOCOLS: " + protocols + " SERVER PROTOCOLS: " + strings);
+            String selected = protocols.get(0);
+            protocolSelected(selected);
+            return selected;
+        }
+    }
+
+    public static void deleteIfExists(File file) throws IOException {
+        // okhttp-changed: was Libcore.os.remove() in a try/catch block
+        file.delete();
+    }
+
+    public static void logW(String warning) {
+        // okhttp-changed: was System.logw()
+        System.out.println(warning);
+    }
+
+    public static int getEffectivePort(URI uri) {
+        return getEffectivePort(uri.getScheme(), uri.getPort());
+    }
+
+    public static int getEffectivePort(URL url) {
+        return getEffectivePort(url.getProtocol(), url.getPort());
+    }
+
+    private static int getEffectivePort(String scheme, int specifiedPort) {
+        if (specifiedPort != -1) {
+            return specifiedPort;
+        }
+
+        if ("http".equalsIgnoreCase(scheme)) {
+            return 80;
+        } else if ("https".equalsIgnoreCase(scheme)) {
+            return 443;
+        } else {
+            return -1;
+        }
+    }
+
+    public static void checkOffsetAndCount(int arrayLength, int offset, int count) {
+        if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) {
+            throw new ArrayIndexOutOfBoundsException();
+        }
+    }
+
+    public static void tagSocket(Socket socket) {
+    }
+
+    public static void untagSocket(Socket socket) throws SocketException {
+    }
+
+    public static URI toUriLenient(URL url) throws URISyntaxException {
+        return url.toURI(); // this isn't as good as the built-in toUriLenient
+    }
+
+    public static void pokeInt(byte[] dst, int offset, int value, ByteOrder order) {
+        if (order == ByteOrder.BIG_ENDIAN) {
+            dst[offset++] = (byte) ((value >> 24) & 0xff);
+            dst[offset++] = (byte) ((value >> 16) & 0xff);
+            dst[offset++] = (byte) ((value >>  8) & 0xff);
+            dst[offset  ] = (byte) ((value >>  0) & 0xff);
+        } else {
+            dst[offset++] = (byte) ((value >>  0) & 0xff);
+            dst[offset++] = (byte) ((value >>  8) & 0xff);
+            dst[offset++] = (byte) ((value >> 16) & 0xff);
+            dst[offset  ] = (byte) ((value >> 24) & 0xff);
+        }
+    }
+}
diff --git a/src/main/java/libcore/util/MutableBoolean.java b/src/main/java/libcore/util/MutableBoolean.java
new file mode 100644
index 0000000..359a8f9
--- /dev/null
+++ b/src/main/java/libcore/util/MutableBoolean.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 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 libcore.util;
+
+public final class MutableBoolean {
+    public boolean value;
+
+    public MutableBoolean(boolean value) {
+        this.value = value;
+    }
+}
diff --git a/src/main/java/libcore/util/MutableByte.java b/src/main/java/libcore/util/MutableByte.java
new file mode 100644
index 0000000..13f780b
--- /dev/null
+++ b/src/main/java/libcore/util/MutableByte.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 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 libcore.util;
+
+public final class MutableByte {
+    public byte value;
+
+    public MutableByte(byte value) {
+        this.value = value;
+    }
+}
diff --git a/src/main/java/libcore/util/MutableChar.java b/src/main/java/libcore/util/MutableChar.java
new file mode 100644
index 0000000..1cafc3c
--- /dev/null
+++ b/src/main/java/libcore/util/MutableChar.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 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 libcore.util;
+
+public final class MutableChar {
+    public char value;
+
+    public MutableChar(char value) {
+        this.value = value;
+    }
+}
diff --git a/src/main/java/libcore/util/MutableDouble.java b/src/main/java/libcore/util/MutableDouble.java
new file mode 100644
index 0000000..4473ae6
--- /dev/null
+++ b/src/main/java/libcore/util/MutableDouble.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 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 libcore.util;
+
+public final class MutableDouble {
+    public double value;
+
+    public MutableDouble(double value) {
+        this.value = value;
+    }
+}
diff --git a/src/main/java/libcore/util/MutableFloat.java b/src/main/java/libcore/util/MutableFloat.java
new file mode 100644
index 0000000..f81fba5
--- /dev/null
+++ b/src/main/java/libcore/util/MutableFloat.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 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 libcore.util;
+
+public final class MutableFloat {
+    public float value;
+
+    public MutableFloat(float value) {
+        this.value = value;
+    }
+}
diff --git a/src/main/java/libcore/util/MutableInt.java b/src/main/java/libcore/util/MutableInt.java
new file mode 100644
index 0000000..c8feb3a
--- /dev/null
+++ b/src/main/java/libcore/util/MutableInt.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 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 libcore.util;
+
+public final class MutableInt {
+    public int value;
+
+    public MutableInt(int value) {
+        this.value = value;
+    }
+}
diff --git a/src/main/java/libcore/util/MutableLong.java b/src/main/java/libcore/util/MutableLong.java
new file mode 100644
index 0000000..ad9b78e
--- /dev/null
+++ b/src/main/java/libcore/util/MutableLong.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 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 libcore.util;
+
+public final class MutableLong {
+    public long value;
+
+    public MutableLong(long value) {
+        this.value = value;
+    }
+}
diff --git a/src/main/java/libcore/util/MutableShort.java b/src/main/java/libcore/util/MutableShort.java
new file mode 100644
index 0000000..78b4c33
--- /dev/null
+++ b/src/main/java/libcore/util/MutableShort.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 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 libcore.util;
+
+public final class MutableShort {
+    public short value;
+
+    public MutableShort(short value) {
+        this.value = value;
+    }
+}
diff --git a/src/main/java/libcore/util/Objects.java b/src/main/java/libcore/util/Objects.java
new file mode 100644
index 0000000..050888d
--- /dev/null
+++ b/src/main/java/libcore/util/Objects.java
@@ -0,0 +1,33 @@
+/*
+ * 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 libcore.util;
+
+public final class Objects {
+    private Objects() {
+    }
+
+    /**
+     * Returns true if two possibly-null objects are equal.
+     */
+    public static boolean equal(Object a, Object b) {
+        return a == b || (a != null && a.equals(b));
+    }
+
+    public static int hashCode(Object o) {
+        return (o == null) ? 0 : o.hashCode();
+    }
+}
diff --git a/src/main/java/libcore/util/ResponseSource.java b/src/main/java/libcore/util/ResponseSource.java
new file mode 100644
index 0000000..8e7bfae
--- /dev/null
+++ b/src/main/java/libcore/util/ResponseSource.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2011 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 libcore.util;
+
+/**
+ * Where the HTTP client should look for a response.
+ *
+ * @hide
+ */
+public enum ResponseSource {
+
+    /**
+     * Return the response from the cache immediately.
+     */
+    CACHE,
+
+    /**
+     * Make a conditional request to the host, returning the cache response if
+     * the cache is valid and the network response otherwise.
+     */
+    CONDITIONAL_CACHE,
+
+    /**
+     * Return the response from the network.
+     */
+    NETWORK;
+
+    public boolean requiresConnection() {
+        return this == CONDITIONAL_CACHE || this == NETWORK;
+    }
+}
diff --git a/src/main/java/libcore/util/SneakyThrow.java b/src/main/java/libcore/util/SneakyThrow.java
new file mode 100644
index 0000000..f5c077c
--- /dev/null
+++ b/src/main/java/libcore/util/SneakyThrow.java
@@ -0,0 +1,71 @@
+/*
+ *  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 libcore.util;
+
+/**
+ * Exploits a weakness in the runtime to throw an arbitrary throwable without
+ * the traditional declaration. <strong>This is a dangerous API that should be
+ * used with great caution.</strong> Typically this is useful when rethrowing
+ * throwables that are of a known range of types.
+ *
+ * <p>The following code must enumerate several types to rethrow:
+ * <pre>
+ * public void close() throws IOException {
+ *     Throwable thrown = null;
+ *     ...
+ *
+ *     if (thrown != null) {
+ *         if (thrown instanceof IOException) {
+ *             throw (IOException) thrown;
+ *         } else if (thrown instanceof RuntimeException) {
+ *             throw (RuntimeException) thrown;
+ *         } else if (thrown instanceof Error) {
+ *             throw (Error) thrown;
+ *         } else {
+ *             throw new AssertionError();
+ *         }
+ *     }
+ * }</pre>
+ * With SneakyThrow, rethrowing is easier:
+ * <pre>
+ * public void close() throws IOException {
+ *     Throwable thrown = null;
+ *     ...
+ *
+ *     if (thrown != null) {
+ *         SneakyThrow.sneakyThrow(thrown);
+ *     }
+ * }</pre>
+ */
+public final class SneakyThrow {
+    private SneakyThrow() {
+    }
+
+    public static void sneakyThrow(Throwable t) {
+        SneakyThrow.<Error>sneakyThrow2(t);
+    }
+
+    /**
+     * Exploits unsafety to throw an exception that the compiler wouldn't permit
+     * but that the runtime doesn't check. See Java Puzzlers #43.
+     */
+    @SuppressWarnings("unchecked")
+    private static <T extends Throwable> void sneakyThrow2(Throwable t) throws T {
+        throw (T) t;
+    }
+}
diff --git a/src/test/java/libcore/net/http/ExternalSpdyExample.java b/src/test/java/libcore/net/http/ExternalSpdyExample.java
new file mode 100644
index 0000000..dc8aa1e
--- /dev/null
+++ b/src/test/java/libcore/net/http/ExternalSpdyExample.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2009 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 libcore.net.http;
+
+import com.squareup.okhttp.OkHttpConnection;
+import com.squareup.okhttp.OkHttpsConnection;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.URL;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
+public final class ExternalSpdyExample {
+    public static void main(String[] args) throws Exception {
+        URL url = new URL("https://www.google.ca/");
+        OkHttpsConnection connection = (OkHttpsConnection) OkHttpConnection.open(url);
+
+        connection.setHostnameVerifier(new HostnameVerifier() {
+            @Override public boolean verify(String s, SSLSession sslSession) {
+                System.out.println("VERIFYING " + s);
+                return true;
+            }
+        });
+
+        int responseCode = connection.getResponseCode();
+        System.out.println(responseCode);
+
+        BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
+        String line;
+        while ((line = reader.readLine()) != null) {
+            System.out.println(line);
+        }
+    }
+}
diff --git a/src/test/java/libcore/net/http/NewURLConnectionTest.java b/src/test/java/libcore/net/http/NewURLConnectionTest.java
new file mode 100644
index 0000000..8c6121e
--- /dev/null
+++ b/src/test/java/libcore/net/http/NewURLConnectionTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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 libcore.net.http;
+
+import junit.framework.TestCase;
+
+public final class NewURLConnectionTest extends TestCase {
+
+    public void testUrlConnection() {
+    }
+
+    // TODO: write a test that shows pooled connections detect HTTP/1.0 (vs. HTTP/1.1)
+
+    // TODO: write a test that shows POST bodies are retained on AUTH problems (or prove it unnecessary)
+
+    // TODO: cookies + trailers. Do cookie headers get processed too many times?
+
+    // TODO: crash on header names or values containing the '\0' character
+
+    // TODO: crash on empty names and empty values
+
+    // TODO: deflate compression
+
+    // TODO: read the outgoing status line and incoming status line?
+
+}
diff --git a/src/test/java/libcore/net/http/URLConnectionTest.java b/src/test/java/libcore/net/http/URLConnectionTest.java
new file mode 100644
index 0000000..6df8f53
--- /dev/null
+++ b/src/test/java/libcore/net/http/URLConnectionTest.java
@@ -0,0 +1,2081 @@
+/*
+ * Copyright (C) 2009 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 libcore.net.http;
+
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.MockWebServer;
+import com.google.mockwebserver.RecordedRequest;
+import com.google.mockwebserver.SocketPolicy;
+import com.squareup.okhttp.OkHttpConnection;
+import com.squareup.okhttp.OkHttpsConnection;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Authenticator;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.ConnectException;
+import java.net.HttpRetryException;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.PasswordAuthentication;
+import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.ResponseCache;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+import junit.framework.TestCase;
+import libcore.net.ssl.SslContextBuilder;
+
+import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
+import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
+import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END;
+import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END;
+
+/**
+ * Android's URLConnectionTest.
+ */
+public final class URLConnectionTest extends TestCase {
+
+    private static final Authenticator SIMPLE_AUTHENTICATOR = new Authenticator() {
+        protected PasswordAuthentication getPasswordAuthentication() {
+            return new PasswordAuthentication("username", "password".toCharArray());
+        }
+    };
+
+    /** base64("username:password") */
+    private static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ=";
+
+    private MockWebServer server = new MockWebServer();
+    private String hostName;
+
+    private static final SSLContext sslContext;
+
+    static {
+        try {
+            sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName())
+                    .build();
+        } catch (GeneralSecurityException e) {
+            throw new RuntimeException(e);
+        } catch (UnknownHostException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override protected void setUp() throws Exception {
+        super.setUp();
+        hostName = server.getHostName();
+    }
+
+    @Override protected void tearDown() throws Exception {
+        ResponseCache.setDefault(null);
+        Authenticator.setDefault(null);
+        System.clearProperty("proxyHost");
+        System.clearProperty("proxyPort");
+        System.clearProperty("http.proxyHost");
+        System.clearProperty("http.proxyPort");
+        System.clearProperty("https.proxyHost");
+        System.clearProperty("https.proxyPort");
+        server.shutdown();
+        super.tearDown();
+    }
+    
+    private static OkHttpConnection openConnection(URL url) {
+        return OkHttpConnection.open(url);
+    }
+
+    private static OkHttpConnection openConnection(URL url, Proxy proxy) {
+        return OkHttpConnection.open(url, proxy);
+    }
+
+    public void testRequestHeaders() throws IOException, InterruptedException {
+        server.enqueue(new MockResponse());
+        server.play();
+
+        OkHttpConnection urlConnection = openConnection(server.getUrl("/"));
+        urlConnection.addRequestProperty("D", "e");
+        urlConnection.addRequestProperty("D", "f");
+        assertEquals("f", urlConnection.getRequestProperty("D"));
+        assertEquals("f", urlConnection.getRequestProperty("d"));
+        Map<String, List<String>> requestHeaders = urlConnection.getRequestProperties();
+        assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("D")));
+        assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("d")));
+        try {
+            requestHeaders.put("G", Arrays.asList("h"));
+            fail("Modified an unmodifiable view.");
+        } catch (UnsupportedOperationException expected) {
+        }
+        try {
+            requestHeaders.get("D").add("i");
+            fail("Modified an unmodifiable view.");
+        } catch (UnsupportedOperationException expected) {
+        }
+        try {
+            urlConnection.setRequestProperty(null, "j");
+            fail();
+        } catch (NullPointerException expected) {
+        }
+        try {
+            urlConnection.addRequestProperty(null, "k");
+            fail();
+        } catch (NullPointerException expected) {
+        }
+        urlConnection.setRequestProperty("NullValue", null); // should fail silently!
+        assertNull(urlConnection.getRequestProperty("NullValue"));
+        urlConnection.addRequestProperty("AnotherNullValue", null);  // should fail silently!
+        assertNull(urlConnection.getRequestProperty("AnotherNullValue"));
+
+        urlConnection.getResponseCode();
+        RecordedRequest request = server.takeRequest();
+        assertContains(request.getHeaders(), "D: e");
+        assertContains(request.getHeaders(), "D: f");
+        assertContainsNoneMatching(request.getHeaders(), "NullValue.*");
+        assertContainsNoneMatching(request.getHeaders(), "AnotherNullValue.*");
+        assertContainsNoneMatching(request.getHeaders(), "G:.*");
+        assertContainsNoneMatching(request.getHeaders(), "null:.*");
+
+        try {
+            urlConnection.addRequestProperty("N", "o");
+            fail("Set header after connect");
+        } catch (IllegalStateException expected) {
+        }
+        try {
+            urlConnection.setRequestProperty("P", "q");
+            fail("Set header after connect");
+        } catch (IllegalStateException expected) {
+        }
+        try {
+            urlConnection.getRequestProperties();
+            fail();
+        } catch (IllegalStateException expected) {
+        }
+    }
+
+    public void testGetRequestPropertyReturnsLastValue() throws Exception {
+        server.play();
+        OkHttpConnection urlConnection = openConnection(server.getUrl("/"));
+        urlConnection.addRequestProperty("A", "value1");
+        urlConnection.addRequestProperty("A", "value2");
+        assertEquals("value2", urlConnection.getRequestProperty("A"));
+    }
+
+    public void testResponseHeaders() throws IOException, InterruptedException {
+        server.enqueue(new MockResponse()
+                .setStatus("HTTP/1.0 200 Fantastic")
+                .addHeader("A: c")
+                .addHeader("B: d")
+                .addHeader("A: e")
+                .setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8));
+        server.play();
+
+        OkHttpConnection urlConnection = openConnection(server.getUrl("/"));
+        assertEquals(200, urlConnection.getResponseCode());
+        assertEquals("Fantastic", urlConnection.getResponseMessage());
+        assertEquals("HTTP/1.0 200 Fantastic", urlConnection.getHeaderField(null));
+        Map<String, List<String>> responseHeaders = urlConnection.getHeaderFields();
+        assertEquals(Arrays.asList("HTTP/1.0 200 Fantastic"), responseHeaders.get(null));
+        assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("A")));
+        assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("a")));
+        try {
+            responseHeaders.put("N", Arrays.asList("o"));
+            fail("Modified an unmodifiable view.");
+        } catch (UnsupportedOperationException expected) {
+        }
+        try {
+            responseHeaders.get("A").add("f");
+            fail("Modified an unmodifiable view.");
+        } catch (UnsupportedOperationException expected) {
+        }
+        assertEquals("A", urlConnection.getHeaderFieldKey(0));
+        assertEquals("c", urlConnection.getHeaderField(0));
+        assertEquals("B", urlConnection.getHeaderFieldKey(1));
+        assertEquals("d", urlConnection.getHeaderField(1));
+        assertEquals("A", urlConnection.getHeaderFieldKey(2));
+        assertEquals("e", urlConnection.getHeaderField(2));
+    }
+
+    public void testGetErrorStreamOnSuccessfulRequest() throws Exception {
+        server.enqueue(new MockResponse().setBody("A"));
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        assertNull(connection.getErrorStream());
+    }
+
+    public void testGetErrorStreamOnUnsuccessfulRequest() throws Exception {
+        server.enqueue(new MockResponse().setResponseCode(404).setBody("A"));
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        assertEquals("A", readAscii(connection.getErrorStream(), Integer.MAX_VALUE));
+    }
+
+    // Check that if we don't read to the end of a response, the next request on the
+    // recycled connection doesn't get the unread tail of the first request's response.
+    // http://code.google.com/p/android/issues/detail?id=2939
+    public void test_2939() throws Exception {
+        MockResponse response = new MockResponse().setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8);
+
+        server.enqueue(response);
+        server.enqueue(response);
+        server.play();
+
+        assertContent("ABCDE", openConnection(server.getUrl("/")), 5);
+        assertContent("ABCDE", openConnection(server.getUrl("/")), 5);
+    }
+
+    // Check that we recognize a few basic mime types by extension.
+    // http://code.google.com/p/android/issues/detail?id=10100
+    public void test_10100() throws Exception {
+        assertEquals("image/jpeg", URLConnection.guessContentTypeFromName("someFile.jpg"));
+        assertEquals("application/pdf", URLConnection.guessContentTypeFromName("stuff.pdf"));
+    }
+
+    public void testConnectionsArePooled() throws Exception {
+        MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR");
+
+        server.enqueue(response);
+        server.enqueue(response);
+        server.enqueue(response);
+        server.play();
+
+        assertContent("ABCDEFGHIJKLMNOPQR", openConnection(server.getUrl("/foo")));
+        assertEquals(0, server.takeRequest().getSequenceNumber());
+        assertContent("ABCDEFGHIJKLMNOPQR", openConnection(server.getUrl("/bar?baz=quux")));
+        assertEquals(1, server.takeRequest().getSequenceNumber());
+        assertContent("ABCDEFGHIJKLMNOPQR", openConnection(server.getUrl("/z")));
+        assertEquals(2, server.takeRequest().getSequenceNumber());
+    }
+
+    public void testChunkedConnectionsArePooled() throws Exception {
+        MockResponse response = new MockResponse().setChunkedBody("ABCDEFGHIJKLMNOPQR", 5);
+
+        server.enqueue(response);
+        server.enqueue(response);
+        server.enqueue(response);
+        server.play();
+
+        assertContent("ABCDEFGHIJKLMNOPQR", openConnection(server.getUrl("/foo")));
+        assertEquals(0, server.takeRequest().getSequenceNumber());
+        assertContent("ABCDEFGHIJKLMNOPQR", openConnection(server.getUrl("/bar?baz=quux")));
+        assertEquals(1, server.takeRequest().getSequenceNumber());
+        assertContent("ABCDEFGHIJKLMNOPQR", openConnection(server.getUrl("/z")));
+        assertEquals(2, server.takeRequest().getSequenceNumber());
+    }
+
+    public void testServerClosesSocket() throws Exception {
+        testServerClosesOutput(DISCONNECT_AT_END);
+    }
+
+    public void testServerShutdownInput() throws Exception {
+        testServerClosesOutput(SHUTDOWN_INPUT_AT_END);
+    }
+
+    public void SUPPRESSED_testServerShutdownOutput() throws Exception {
+        testServerClosesOutput(SHUTDOWN_OUTPUT_AT_END);
+    }
+
+    private void testServerClosesOutput(SocketPolicy socketPolicy) throws Exception {
+        server.enqueue(new MockResponse()
+                .setBody("This connection won't pool properly")
+                .setSocketPolicy(socketPolicy));
+        server.enqueue(new MockResponse()
+                .setBody("This comes after a busted connection"));
+        server.play();
+
+        assertContent("This connection won't pool properly", openConnection(server.getUrl("/a")));
+        assertEquals(0, server.takeRequest().getSequenceNumber());
+        assertContent("This comes after a busted connection", openConnection(server.getUrl("/b")));
+        // sequence number 0 means the HTTP socket connection was not reused
+        assertEquals(0, server.takeRequest().getSequenceNumber());
+    }
+
+    enum WriteKind { BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS }
+
+    public void test_chunkedUpload_byteByByte() throws Exception {
+        doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE);
+    }
+
+    public void test_chunkedUpload_smallBuffers() throws Exception {
+        doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS);
+    }
+
+    public void test_chunkedUpload_largeBuffers() throws Exception {
+        doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS);
+    }
+
+    public void SUPPRESSED_test_fixedLengthUpload_byteByByte() throws Exception {
+        doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE);
+    }
+
+    public void test_fixedLengthUpload_smallBuffers() throws Exception {
+        doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS);
+    }
+
+    public void test_fixedLengthUpload_largeBuffers() throws Exception {
+        doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS);
+    }
+
+    private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception {
+        int n = 512*1024;
+        server.setBodyLimit(0);
+        server.enqueue(new MockResponse());
+        server.play();
+
+        OkHttpConnection conn = openConnection(server.getUrl("/"));
+        conn.setDoOutput(true);
+        conn.setRequestMethod("POST");
+        if (uploadKind == TransferKind.CHUNKED) {
+            conn.setChunkedStreamingMode(-1);
+        } else {
+            conn.setFixedLengthStreamingMode(n);
+        }
+        OutputStream out = conn.getOutputStream();
+        if (writeKind == WriteKind.BYTE_BY_BYTE) {
+            for (int i = 0; i < n; ++i) {
+                out.write('x');
+            }
+        } else {
+            byte[] buf = new byte[writeKind == WriteKind.SMALL_BUFFERS ? 256 : 64*1024];
+            Arrays.fill(buf, (byte) 'x');
+            for (int i = 0; i < n; i += buf.length) {
+                out.write(buf, 0, Math.min(buf.length, n - i));
+            }
+        }
+        out.close();
+        assertEquals(200, conn.getResponseCode());
+        RecordedRequest request = server.takeRequest();
+        assertEquals(n, request.getBodySize());
+        if (uploadKind == TransferKind.CHUNKED) {
+            assertTrue(request.getChunkSizes().size() > 0);
+        } else {
+            assertTrue(request.getChunkSizes().isEmpty());
+        }
+    }
+
+    public void testGetResponseCodeNoResponseBody() throws Exception {
+        server.enqueue(new MockResponse()
+                .addHeader("abc: def"));
+        server.play();
+
+        URL url = server.getUrl("/");
+        OkHttpConnection conn = openConnection(url);
+        conn.setDoInput(false);
+        assertEquals("def", conn.getHeaderField("abc"));
+        assertEquals(200, conn.getResponseCode());
+        try {
+            conn.getInputStream();
+            fail();
+        } catch (ProtocolException expected) {
+        }
+    }
+
+    public void testConnectViaHttps() throws Exception {
+        server.useHttps(sslContext.getSocketFactory(), false);
+        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
+        server.play();
+
+        OkHttpsConnection connection = (OkHttpsConnection) openConnection(server.getUrl("/foo"));
+        connection.setSSLSocketFactory(sslContext.getSocketFactory());
+        connection.setHostnameVerifier(new RecordingHostnameVerifier());
+
+        assertContent("this response comes via HTTPS", connection);
+
+        RecordedRequest request = server.takeRequest();
+        assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+    }
+
+    public void testConnectViaHttpsReusingConnections() throws IOException, InterruptedException {
+        server.useHttps(sslContext.getSocketFactory(), false);
+        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
+        server.enqueue(new MockResponse().setBody("another response via HTTPS"));
+        server.play();
+
+        // The pool will only reuse sockets if the SSL socket factories are the same.
+        SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory();
+
+        OkHttpsConnection connection = (OkHttpsConnection) openConnection(server.getUrl("/"));
+        connection.setSSLSocketFactory(clientSocketFactory);
+        connection.setHostnameVerifier(new RecordingHostnameVerifier());
+        assertContent("this response comes via HTTPS", connection);
+
+        connection = (OkHttpsConnection) openConnection(server.getUrl("/"));
+        connection.setSSLSocketFactory(clientSocketFactory);
+        connection.setHostnameVerifier(new RecordingHostnameVerifier());
+        assertContent("another response via HTTPS", connection);
+
+        assertEquals(0, server.takeRequest().getSequenceNumber());
+        assertEquals(1, server.takeRequest().getSequenceNumber());
+    }
+
+    public void testConnectViaHttpsReusingConnectionsDifferentFactories()
+            throws IOException, InterruptedException {
+        server.useHttps(sslContext.getSocketFactory(), false);
+        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
+        server.enqueue(new MockResponse().setBody("another response via HTTPS"));
+        server.play();
+
+        // install a custom SSL socket factory so the server can be authorized
+        OkHttpsConnection connection = (OkHttpsConnection) openConnection(server.getUrl("/"));
+        connection.setSSLSocketFactory(sslContext.getSocketFactory());
+        connection.setHostnameVerifier(new RecordingHostnameVerifier());
+        assertContent("this response comes via HTTPS", connection);
+
+        connection = (OkHttpsConnection) openConnection(server.getUrl("/"));
+        try {
+            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
+            fail("without an SSL socket factory, the connection should fail");
+        } catch (SSLException expected) {
+        }
+    }
+
+    public void testConnectViaHttpsWithSSLFallback() throws IOException, InterruptedException {
+        server.useHttps(sslContext.getSocketFactory(), false);
+        server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START));
+        server.enqueue(new MockResponse().setBody("this response comes via SSL"));
+        server.play();
+
+        OkHttpsConnection connection = (OkHttpsConnection) openConnection(server.getUrl("/foo"));
+        connection.setSSLSocketFactory(sslContext.getSocketFactory());
+        connection.setHostnameVerifier(new RecordingHostnameVerifier());
+
+        assertContent("this response comes via SSL", connection);
+
+        RecordedRequest request = server.takeRequest();
+        assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+    }
+
+    /**
+     * Verify that we don't retry connections on certificate verification errors.
+     *
+     * http://code.google.com/p/android/issues/detail?id=13178
+     */
+//    public void testConnectViaHttpsToUntrustedServer() throws IOException, InterruptedException {
+//        TestSSLContext testSSLContext = TestSSLContext.create(TestKeyStore.getClientCA2(),
+//                                                              TestKeyStore.getServer());
+//
+//        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
+//        server.enqueue(new MockResponse()); // unused
+//        server.play();
+//
+//        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection();
+//        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
+//        try {
+//            connection.getInputStream();
+//            fail();
+//        } catch (SSLHandshakeException expected) {
+//            assertTrue(expected.getCause() instanceof CertificateException);
+//        }
+//        assertEquals(0, server.getRequestCount());
+//    }
+
+    public void testConnectViaProxyUsingProxyArg() throws Exception {
+        testConnectViaProxy(ProxyConfig.CREATE_ARG);
+    }
+
+    public void testConnectViaProxyUsingProxySystemProperty() throws Exception {
+        testConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY);
+    }
+
+    public void testConnectViaProxyUsingHttpProxySystemProperty() throws Exception {
+        testConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
+    }
+
+    private void testConnectViaProxy(ProxyConfig proxyConfig) throws Exception {
+        MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy");
+        server.enqueue(mockResponse);
+        server.play();
+
+        URL url = new URL("http://android.com/foo");
+        OkHttpConnection connection = proxyConfig.connect(server, url);
+        assertContent("this response comes via a proxy", connection);
+
+        RecordedRequest request = server.takeRequest();
+        assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine());
+        assertContains(request.getHeaders(), "Host: android.com");
+    }
+
+    public void testContentDisagreesWithContentLengthHeader() throws IOException {
+        server.enqueue(new MockResponse()
+                .setBody("abc\r\nYOU SHOULD NOT SEE THIS")
+                .clearHeaders()
+                .addHeader("Content-Length: 3"));
+        server.play();
+
+        assertContent("abc", openConnection(server.getUrl("/")));
+    }
+
+    public void testContentDisagreesWithChunkedHeader() throws IOException {
+        MockResponse mockResponse = new MockResponse();
+        mockResponse.setChunkedBody("abc", 3);
+        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+        bytesOut.write(mockResponse.getBody());
+        bytesOut.write("\r\nYOU SHOULD NOT SEE THIS".getBytes("UTF-8"));
+        mockResponse.setBody(bytesOut.toByteArray());
+        mockResponse.clearHeaders();
+        mockResponse.addHeader("Transfer-encoding: chunked");
+
+        server.enqueue(mockResponse);
+        server.play();
+
+        assertContent("abc", openConnection(server.getUrl("/")));
+    }
+
+    public void testConnectViaHttpProxyToHttpsUsingProxyArgWithNoProxy() throws Exception {
+        testConnectViaDirectProxyToHttps(ProxyConfig.NO_PROXY);
+    }
+
+    public void testConnectViaHttpProxyToHttpsUsingHttpProxySystemProperty() throws Exception {
+        // https should not use http proxy
+        testConnectViaDirectProxyToHttps(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
+    }
+
+    private void testConnectViaDirectProxyToHttps(ProxyConfig proxyConfig) throws Exception {
+        server.useHttps(sslContext.getSocketFactory(), false);
+        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
+        server.play();
+
+        URL url = server.getUrl("/foo");
+        OkHttpsConnection connection = (OkHttpsConnection) proxyConfig.connect(server, url);
+        connection.setSSLSocketFactory(sslContext.getSocketFactory());
+        connection.setHostnameVerifier(new RecordingHostnameVerifier());
+
+        assertContent("this response comes via HTTPS", connection);
+
+        RecordedRequest request = server.takeRequest();
+        assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+    }
+
+    public void testConnectViaHttpProxyToHttpsUsingProxyArg() throws Exception {
+        testConnectViaHttpProxyToHttps(ProxyConfig.CREATE_ARG);
+    }
+
+    /**
+     * We weren't honoring all of the appropriate proxy system properties when
+     * connecting via HTTPS. http://b/3097518
+     */
+    public void testConnectViaHttpProxyToHttpsUsingProxySystemProperty() throws Exception {
+        testConnectViaHttpProxyToHttps(ProxyConfig.PROXY_SYSTEM_PROPERTY);
+    }
+
+    public void testConnectViaHttpProxyToHttpsUsingHttpsProxySystemProperty() throws Exception {
+        testConnectViaHttpProxyToHttps(ProxyConfig.HTTPS_PROXY_SYSTEM_PROPERTY);
+    }
+
+    /**
+     * We were verifying the wrong hostname when connecting to an HTTPS site
+     * through a proxy. http://b/3097277
+     */
+    private void testConnectViaHttpProxyToHttps(ProxyConfig proxyConfig) throws Exception {
+        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+
+        server.useHttps(sslContext.getSocketFactory(), true);
+        server.enqueue(new MockResponse()
+                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+                .clearHeaders());
+        server.enqueue(new MockResponse().setBody("this response comes via a secure proxy"));
+        server.play();
+
+        URL url = new URL("https://android.com/foo");
+        OkHttpsConnection connection = (OkHttpsConnection) proxyConfig.connect(server, url);
+        connection.setSSLSocketFactory(sslContext.getSocketFactory());
+        connection.setHostnameVerifier(hostnameVerifier);
+
+        assertContent("this response comes via a secure proxy", connection);
+
+        RecordedRequest connect = server.takeRequest();
+        assertEquals("Connect line failure on proxy",
+                "CONNECT android.com:443 HTTP/1.1", connect.getRequestLine());
+        assertContains(connect.getHeaders(), "Host: android.com");
+
+        RecordedRequest get = server.takeRequest();
+        assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
+        assertContains(get.getHeaders(), "Host: android.com");
+        assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
+    }
+
+    /**
+     * Test which headers are sent unencrypted to the HTTP proxy.
+     */
+    public void testProxyConnectIncludesProxyHeadersOnly()
+            throws IOException, InterruptedException {
+        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+
+        server.useHttps(sslContext.getSocketFactory(), true);
+        server.enqueue(new MockResponse()
+                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+                .clearHeaders());
+        server.enqueue(new MockResponse().setBody("encrypted response from the origin server"));
+        server.play();
+
+        URL url = new URL("https://android.com/foo");
+        OkHttpsConnection connection = (OkHttpsConnection) openConnection(
+                url, server.toProxyAddress());
+        connection.addRequestProperty("Private", "Secret");
+        connection.addRequestProperty("Proxy-Authorization", "bar");
+        connection.addRequestProperty("User-Agent", "baz");
+        connection.setSSLSocketFactory(sslContext.getSocketFactory());
+        connection.setHostnameVerifier(hostnameVerifier);
+        assertContent("encrypted response from the origin server", connection);
+
+        RecordedRequest connect = server.takeRequest();
+        assertContainsNoneMatching(connect.getHeaders(), "Private.*");
+        assertContains(connect.getHeaders(), "Proxy-Authorization: bar");
+        assertContains(connect.getHeaders(), "User-Agent: baz");
+        assertContains(connect.getHeaders(), "Host: android.com");
+        assertContains(connect.getHeaders(), "Proxy-Connection: Keep-Alive");
+
+        RecordedRequest get = server.takeRequest();
+        assertContains(get.getHeaders(), "Private: Secret");
+        assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
+    }
+
+    public void testProxyAuthenticateOnConnect() throws Exception {
+        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
+        server.useHttps(sslContext.getSocketFactory(), true);
+        server.enqueue(new MockResponse()
+                .setResponseCode(407)
+                .addHeader("Proxy-Authenticate: Basic realm=\"localhost\""));
+        server.enqueue(new MockResponse()
+                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+                .clearHeaders());
+        server.enqueue(new MockResponse().setBody("A"));
+        server.play();
+
+        URL url = new URL("https://android.com/foo");
+        OkHttpsConnection connection = (OkHttpsConnection) openConnection(
+                url, server.toProxyAddress());
+        connection.setSSLSocketFactory(sslContext.getSocketFactory());
+        connection.setHostnameVerifier(new RecordingHostnameVerifier());
+        assertContent("A", connection);
+
+        RecordedRequest connect1 = server.takeRequest();
+        assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine());
+        assertContainsNoneMatching(connect1.getHeaders(), "Proxy\\-Authorization.*");
+
+        RecordedRequest connect2 = server.takeRequest();
+        assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine());
+        assertContains(connect2.getHeaders(), "Proxy-Authorization: Basic " + BASE_64_CREDENTIALS);
+
+        RecordedRequest get = server.takeRequest();
+        assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
+        assertContainsNoneMatching(get.getHeaders(), "Proxy\\-Authorization.*");
+    }
+
+    public void testDisconnectedConnection() throws IOException {
+        server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"));
+        server.play();
+
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        InputStream in = connection.getInputStream();
+        assertEquals('A', (char) in.read());
+        connection.disconnect();
+        try {
+            in.read();
+            fail("Expected a connection closed exception");
+        } catch (IOException expected) {
+        }
+    }
+
+    public void testDisconnectBeforeConnect() throws IOException {
+        server.enqueue(new MockResponse().setBody("A"));
+        server.play();
+
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        connection.disconnect();
+
+        assertContent("A", connection);
+        assertEquals(200, connection.getResponseCode());
+    }
+
+    public void testDefaultRequestProperty() throws Exception {
+        URLConnection.setDefaultRequestProperty("X-testSetDefaultRequestProperty", "A");
+        assertNull(URLConnection.getDefaultRequestProperty("X-setDefaultRequestProperty"));
+    }
+
+    /**
+     * Reads {@code count} characters from the stream. If the stream is
+     * exhausted before {@code count} characters can be read, the remaining
+     * characters are returned and the stream is closed.
+     */
+    private String readAscii(InputStream in, int count) throws IOException {
+        StringBuilder result = new StringBuilder();
+        for (int i = 0; i < count; i++) {
+            int value = in.read();
+            if (value == -1) {
+                in.close();
+                break;
+            }
+            result.append((char) value);
+        }
+        return result.toString();
+    }
+
+    public void testMarkAndResetWithContentLengthHeader() throws IOException {
+        testMarkAndReset(TransferKind.FIXED_LENGTH);
+    }
+
+    public void testMarkAndResetWithChunkedEncoding() throws IOException {
+        testMarkAndReset(TransferKind.CHUNKED);
+    }
+
+    public void testMarkAndResetWithNoLengthHeaders() throws IOException {
+        testMarkAndReset(TransferKind.END_OF_STREAM);
+    }
+
+    private void testMarkAndReset(TransferKind transferKind) throws IOException {
+        MockResponse response = new MockResponse();
+        transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024);
+        server.enqueue(response);
+        server.enqueue(response);
+        server.play();
+
+        InputStream in = openConnection(server.getUrl("/")).getInputStream();
+        assertFalse("This implementation claims to support mark().", in.markSupported());
+        in.mark(5);
+        assertEquals("ABCDE", readAscii(in, 5));
+        try {
+            in.reset();
+            fail();
+        } catch (IOException expected) {
+        }
+        assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE));
+        assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", openConnection(server.getUrl("/")));
+    }
+
+    /**
+     * We've had a bug where we forget the HTTP response when we see response
+     * code 401. This causes a new HTTP request to be issued for every call into
+     * the URLConnection.
+     */
+    public void SUPPRESSED_testUnauthorizedResponseHandling() throws IOException {
+        MockResponse response = new MockResponse()
+                .addHeader("WWW-Authenticate: challenge")
+                .setResponseCode(401) // UNAUTHORIZED
+                .setBody("Unauthorized");
+        server.enqueue(response);
+        server.enqueue(response);
+        server.enqueue(response);
+        server.play();
+
+        URL url = server.getUrl("/");
+        OkHttpConnection conn = openConnection(url);
+
+        assertEquals(401, conn.getResponseCode());
+        assertEquals(401, conn.getResponseCode());
+        assertEquals(401, conn.getResponseCode());
+        assertEquals(1, server.getRequestCount());
+    }
+
+    public void testNonHexChunkSize() throws IOException {
+        server.enqueue(new MockResponse()
+                .setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n")
+                .clearHeaders()
+                .addHeader("Transfer-encoding: chunked"));
+        server.play();
+
+        URLConnection connection = openConnection(server.getUrl("/"));
+        try {
+            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
+            fail();
+        } catch (IOException e) {
+        }
+    }
+
+    public void testMissingChunkBody() throws IOException {
+        server.enqueue(new MockResponse()
+                .setBody("5")
+                .clearHeaders()
+                .addHeader("Transfer-encoding: chunked")
+                .setSocketPolicy(DISCONNECT_AT_END));
+        server.play();
+
+        URLConnection connection = openConnection(server.getUrl("/"));
+        try {
+            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
+            fail();
+        } catch (IOException e) {
+        }
+    }
+
+    /**
+     * This test checks whether connections are gzipped by default. This
+     * behavior in not required by the API, so a failure of this test does not
+     * imply a bug in the implementation.
+     */
+    public void testGzipEncodingEnabledByDefault() throws IOException, InterruptedException {
+        server.enqueue(new MockResponse()
+                .setBody(gzip("ABCABCABC".getBytes("UTF-8")))
+                .addHeader("Content-Encoding: gzip"));
+        server.play();
+
+        URLConnection connection = openConnection(server.getUrl("/"));
+        assertEquals("ABCABCABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+        assertNull(connection.getContentEncoding());
+
+        RecordedRequest request = server.takeRequest();
+        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
+    }
+
+    public void testClientConfiguredGzipContentEncoding() throws Exception {
+        server.enqueue(new MockResponse()
+                .setBody(gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8")))
+                .addHeader("Content-Encoding: gzip"));
+        server.play();
+
+        URLConnection connection = openConnection(server.getUrl("/"));
+        connection.addRequestProperty("Accept-Encoding", "gzip");
+        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
+        assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE));
+
+        RecordedRequest request = server.takeRequest();
+        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
+    }
+
+    public void testGzipAndConnectionReuseWithFixedLength() throws Exception {
+        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH);
+    }
+
+    public void testGzipAndConnectionReuseWithChunkedEncoding() throws Exception {
+        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED);
+    }
+
+    public void testClientConfiguredCustomContentEncoding() throws Exception {
+        server.enqueue(new MockResponse()
+                .setBody("ABCDE")
+                .addHeader("Content-Encoding: custom"));
+        server.play();
+
+        URLConnection connection = openConnection(server.getUrl("/"));
+        connection.addRequestProperty("Accept-Encoding", "custom");
+        assertEquals("ABCDE", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+        RecordedRequest request = server.takeRequest();
+        assertContains(request.getHeaders(), "Accept-Encoding: custom");
+    }
+
+    /**
+     * Test a bug where gzip input streams weren't exhausting the input stream,
+     * which corrupted the request that followed.
+     * http://code.google.com/p/android/issues/detail?id=7059
+     */
+    private void testClientConfiguredGzipContentEncodingAndConnectionReuse(
+            TransferKind transferKind) throws Exception {
+        MockResponse responseOne = new MockResponse();
+        responseOne.addHeader("Content-Encoding: gzip");
+        transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5);
+        server.enqueue(responseOne);
+        MockResponse responseTwo = new MockResponse();
+        transferKind.setBody(responseTwo, "two (identity)", 5);
+        server.enqueue(responseTwo);
+        server.play();
+
+        URLConnection connection = openConnection(server.getUrl("/"));
+        connection.addRequestProperty("Accept-Encoding", "gzip");
+        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
+        assertEquals("one (gzipped)", readAscii(gunzippedIn, Integer.MAX_VALUE));
+        assertEquals(0, server.takeRequest().getSequenceNumber());
+
+        connection = openConnection(server.getUrl("/"));
+        assertEquals("two (identity)", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+        assertEquals(1, server.takeRequest().getSequenceNumber());
+    }
+
+    /**
+     * Obnoxiously test that the chunk sizes transmitted exactly equal the
+     * requested data+chunk header size. Although setChunkedStreamingMode()
+     * isn't specific about whether the size applies to the data or the
+     * complete chunk, the RI interprets it as a complete chunk.
+     */
+    public void testSetChunkedStreamingMode() throws IOException, InterruptedException {
+        server.enqueue(new MockResponse());
+        server.play();
+
+        OkHttpConnection urlConnection = openConnection(server.getUrl("/"));
+        urlConnection.setChunkedStreamingMode(8);
+        urlConnection.setDoOutput(true);
+        OutputStream outputStream = urlConnection.getOutputStream();
+        outputStream.write("ABCDEFGHIJKLMNOPQ".getBytes("US-ASCII"));
+        assertEquals(200, urlConnection.getResponseCode());
+
+        RecordedRequest request = server.takeRequest();
+        assertEquals("ABCDEFGHIJKLMNOPQ", new String(request.getBody(), "US-ASCII"));
+        assertEquals(Arrays.asList(3, 3, 3, 3, 3, 2), request.getChunkSizes());
+    }
+
+    public void testAuthenticateWithFixedLengthStreaming() throws Exception {
+        testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH);
+    }
+
+    public void testAuthenticateWithChunkedStreaming() throws Exception {
+        testAuthenticateWithStreamingPost(StreamingMode.CHUNKED);
+    }
+
+    private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception {
+        MockResponse pleaseAuthenticate = new MockResponse()
+                .setResponseCode(401)
+                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
+                .setBody("Please authenticate.");
+        server.enqueue(pleaseAuthenticate);
+        server.play();
+
+        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        connection.setDoOutput(true);
+        byte[] requestBody = { 'A', 'B', 'C', 'D' };
+        if (streamingMode == StreamingMode.FIXED_LENGTH) {
+            connection.setFixedLengthStreamingMode(requestBody.length);
+        } else if (streamingMode == StreamingMode.CHUNKED) {
+            connection.setChunkedStreamingMode(0);
+        }
+        OutputStream outputStream = connection.getOutputStream();
+        outputStream.write(requestBody);
+        outputStream.close();
+        try {
+            connection.getInputStream();
+            fail();
+        } catch (HttpRetryException expected) {
+        }
+
+        // no authorization header for the request...
+        RecordedRequest request = server.takeRequest();
+        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
+        assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
+    }
+
+    public void testSetValidRequestMethod() throws Exception {
+        server.play();
+        assertValidRequestMethod("GET");
+        assertValidRequestMethod("DELETE");
+        assertValidRequestMethod("HEAD");
+        assertValidRequestMethod("OPTIONS");
+        assertValidRequestMethod("POST");
+        assertValidRequestMethod("PUT");
+        assertValidRequestMethod("TRACE");
+    }
+
+    private void assertValidRequestMethod(String requestMethod) throws Exception {
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        connection.setRequestMethod(requestMethod);
+        assertEquals(requestMethod, connection.getRequestMethod());
+    }
+
+    public void testSetInvalidRequestMethodLowercase() throws Exception {
+        server.play();
+        assertInvalidRequestMethod("get");
+    }
+
+    public void testSetInvalidRequestMethodConnect() throws Exception {
+        server.play();
+        assertInvalidRequestMethod("CONNECT");
+    }
+
+    private void assertInvalidRequestMethod(String requestMethod) throws Exception {
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        try {
+            connection.setRequestMethod(requestMethod);
+            fail();
+        } catch (ProtocolException expected) {
+        }
+    }
+
+    public void testCannotSetNegativeFixedLengthStreamingMode() throws Exception {
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        try {
+            connection.setFixedLengthStreamingMode(-2);
+            fail();
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    public void testCanSetNegativeChunkedStreamingMode() throws Exception {
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        connection.setChunkedStreamingMode(-2);
+    }
+
+    public void testCannotSetFixedLengthStreamingModeAfterConnect() throws Exception {
+        server.enqueue(new MockResponse().setBody("A"));
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+        try {
+            connection.setFixedLengthStreamingMode(1);
+            fail();
+        } catch (IllegalStateException expected) {
+        }
+    }
+
+    public void testCannotSetChunkedStreamingModeAfterConnect() throws Exception {
+        server.enqueue(new MockResponse().setBody("A"));
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+        try {
+            connection.setChunkedStreamingMode(1);
+            fail();
+        } catch (IllegalStateException expected) {
+        }
+    }
+
+    public void testCannotSetFixedLengthStreamingModeAfterChunkedStreamingMode() throws Exception {
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        connection.setChunkedStreamingMode(1);
+        try {
+            connection.setFixedLengthStreamingMode(1);
+            fail();
+        } catch (IllegalStateException expected) {
+        }
+    }
+
+    public void testCannotSetChunkedStreamingModeAfterFixedLengthStreamingMode() throws Exception {
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        connection.setFixedLengthStreamingMode(1);
+        try {
+            connection.setChunkedStreamingMode(1);
+            fail();
+        } catch (IllegalStateException expected) {
+        }
+    }
+
+    public void testSecureFixedLengthStreaming() throws Exception {
+        testSecureStreamingPost(StreamingMode.FIXED_LENGTH);
+    }
+
+    public void testSecureChunkedStreaming() throws Exception {
+        testSecureStreamingPost(StreamingMode.CHUNKED);
+    }
+
+    /**
+     * Users have reported problems using HTTPS with streaming request bodies.
+     * http://code.google.com/p/android/issues/detail?id=12860
+     */
+    private void testSecureStreamingPost(StreamingMode streamingMode) throws Exception {
+        server.useHttps(sslContext.getSocketFactory(), false);
+        server.enqueue(new MockResponse().setBody("Success!"));
+        server.play();
+
+        OkHttpsConnection connection = (OkHttpsConnection) openConnection(server.getUrl("/"));
+        connection.setSSLSocketFactory(sslContext.getSocketFactory());
+        connection.setHostnameVerifier(new RecordingHostnameVerifier());
+        connection.setDoOutput(true);
+        byte[] requestBody = { 'A', 'B', 'C', 'D' };
+        if (streamingMode == StreamingMode.FIXED_LENGTH) {
+            connection.setFixedLengthStreamingMode(requestBody.length);
+        } else if (streamingMode == StreamingMode.CHUNKED) {
+            connection.setChunkedStreamingMode(0);
+        }
+        OutputStream outputStream = connection.getOutputStream();
+        outputStream.write(requestBody);
+        outputStream.close();
+        assertEquals("Success!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+        RecordedRequest request = server.takeRequest();
+        assertEquals("POST / HTTP/1.1", request.getRequestLine());
+        if (streamingMode == StreamingMode.FIXED_LENGTH) {
+            assertEquals(Collections.<Integer>emptyList(), request.getChunkSizes());
+        } else if (streamingMode == StreamingMode.CHUNKED) {
+            assertEquals(Arrays.asList(4), request.getChunkSizes());
+        }
+        assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
+    }
+
+    enum StreamingMode {
+        FIXED_LENGTH, CHUNKED
+    }
+
+    public void testAuthenticateWithPost() throws Exception {
+        MockResponse pleaseAuthenticate = new MockResponse()
+                .setResponseCode(401)
+                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
+                .setBody("Please authenticate.");
+        // fail auth three times...
+        server.enqueue(pleaseAuthenticate);
+        server.enqueue(pleaseAuthenticate);
+        server.enqueue(pleaseAuthenticate);
+        // ...then succeed the fourth time
+        server.enqueue(new MockResponse().setBody("Successful auth!"));
+        server.play();
+
+        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        connection.setDoOutput(true);
+        byte[] requestBody = { 'A', 'B', 'C', 'D' };
+        OutputStream outputStream = connection.getOutputStream();
+        outputStream.write(requestBody);
+        outputStream.close();
+        assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+        // no authorization header for the first request...
+        RecordedRequest request = server.takeRequest();
+        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
+
+        // ...but the three requests that follow include an authorization header
+        for (int i = 0; i < 3; i++) {
+            request = server.takeRequest();
+            assertEquals("POST / HTTP/1.1", request.getRequestLine());
+            assertContains(request.getHeaders(), "Authorization: Basic " + BASE_64_CREDENTIALS);
+            assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
+        }
+    }
+
+    public void testAuthenticateWithGet() throws Exception {
+        MockResponse pleaseAuthenticate = new MockResponse()
+                .setResponseCode(401)
+                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
+                .setBody("Please authenticate.");
+        // fail auth three times...
+        server.enqueue(pleaseAuthenticate);
+        server.enqueue(pleaseAuthenticate);
+        server.enqueue(pleaseAuthenticate);
+        // ...then succeed the fourth time
+        server.enqueue(new MockResponse().setBody("Successful auth!"));
+        server.play();
+
+        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+        // no authorization header for the first request...
+        RecordedRequest request = server.takeRequest();
+        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
+
+        // ...but the three requests that follow requests include an authorization header
+        for (int i = 0; i < 3; i++) {
+            request = server.takeRequest();
+            assertEquals("GET / HTTP/1.1", request.getRequestLine());
+            assertContains(request.getHeaders(), "Authorization: Basic " + BASE_64_CREDENTIALS);
+        }
+    }
+
+    public void testRedirectedWithChunkedEncoding() throws Exception {
+        testRedirected(TransferKind.CHUNKED, true);
+    }
+
+    public void testRedirectedWithContentLengthHeader() throws Exception {
+        testRedirected(TransferKind.FIXED_LENGTH, true);
+    }
+
+    public void testRedirectedWithNoLengthHeaders() throws Exception {
+        testRedirected(TransferKind.END_OF_STREAM, false);
+    }
+
+    private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception {
+        MockResponse response = new MockResponse()
+                .setResponseCode(OkHttpConnection.HTTP_MOVED_TEMP)
+                .addHeader("Location: /foo");
+        transferKind.setBody(response, "This page has moved!", 10);
+        server.enqueue(response);
+        server.enqueue(new MockResponse().setBody("This is the new location!"));
+        server.play();
+
+        URLConnection connection = openConnection(server.getUrl("/"));
+        assertEquals("This is the new location!",
+                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+        RecordedRequest first = server.takeRequest();
+        assertEquals("GET / HTTP/1.1", first.getRequestLine());
+        RecordedRequest retry = server.takeRequest();
+        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
+        if (reuse) {
+            assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
+        }
+    }
+
+    public void testRedirectedOnHttps() throws IOException, InterruptedException {
+        server.useHttps(sslContext.getSocketFactory(), false);
+        server.enqueue(new MockResponse()
+                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+                .addHeader("Location: /foo")
+                .setBody("This page has moved!"));
+        server.enqueue(new MockResponse().setBody("This is the new location!"));
+        server.play();
+
+        OkHttpsConnection connection = (OkHttpsConnection) openConnection(server.getUrl("/"));
+        connection.setSSLSocketFactory(sslContext.getSocketFactory());
+        connection.setHostnameVerifier(new RecordingHostnameVerifier());
+        assertEquals("This is the new location!",
+                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+        RecordedRequest first = server.takeRequest();
+        assertEquals("GET / HTTP/1.1", first.getRequestLine());
+        RecordedRequest retry = server.takeRequest();
+        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
+        assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
+    }
+
+    public void testNotRedirectedFromHttpsToHttp() throws IOException, InterruptedException {
+        server.useHttps(sslContext.getSocketFactory(), false);
+        server.enqueue(new MockResponse()
+                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+                .addHeader("Location: http://anyhost/foo")
+                .setBody("This page has moved!"));
+        server.play();
+
+        OkHttpsConnection connection = (OkHttpsConnection) openConnection(server.getUrl("/"));
+        connection.setSSLSocketFactory(sslContext.getSocketFactory());
+        connection.setHostnameVerifier(new RecordingHostnameVerifier());
+        assertEquals("This page has moved!",
+                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+    }
+
+    public void testNotRedirectedFromHttpToHttps() throws IOException, InterruptedException {
+        server.enqueue(new MockResponse()
+                .setResponseCode(OkHttpConnection.HTTP_MOVED_TEMP)
+                .addHeader("Location: https://anyhost/foo")
+                .setBody("This page has moved!"));
+        server.play();
+
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        assertEquals("This page has moved!",
+                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+    }
+
+    public void SUPPRESSED_testRedirectToAnotherOriginServer() throws Exception {
+        MockWebServer server2 = new MockWebServer();
+        server2.enqueue(new MockResponse().setBody("This is the 2nd server!"));
+        server2.play();
+
+        server.enqueue(new MockResponse()
+                .setResponseCode(OkHttpConnection.HTTP_MOVED_TEMP)
+                .addHeader("Location: " + server2.getUrl("/").toString())
+                .setBody("This page has moved!"));
+        server.enqueue(new MockResponse().setBody("This is the first server again!"));
+        server.play();
+
+        URLConnection connection = openConnection(server.getUrl("/"));
+        assertEquals("This is the 2nd server!",
+                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+        assertEquals(server2.getUrl("/"), connection.getURL());
+
+        // make sure the first server was careful to recycle the connection
+        assertEquals("This is the first server again!",
+                readAscii(server.getUrl("/").openStream(), Integer.MAX_VALUE));
+
+        RecordedRequest first = server.takeRequest();
+        assertContains(first.getHeaders(), "Host: " + hostName + ":" + server.getPort());
+        RecordedRequest second = server2.takeRequest();
+        assertContains(second.getHeaders(), "Host: " + hostName + ":" + server2.getPort());
+        RecordedRequest third = server.takeRequest();
+        assertEquals("Expected connection reuse", 1, third.getSequenceNumber());
+
+        server2.shutdown();
+    }
+
+    public void testResponse300MultipleChoiceWithPost() throws Exception {
+        // Chrome doesn't follow the redirect, but Firefox and the RI both do
+        testResponseRedirectedWithPost(OkHttpConnection.HTTP_MULT_CHOICE);
+    }
+
+    public void testResponse301MovedPermanentlyWithPost() throws Exception {
+        testResponseRedirectedWithPost(OkHttpConnection.HTTP_MOVED_PERM);
+    }
+
+    public void testResponse302MovedTemporarilyWithPost() throws Exception {
+        testResponseRedirectedWithPost(OkHttpConnection.HTTP_MOVED_TEMP);
+    }
+
+    public void testResponse303SeeOtherWithPost() throws Exception {
+        testResponseRedirectedWithPost(OkHttpConnection.HTTP_SEE_OTHER);
+    }
+
+    private void testResponseRedirectedWithPost(int redirectCode) throws Exception {
+        server.enqueue(new MockResponse()
+                .setResponseCode(redirectCode)
+                .addHeader("Location: /page2")
+                .setBody("This page has moved!"));
+        server.enqueue(new MockResponse().setBody("Page 2"));
+        server.play();
+
+        OkHttpConnection connection = openConnection(server.getUrl("/page1"));
+        connection.setDoOutput(true);
+        byte[] requestBody = { 'A', 'B', 'C', 'D' };
+        OutputStream outputStream = connection.getOutputStream();
+        outputStream.write(requestBody);
+        outputStream.close();
+        assertEquals("Page 2", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+        assertTrue(connection.getDoOutput());
+
+        RecordedRequest page1 = server.takeRequest();
+        assertEquals("POST /page1 HTTP/1.1", page1.getRequestLine());
+        assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody()));
+
+        RecordedRequest page2 = server.takeRequest();
+        assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine());
+    }
+
+    public void testResponse305UseProxy() throws Exception {
+        server.play();
+        server.enqueue(new MockResponse()
+                .setResponseCode(OkHttpConnection.HTTP_USE_PROXY)
+                .addHeader("Location: " + server.getUrl("/"))
+                .setBody("This page has moved!"));
+        server.enqueue(new MockResponse().setBody("Proxy Response"));
+
+        OkHttpConnection connection = openConnection(server.getUrl("/foo"));
+        // Fails on the RI, which gets "Proxy Response"
+        assertEquals("This page has moved!",
+                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+        RecordedRequest page1 = server.takeRequest();
+        assertEquals("GET /foo HTTP/1.1", page1.getRequestLine());
+        assertEquals(1, server.getRequestCount());
+    }
+
+//    public void testHttpsWithCustomTrustManager() throws Exception {
+//        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+//        RecordingTrustManager trustManager = new RecordingTrustManager();
+//        SSLContext sc = SSLContext.getInstance("TLS");
+//        sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
+//
+//        HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
+//        HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
+//        SSLSocketFactory defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
+//        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+//        try {
+//            TestSSLContext testSSLContext = TestSSLContext.create();
+//            server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
+//            server.enqueue(new MockResponse().setBody("ABC"));
+//            server.enqueue(new MockResponse().setBody("DEF"));
+//            server.enqueue(new MockResponse().setBody("GHI"));
+//            server.play();
+//
+//            URL url = server.getUrl("/");
+//            assertEquals("ABC", readAscii(url.openStream(), Integer.MAX_VALUE));
+//            assertEquals("DEF", readAscii(url.openStream(), Integer.MAX_VALUE));
+//            assertEquals("GHI", readAscii(url.openStream(), Integer.MAX_VALUE));
+//
+//            assertEquals(Arrays.asList("verify " + hostName), hostnameVerifier.calls);
+//            assertEquals(Arrays.asList("checkServerTrusted ["
+//                    + "CN=" + hostName + " 1, "
+//                    + "CN=Test Intermediate Certificate Authority 1, "
+//                    + "CN=Test Root Certificate Authority 1"
+//                    + "] RSA"),
+//                    trustManager.calls);
+//        } finally {
+//            HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier);
+//            HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
+//        }
+//    }
+//
+//    public void testConnectTimeouts() throws IOException {
+//        StuckServer ss = new StuckServer();
+//        int serverPort = ss.getLocalPort();
+//        URLConnection urlConnection = new URL("http://localhost:" + serverPort).openConnection();
+//        int timeout = 1000;
+//        urlConnection.setConnectTimeout(timeout);
+//        long start = System.currentTimeMillis();
+//        try {
+//            urlConnection.getInputStream();
+//            fail();
+//        } catch (SocketTimeoutException expected) {
+//            long actual = System.currentTimeMillis() - start;
+//            assertTrue(Math.abs(timeout - actual) < 500);
+//        } finally {
+//            ss.close();
+//        }
+//    }
+
+    public void testReadTimeouts() throws IOException {
+        /*
+         * This relies on the fact that MockWebServer doesn't close the
+         * connection after a response has been sent. This causes the client to
+         * try to read more bytes than are sent, which results in a timeout.
+         */
+        MockResponse timeout = new MockResponse()
+                .setBody("ABC")
+                .clearHeaders()
+                .addHeader("Content-Length: 4");
+        server.enqueue(timeout);
+        server.enqueue(new MockResponse().setBody("unused")); // to keep the server alive
+        server.play();
+
+        URLConnection urlConnection = openConnection(server.getUrl("/"));
+        urlConnection.setReadTimeout(1000);
+        InputStream in = urlConnection.getInputStream();
+        assertEquals('A', in.read());
+        assertEquals('B', in.read());
+        assertEquals('C', in.read());
+        try {
+            in.read(); // if Content-Length was accurate, this would return -1 immediately
+            fail();
+        } catch (SocketTimeoutException expected) {
+        }
+    }
+
+    public void testSetChunkedEncodingAsRequestProperty() throws IOException, InterruptedException {
+        server.enqueue(new MockResponse());
+        server.play();
+
+        OkHttpConnection urlConnection = openConnection(server.getUrl("/"));
+        urlConnection.setRequestProperty("Transfer-encoding", "chunked");
+        urlConnection.setDoOutput(true);
+        urlConnection.getOutputStream().write("ABC".getBytes("UTF-8"));
+        assertEquals(200, urlConnection.getResponseCode());
+
+        RecordedRequest request = server.takeRequest();
+        assertEquals("ABC", new String(request.getBody(), "UTF-8"));
+    }
+
+    public void testConnectionCloseInRequest() throws IOException, InterruptedException {
+        server.enqueue(new MockResponse()); // server doesn't honor the connection: close header!
+        server.enqueue(new MockResponse());
+        server.play();
+
+        OkHttpConnection a = openConnection(server.getUrl("/"));
+        a.setRequestProperty("Connection", "close");
+        assertEquals(200, a.getResponseCode());
+
+        OkHttpConnection b = openConnection(server.getUrl("/"));
+        assertEquals(200, b.getResponseCode());
+
+        assertEquals(0, server.takeRequest().getSequenceNumber());
+        assertEquals("When connection: close is used, each request should get its own connection",
+                0, server.takeRequest().getSequenceNumber());
+    }
+
+    public void testConnectionCloseInResponse() throws IOException, InterruptedException {
+        server.enqueue(new MockResponse().addHeader("Connection: close"));
+        server.enqueue(new MockResponse());
+        server.play();
+
+        OkHttpConnection a = openConnection(server.getUrl("/"));
+        assertEquals(200, a.getResponseCode());
+
+        OkHttpConnection b = openConnection(server.getUrl("/"));
+        assertEquals(200, b.getResponseCode());
+
+        assertEquals(0, server.takeRequest().getSequenceNumber());
+        assertEquals("When connection: close is used, each request should get its own connection",
+                0, server.takeRequest().getSequenceNumber());
+    }
+
+    public void testConnectionCloseWithRedirect() throws IOException, InterruptedException {
+        MockResponse response = new MockResponse()
+                .setResponseCode(OkHttpConnection.HTTP_MOVED_TEMP)
+                .addHeader("Location: /foo")
+                .addHeader("Connection: close");
+        server.enqueue(response);
+        server.enqueue(new MockResponse().setBody("This is the new location!"));
+        server.play();
+
+        URLConnection connection = openConnection(server.getUrl("/"));
+        assertEquals("This is the new location!",
+                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+        assertEquals(0, server.takeRequest().getSequenceNumber());
+        assertEquals("When connection: close is used, each request should get its own connection",
+                0, server.takeRequest().getSequenceNumber());
+    }
+
+    public void testResponseCodeDisagreesWithHeaders() throws IOException, InterruptedException {
+        server.enqueue(new MockResponse()
+                .setResponseCode(OkHttpConnection.HTTP_NO_CONTENT)
+                .setBody("This body is not allowed!"));
+        server.play();
+
+        URLConnection connection = openConnection(server.getUrl("/"));
+        assertEquals("This body is not allowed!",
+                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+    }
+
+    public void testSingleByteReadIsSigned() throws IOException {
+        server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 }));
+        server.play();
+
+        URLConnection connection = openConnection(server.getUrl("/"));
+        InputStream in = connection.getInputStream();
+        assertEquals(254, in.read());
+        assertEquals(255, in.read());
+        assertEquals(-1, in.read());
+    }
+
+    public void testFlushAfterStreamTransmittedWithChunkedEncoding() throws IOException {
+        testFlushAfterStreamTransmitted(TransferKind.CHUNKED);
+    }
+
+    public void testFlushAfterStreamTransmittedWithFixedLength() throws IOException {
+        testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH);
+    }
+
+    public void testFlushAfterStreamTransmittedWithNoLengthHeaders() throws IOException {
+        testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM);
+    }
+
+    /**
+     * We explicitly permit apps to close the upload stream even after it has
+     * been transmitted.  We also permit flush so that buffered streams can
+     * do a no-op flush when they are closed. http://b/3038470
+     */
+    private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws IOException {
+        server.enqueue(new MockResponse().setBody("abc"));
+        server.play();
+
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        connection.setDoOutput(true);
+        byte[] upload = "def".getBytes("UTF-8");
+
+        if (transferKind == TransferKind.CHUNKED) {
+            connection.setChunkedStreamingMode(0);
+        } else if (transferKind == TransferKind.FIXED_LENGTH) {
+            connection.setFixedLengthStreamingMode(upload.length);
+        }
+
+        OutputStream out = connection.getOutputStream();
+        out.write(upload);
+        assertEquals("abc", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+        out.flush(); // dubious but permitted
+        try {
+            out.write("ghi".getBytes("UTF-8"));
+            fail();
+        } catch (IOException expected) {
+        }
+    }
+
+    public void testGetHeadersThrows() throws IOException {
+        server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START));
+        server.play();
+
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        try {
+            connection.getInputStream();
+            fail();
+        } catch (IOException expected) {
+        }
+
+        try {
+            connection.getInputStream();
+            fail();
+        } catch (IOException expected) {
+        }
+    }
+
+    public void SUPPRESSED_testGetKeepAlive() throws Exception {
+        MockWebServer server = new MockWebServer();
+        server.enqueue(new MockResponse().setBody("ABC"));
+        server.play();
+
+        // The request should work once and then fail
+        URLConnection connection = openConnection(server.getUrl(""));
+        InputStream input = connection.getInputStream();
+        assertEquals("ABC", readAscii(input, Integer.MAX_VALUE));
+        input.close();
+        try {
+            openConnection(server.getUrl("")).getInputStream();
+            fail();
+        } catch (ConnectException expected) {
+        }
+    }
+
+    /**
+     * This test goes through the exhaustive set of interesting ASCII characters
+     * because most of those characters are interesting in some way according to
+     * RFC 2396 and RFC 2732. http://b/1158780
+     */
+    public void SUPPRESSED_testLenientUrlToUri() throws Exception {
+        // alphanum
+        testUrlToUriMapping("abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09");
+
+        // control characters
+        testUrlToUriMapping("\u0001", "%01", "%01", "%01", "%01");
+        testUrlToUriMapping("\u001f", "%1F", "%1F", "%1F", "%1F");
+
+        // ascii characters
+        testUrlToUriMapping("%20", "%20", "%20", "%20", "%20");
+        testUrlToUriMapping("%20", "%20", "%20", "%20", "%20");
+        testUrlToUriMapping(" ", "%20", "%20", "%20", "%20");
+        testUrlToUriMapping("!", "!", "!", "!", "!");
+        testUrlToUriMapping("\"", "%22", "%22", "%22", "%22");
+        testUrlToUriMapping("#", null, null, null, "%23");
+        testUrlToUriMapping("$", "$", "$", "$", "$");
+        testUrlToUriMapping("&", "&", "&", "&", "&");
+        testUrlToUriMapping("'", "'", "'", "'", "'");
+        testUrlToUriMapping("(", "(", "(", "(", "(");
+        testUrlToUriMapping(")", ")", ")", ")", ")");
+        testUrlToUriMapping("*", "*", "*", "*", "*");
+        testUrlToUriMapping("+", "+", "+", "+", "+");
+        testUrlToUriMapping(",", ",", ",", ",", ",");
+        testUrlToUriMapping("-", "-", "-", "-", "-");
+        testUrlToUriMapping(".", ".", ".", ".", ".");
+        testUrlToUriMapping("/", null, "/", "/", "/");
+        testUrlToUriMapping(":", null, ":", ":", ":");
+        testUrlToUriMapping(";", ";", ";", ";", ";");
+        testUrlToUriMapping("<", "%3C", "%3C", "%3C", "%3C");
+        testUrlToUriMapping("=", "=", "=", "=", "=");
+        testUrlToUriMapping(">", "%3E", "%3E", "%3E", "%3E");
+        testUrlToUriMapping("?", null, null, "?", "?");
+        testUrlToUriMapping("@", "@", "@", "@", "@");
+        testUrlToUriMapping("[", null, "%5B", null, "%5B");
+        testUrlToUriMapping("\\", "%5C", "%5C", "%5C", "%5C");
+        testUrlToUriMapping("]", null, "%5D", null, "%5D");
+        testUrlToUriMapping("^", "%5E", "%5E", "%5E", "%5E");
+        testUrlToUriMapping("_", "_", "_", "_", "_");
+        testUrlToUriMapping("`", "%60", "%60", "%60", "%60");
+        testUrlToUriMapping("{", "%7B", "%7B", "%7B", "%7B");
+        testUrlToUriMapping("|", "%7C", "%7C", "%7C", "%7C");
+        testUrlToUriMapping("}", "%7D", "%7D", "%7D", "%7D");
+        testUrlToUriMapping("~", "~", "~", "~", "~");
+        testUrlToUriMapping("~", "~", "~", "~", "~");
+        testUrlToUriMapping("\u007f", "%7F", "%7F", "%7F", "%7F");
+
+        // beyond ascii
+        testUrlToUriMapping("\u0080", "%C2%80", "%C2%80", "%C2%80", "%C2%80");
+        testUrlToUriMapping("\u20ac", "\u20ac", "\u20ac", "\u20ac", "\u20ac");
+        testUrlToUriMapping("\ud842\udf9f",
+                "\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f");
+    }
+
+    public void SUPPRESSED_testLenientUrlToUriNul() throws Exception {
+        testUrlToUriMapping("\u0000", "%00", "%00", "%00", "%00"); // RI fails this
+    }
+
+    private void testUrlToUriMapping(String string, String asAuthority, String asFile,
+            String asQuery, String asFragment) throws Exception {
+        if (asAuthority != null) {
+            assertEquals("http://host" + asAuthority + ".tld/",
+                    backdoorUrlToUri(new URL("http://host" + string + ".tld/")).toString());
+        }
+        if (asFile != null) {
+            assertEquals("http://host.tld/file" + asFile + "/",
+                    backdoorUrlToUri(new URL("http://host.tld/file" + string + "/")).toString());
+        }
+        if (asQuery != null) {
+            assertEquals("http://host.tld/file?q" + asQuery + "=x",
+                    backdoorUrlToUri(new URL("http://host.tld/file?q" + string + "=x")).toString());
+        }
+        assertEquals("http://host.tld/file#" + asFragment + "-x",
+                backdoorUrlToUri(new URL("http://host.tld/file#" + asFragment + "-x")).toString());
+    }
+
+    /**
+     * Exercises HttpURLConnection to convert URL to a URI. Unlike URL#toURI,
+     * HttpURLConnection recovers from URLs with unescaped but unsupported URI
+     * characters like '{' and '|' by escaping these characters.
+     */
+    private URI backdoorUrlToUri(URL url) throws Exception {
+        final AtomicReference<URI> uriReference = new AtomicReference<URI>();
+
+        ResponseCache.setDefault(new ResponseCache() {
+            @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+                return null;
+            }
+            @Override public CacheResponse get(URI uri, String requestMethod,
+                    Map<String, List<String>> requestHeaders) throws IOException {
+                uriReference.set(uri);
+                throw new UnsupportedOperationException();
+            }
+        });
+
+        try {
+            OkHttpConnection connection = openConnection(url);
+            connection.getResponseCode();
+        } catch (Exception expected) {
+            if (expected.getCause() instanceof URISyntaxException) {
+                expected.printStackTrace();
+            }
+        }
+
+        return uriReference.get();
+    }
+
+    /**
+     * Don't explode if the cache returns a null body. http://b/3373699
+     */
+    public void testResponseCacheReturnsNullOutputStream() throws Exception {
+        final AtomicBoolean aborted = new AtomicBoolean();
+        ResponseCache.setDefault(new ResponseCache() {
+            @Override public CacheResponse get(URI uri, String requestMethod,
+                    Map<String, List<String>> requestHeaders) throws IOException {
+                return null;
+            }
+            @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+                return new CacheRequest() {
+                    @Override public void abort() {
+                        aborted.set(true);
+                    }
+                    @Override public OutputStream getBody() throws IOException {
+                        return null;
+                    }
+                };
+            }
+        });
+
+        server.enqueue(new MockResponse().setBody("abcdef"));
+        server.play();
+
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        InputStream in = connection.getInputStream();
+        assertEquals("abc", readAscii(in, 3));
+        in.close();
+        assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here
+    }
+
+
+    /**
+     * http://code.google.com/p/android/issues/detail?id=14562
+     */
+    public void testReadAfterLastByte() throws Exception {
+        server.enqueue(new MockResponse()
+                .setBody("ABC")
+                .clearHeaders()
+                .addHeader("Connection: close")
+                .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+        server.play();
+
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        InputStream in = connection.getInputStream();
+        assertEquals("ABC", readAscii(in, 3));
+        assertEquals(-1, in.read());
+        assertEquals(-1, in.read()); // throws IOException in Gingerbread
+    }
+
+    public void testGetContent() throws Exception {
+        server.enqueue(new MockResponse()
+                .addHeader("Content-Type: text/plain")
+                .setBody("A"));
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        InputStream in = (InputStream) connection.getContent();
+        assertEquals("A", readAscii(in, Integer.MAX_VALUE));
+    }
+
+    public void testGetContentOfType() throws Exception {
+        server.enqueue(new MockResponse()
+                .addHeader("Content-Type: text/plain")
+                .setBody("A"));
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        try {
+            connection.getContent(null);
+            fail();
+        } catch (NullPointerException expected) {
+        }
+        try {
+            connection.getContent(new Class[] { null });
+            fail();
+        } catch (NullPointerException expected) {
+        }
+        assertNull(connection.getContent(new Class[] { getClass() }));
+        connection.disconnect();
+    }
+
+    public void testGetOutputStreamOnGetFails() throws Exception {
+        server.enqueue(new MockResponse());
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        try {
+            connection.getOutputStream();
+            fail();
+        } catch (ProtocolException expected) {
+        }
+    }
+
+    public void testGetOutputAfterGetInputStreamFails() throws Exception {
+        server.enqueue(new MockResponse());
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        connection.setDoOutput(true);
+        try {
+            connection.getInputStream();
+            connection.getOutputStream();
+            fail();
+        } catch (ProtocolException expected) {
+        }
+    }
+
+    public void testSetDoOutputOrDoInputAfterConnectFails() throws Exception {
+        server.enqueue(new MockResponse());
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        connection.connect();
+        try {
+            connection.setDoOutput(true);
+            fail();
+        } catch (IllegalStateException expected) {
+        }
+        try {
+            connection.setDoInput(true);
+            fail();
+        } catch (IllegalStateException expected) {
+        }
+        connection.disconnect();
+    }
+
+    public void testClientSendsContentLength() throws Exception {
+        server.enqueue(new MockResponse().setBody("A"));
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        connection.setDoOutput(true);
+        OutputStream out = connection.getOutputStream();
+        out.write(new byte[] { 'A', 'B', 'C' });
+        out.close();
+        assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+        RecordedRequest request = server.takeRequest();
+        assertContains(request.getHeaders(), "Content-Length: 3");
+    }
+
+    public void testGetContentLengthConnects() throws Exception {
+        server.enqueue(new MockResponse().setBody("ABC"));
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        assertEquals(3, connection.getContentLength());
+        connection.disconnect();
+    }
+
+    public void testGetContentTypeConnects() throws Exception {
+        server.enqueue(new MockResponse()
+                .addHeader("Content-Type: text/plain")
+                .setBody("ABC"));
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        assertEquals("text/plain", connection.getContentType());
+        connection.disconnect();
+    }
+
+    public void testGetContentEncodingConnects() throws Exception {
+        server.enqueue(new MockResponse()
+                .addHeader("Content-Encoding: identity")
+                .setBody("ABC"));
+        server.play();
+        OkHttpConnection connection = openConnection(server.getUrl("/"));
+        assertEquals("identity", connection.getContentEncoding());
+        connection.disconnect();
+    }
+
+    // http://b/4361656
+    public void testUrlContainsQueryButNoPath() throws Exception {
+        server.enqueue(new MockResponse().setBody("A"));
+        server.play();
+        URL url = new URL("http", server.getHostName(), server.getPort(), "?query");
+        assertEquals("A", readAscii(openConnection(url).getInputStream(), Integer.MAX_VALUE));
+        RecordedRequest request = server.takeRequest();
+        assertEquals("GET /?query HTTP/1.1", request.getRequestLine());
+    }
+
+    // http://code.google.com/p/android/issues/detail?id=20442
+    public void testInputStreamAvailableWithChunkedEncoding() throws Exception {
+        testInputStreamAvailable(TransferKind.CHUNKED);
+    }
+
+    public void testInputStreamAvailableWithContentLengthHeader() throws Exception {
+        testInputStreamAvailable(TransferKind.FIXED_LENGTH);
+    }
+
+    public void testInputStreamAvailableWithNoLengthHeaders() throws Exception {
+        testInputStreamAvailable(TransferKind.END_OF_STREAM);
+    }
+
+    private void testInputStreamAvailable(TransferKind transferKind) throws IOException {
+        String body = "ABCDEFGH";
+        MockResponse response = new MockResponse();
+        transferKind.setBody(response, body, 4);
+        server.enqueue(response);
+        server.play();
+        URLConnection connection = openConnection(server.getUrl("/"));
+        InputStream in = connection.getInputStream();
+        for (int i = 0; i < body.length(); i++) {
+            assertTrue(in.available() >= 0);
+            assertEquals(body.charAt(i), in.read());
+        }
+        assertEquals(0, in.available());
+        assertEquals(-1, in.read());
+    }
+
+    /**
+     * Returns a gzipped copy of {@code bytes}.
+     */
+    public byte[] gzip(byte[] bytes) throws IOException {
+        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+        OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
+        gzippedOut.write(bytes);
+        gzippedOut.close();
+        return bytesOut.toByteArray();
+    }
+
+    /**
+     * Reads at most {@code limit} characters from {@code in} and asserts that
+     * content equals {@code expected}.
+     */
+    private void assertContent(String expected, URLConnection connection, int limit)
+            throws IOException {
+        connection.connect();
+        assertEquals(expected, readAscii(connection.getInputStream(), limit));
+        ((OkHttpConnection) connection).disconnect();
+    }
+
+    private void assertContent(String expected, URLConnection connection) throws IOException {
+        assertContent(expected, connection, Integer.MAX_VALUE);
+    }
+
+    private void assertContains(List<String> headers, String header) {
+        assertTrue(headers.toString(), headers.contains(header));
+    }
+
+    private void assertContainsNoneMatching(List<String> headers, String pattern) {
+        for (String header : headers) {
+            if (header.matches(pattern)) {
+                fail("Header " + header + " matches " + pattern);
+            }
+        }
+    }
+
+    private Set<String> newSet(String... elements) {
+        return new HashSet<String>(Arrays.asList(elements));
+    }
+
+    enum TransferKind {
+        CHUNKED() {
+            @Override void setBody(MockResponse response, byte[] content, int chunkSize)
+                    throws IOException {
+                response.setChunkedBody(content, chunkSize);
+            }
+        },
+        FIXED_LENGTH() {
+            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
+                response.setBody(content);
+            }
+        },
+        END_OF_STREAM() {
+            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
+                response.setBody(content);
+                response.setSocketPolicy(DISCONNECT_AT_END);
+                for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
+                    if (h.next().startsWith("Content-Length:")) {
+                        h.remove();
+                        break;
+                    }
+                }
+            }
+        };
+
+        abstract void setBody(MockResponse response, byte[] content, int chunkSize)
+                throws IOException;
+
+        void setBody(MockResponse response, String content, int chunkSize) throws IOException {
+            setBody(response, content.getBytes("UTF-8"), chunkSize);
+        }
+    }
+
+    enum ProxyConfig {
+        NO_PROXY() {
+            @Override public OkHttpConnection connect(MockWebServer server, URL url)
+                    throws IOException {
+                return openConnection(url, Proxy.NO_PROXY);
+            }
+        },
+
+        CREATE_ARG() {
+            @Override public OkHttpConnection connect(MockWebServer server, URL url)
+                    throws IOException {
+                return openConnection(url, server.toProxyAddress());
+            }
+        },
+
+        PROXY_SYSTEM_PROPERTY() {
+            @Override public OkHttpConnection connect(MockWebServer server, URL url)
+                    throws IOException {
+                System.setProperty("proxyHost", "localhost");
+                System.setProperty("proxyPort", Integer.toString(server.getPort()));
+                return openConnection(url);
+            }
+        },
+
+        HTTP_PROXY_SYSTEM_PROPERTY() {
+            @Override public OkHttpConnection connect(MockWebServer server, URL url)
+                    throws IOException {
+                System.setProperty("http.proxyHost", "localhost");
+                System.setProperty("http.proxyPort", Integer.toString(server.getPort()));
+                return openConnection(url);
+            }
+        },
+
+        HTTPS_PROXY_SYSTEM_PROPERTY() {
+            @Override public OkHttpConnection connect(MockWebServer server, URL url)
+                    throws IOException {
+                System.setProperty("https.proxyHost", "localhost");
+                System.setProperty("https.proxyPort", Integer.toString(server.getPort()));
+                return openConnection(url);
+            }
+        };
+
+        public abstract OkHttpConnection connect(MockWebServer server, URL url) throws IOException;
+    }
+
+    private static class RecordingTrustManager implements X509TrustManager {
+        private final List<String> calls = new ArrayList<String>();
+
+        public X509Certificate[] getAcceptedIssuers() {
+            calls.add("getAcceptedIssuers");
+            return new X509Certificate[] {};
+        }
+
+        public void checkClientTrusted(X509Certificate[] chain, String authType)
+                throws CertificateException {
+            calls.add("checkClientTrusted " + certificatesToString(chain) + " " + authType);
+        }
+
+        public void checkServerTrusted(X509Certificate[] chain, String authType)
+                throws CertificateException {
+            calls.add("checkServerTrusted " + certificatesToString(chain) + " " + authType);
+        }
+
+        private String certificatesToString(X509Certificate[] certificates) {
+            List<String> result = new ArrayList<String>();
+            for (X509Certificate certificate : certificates) {
+                result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber());
+            }
+            return result.toString();
+        }
+    }
+
+    private static class RecordingHostnameVerifier implements HostnameVerifier {
+        private final List<String> calls = new ArrayList<String>();
+
+        public boolean verify(String hostname, SSLSession session) {
+            calls.add("verify " + hostname);
+            return true;
+        }
+    }
+}
diff --git a/src/test/java/libcore/net/spdy/MockSpdyPeer.java b/src/test/java/libcore/net/spdy/MockSpdyPeer.java
new file mode 100644
index 0000000..0ea3d86
--- /dev/null
+++ b/src/test/java/libcore/net/spdy/MockSpdyPeer.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.spdy;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import libcore.io.Streams;
+
+/**
+ * Replays prerecorded outgoing frames and records incoming frames.
+ */
+public final class MockSpdyPeer {
+    private int frameCount = 0;
+    private final List<OutFrame> outFrames = new ArrayList<OutFrame>();
+    private final BlockingQueue<InFrame> inFrames = new LinkedBlockingQueue<InFrame>();
+    private int port;
+    private final Executor executor = Executors.newCachedThreadPool(
+            Threads.newThreadFactory("MockSpdyPeer"));
+
+    public void acceptFrame() {
+        frameCount++;
+    }
+
+    public SpdyWriter sendFrame() {
+        OutFrame frame = new OutFrame(frameCount++);
+        outFrames.add(frame);
+        return new SpdyWriter(frame.out);
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public InFrame takeFrame() throws InterruptedException {
+        return inFrames.take();
+    }
+
+    public void play() throws IOException {
+        final ServerSocket serverSocket = new ServerSocket(0);
+        serverSocket.setReuseAddress(true);
+        this.port = serverSocket.getLocalPort();
+        executor.execute(new Runnable() {
+            @Override public void run() {
+                try {
+                    readAndWriteFrames(serverSocket);
+                } catch (IOException e) {
+                    e.printStackTrace(); // TODO
+                }
+            }
+        });
+    }
+
+    private void readAndWriteFrames(ServerSocket serverSocket) throws IOException {
+        Socket socket = serverSocket.accept();
+        OutputStream out = socket.getOutputStream();
+        InputStream in = socket.getInputStream();
+
+        Iterator<OutFrame> outFramesIterator = outFrames.iterator();
+        OutFrame nextOutFrame = null;
+
+        for (int i = 0; i < frameCount; i++) {
+            if (nextOutFrame == null && outFramesIterator.hasNext()) {
+                nextOutFrame = outFramesIterator.next();
+            }
+
+            if (nextOutFrame != null && nextOutFrame.sequence == i) {
+                // write a frame
+                nextOutFrame.out.writeTo(out);
+                nextOutFrame = null;
+
+            } else {
+                // read a frame
+                SpdyReader reader = new SpdyReader(in);
+                byte[] data = null;
+                int type = reader.nextFrame();
+                if (type == SpdyConnection.TYPE_DATA) {
+                    data = new byte[reader.length];
+                    Streams.readFully(in, data);
+                }
+                inFrames.add(new InFrame(i, reader, data));
+            }
+        }
+    }
+
+    public Socket openSocket() throws IOException {
+        return new Socket("localhost", port);
+    }
+
+    private static class OutFrame {
+        private final int sequence;
+        private final ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+        private OutFrame(int sequence) {
+            this.sequence = sequence;
+        }
+    }
+
+    public static class InFrame {
+        public final int sequence;
+        public final SpdyReader reader;
+        public final byte[] data;
+
+        public InFrame(int sequence, SpdyReader reader, byte[] data) {
+            this.sequence = sequence;
+            this.reader = reader;
+            this.data = data;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/libcore/net/spdy/SpdyConnectionTest.java b/src/test/java/libcore/net/spdy/SpdyConnectionTest.java
new file mode 100644
index 0000000..09172fa
--- /dev/null
+++ b/src/test/java/libcore/net/spdy/SpdyConnectionTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2011 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 libcore.net.spdy;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.List;
+import junit.framework.TestCase;
+
+public final class SpdyConnectionTest extends TestCase {
+    private final MockSpdyPeer peer = new MockSpdyPeer();
+
+    public void testClientCreatesStreamAndServerReplies() throws Exception {
+        // write the mocking script
+        peer.acceptFrame();
+        SpdyWriter reply = peer.sendFrame();
+        reply.streamId = 1;
+        reply.nameValueBlock = Arrays.asList("a", "android");
+        reply.synReply();
+        SpdyWriter replyData = peer.sendFrame();
+        replyData.flags = SpdyConnection.FLAG_FIN;
+        replyData.streamId = 1;
+        replyData.data("robot".getBytes("UTF-8"));
+        peer.acceptFrame();
+        peer.play();
+
+        // play it back
+        SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
+        SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true);
+        List<String> responseHeaders = stream.getResponseHeaders();
+        assertEquals(Arrays.asList("a", "android"), responseHeaders);
+        BufferedReader reader = new BufferedReader(new InputStreamReader(stream.getInputStream()));
+        assertEquals("robot", reader.readLine());
+        assertEquals(null, reader.readLine());
+        OutputStream out = stream.getOutputStream();
+        out.write("c3po".getBytes("UTF-8"));
+        out.close();
+
+        // verify the peer received what was expected
+        MockSpdyPeer.InFrame synStream = peer.takeFrame();
+        assertEquals(0, synStream.reader.flags);
+        assertEquals(1, synStream.reader.streamId);
+        assertEquals(0, synStream.reader.associatedStreamId);
+        assertEquals(Arrays.asList("b", "banana"), synStream.reader.nameValueBlock);
+        MockSpdyPeer.InFrame requestData = peer.takeFrame();
+        assertTrue(Arrays.equals("c3po".getBytes("UTF-8"), requestData.data));
+    }
+
+    public void testServerCreatesStreamAndClientReplies() throws Exception {
+        // write the mocking script
+        SpdyWriter newStream = peer.sendFrame();
+        newStream.flags = 0;
+        newStream.streamId = 2;
+        newStream.associatedStreamId = 0;
+        newStream.nameValueBlock = Arrays.asList("a", "android");
+        newStream.synStream();
+        peer.acceptFrame();
+        peer.play();
+
+        // play it back
+        IncomingStreamHandler handler = new IncomingStreamHandler() {
+            @Override public void receive(SpdyStream stream) throws IOException {
+                assertEquals(Arrays.asList("a", "android"), stream.getRequestHeaders());
+                assertEquals(-1, stream.getRstStatusCode());
+                stream.reply(Arrays.asList("b", "banana"));
+            }
+        };
+        new SpdyConnection.Builder(true, peer.openSocket())
+                .handler(handler)
+                .build();
+
+        // verify the peer received what was expected
+        MockSpdyPeer.InFrame synStream = peer.takeFrame();
+        assertEquals(0, synStream.reader.flags);
+        assertEquals(2, synStream.reader.streamId);
+        assertEquals(0, synStream.reader.associatedStreamId);
+        assertEquals(Arrays.asList("b", "banana"), synStream.reader.nameValueBlock);
+    }
+}
diff --git a/src/test/java/libcore/net/ssl/SslContextBuilder.java b/src/test/java/libcore/net/ssl/SslContextBuilder.java
new file mode 100644
index 0000000..d88ca9c
--- /dev/null
+++ b/src/test/java/libcore/net/ssl/SslContextBuilder.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 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 libcore.net.ssl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import javax.security.auth.x500.X500Principal;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.x509.X509V3CertificateGenerator;
+
+/**
+ * Constructs an SSL context for testing. This uses Bouncy Castle to generate a
+ * self-signed certificate for a single hostname such as "localhost".
+ *
+ * <p>The crypto performed by this class is relatively slow. Clients should
+ * reuse SSL context instances where possible.
+ */
+public final class SslContextBuilder {
+    static {
+        Security.addProvider(new BouncyCastleProvider());
+    }
+
+    private static final long ONE_DAY_MILLIS = 1000L * 60 * 60 * 24;
+    private final String hostName;
+    private long notBefore = System.currentTimeMillis();
+    private long notAfter = System.currentTimeMillis() + ONE_DAY_MILLIS;
+
+    /**
+     * @param hostName the subject of the host. For TLS this should be the
+     *     domain name that the client uses to identify the server.
+     */
+    public SslContextBuilder(String hostName) {
+        this.hostName = hostName;
+    }
+
+    public SSLContext build() throws GeneralSecurityException {
+        char[] password = "password".toCharArray();
+
+        // Generate public and private keys and use them to make a self-signed certificate.
+        KeyPair keyPair = generateKeyPair();
+        X509Certificate certificate = selfSignedCertificate(keyPair);
+
+        // Put 'em in a key store.
+        KeyStore keyStore = newEmptyKeyStore(password);
+        Certificate[] certificateChain = {
+                certificate
+        };
+        keyStore.setKeyEntry("private", keyPair.getPrivate(), password, certificateChain);
+        keyStore.setCertificateEntry("cert", certificate);
+
+        // Wrap it up in an SSL context.
+        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
+                KeyManagerFactory.getDefaultAlgorithm());
+        keyManagerFactory.init(keyStore, password);
+        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
+                TrustManagerFactory.getDefaultAlgorithm());
+        trustManagerFactory.init(keyStore);
+        SSLContext sslContext = SSLContext.getInstance("TLS");
+        sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(),
+                new SecureRandom());
+        return sslContext;
+    }
+
+    private KeyPair generateKeyPair() throws GeneralSecurityException {
+        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
+        keyPairGenerator.initialize(1024, new SecureRandom());
+        return keyPairGenerator.generateKeyPair();
+    }
+
+    /**
+     * Generates a certificate for {@code hostName} containing {@code keyPair}'s
+     * public key, signed by {@code keyPair}'s private key.
+     */
+    @SuppressWarnings("deprecation") // use the old Bouncy Castle APIs to reduce dependencies.
+    private X509Certificate selfSignedCertificate(KeyPair keyPair) throws GeneralSecurityException {
+        X509V3CertificateGenerator generator = new X509V3CertificateGenerator();
+        X500Principal issuer = new X500Principal("CN=" + hostName);
+        X500Principal subject = new X500Principal("CN=" + hostName);
+        generator.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
+        generator.setIssuerDN(issuer);
+        generator.setNotBefore(new Date(notBefore));
+        generator.setNotAfter(new Date(notAfter));
+        generator.setSubjectDN(subject);
+        generator.setPublicKey(keyPair.getPublic());
+        generator.setSignatureAlgorithm("SHA256WithRSAEncryption");
+        return generator.generateX509Certificate(keyPair.getPrivate(), "BC");
+    }
+
+    private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
+        try {
+            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+            InputStream in = null; // By convention, 'null' creates an empty key store.
+            keyStore.load(in, password);
+            return keyStore;
+        } catch (IOException e) {
+            throw new AssertionError(e);
+        }
+    }
+}