added fireos code from https://github.com/archananaik/cordova-amazon-fireos sans history
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/LICENSE
@@ -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.
\ No newline at end of file
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..b8172a6
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,17 @@
+Apache Cordova
+Copyright 2012 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org)
+
+=========================================================================
+==  NOTICE file corresponding to the section 4 d of                    ==
+==  the Apache License, Version 2.0,                                   ==
+==  in this case for the Android-specific code.                        ==
+=========================================================================
+
+Android Code
+Copyright 2005-2008 The Android Open Source Project
+
+This product includes software developed as part of
+The Android Open Source Project (http://source.android.com).
diff --git a/README.md b/README.md
new file mode 100755
index 0000000..e4c9eea
--- /dev/null
+++ b/README.md
@@ -0,0 +1,86 @@
+<!--
+#
+# 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.
+#
+-->
+Cordova Amazon Fire OS
+===
+
+Cordova Amazon Fire OS is an application library that allows for Cordova-based
+projects to be built for the Amazon Fire OS Platform. It uses Amazon's web app runtime that is built on open-source Chromium project. With the web app runtime, your web apps can achieve fluidity and speed approaching that of native apps. 
+
+[Apache Cordova](http://cordova.io) is a project at The Apache Software Foundation (ASF).
+
+
+Requires
+---
+
+- Java JDK 1.5 or greater
+- Apache ANT 1.8.0 or greater
+- Android SDK [http://developer.android.com](http://developer.android.com)
+- Amazon WebView SDK [https://developer.amazon.com/sdk/fire/IntegratingAWV.html#installawv](https://developer.amazon.com/sdk/fire/IntegratingAWV.html#installawv)
+ 
+Cordova Amazon Fire OS Developer Tools
+---
+
+The Cordova developer tooling is split between general tooling and project level tooling. 
+
+General Commands
+
+    ./bin/create [path package activity] ... create the ./example app or a cordova-amazon-fireos project
+    ./bin/check_reqs ....................... checks that your environment is set up for cordova-amazon-fireos development
+    ./bin/update [path] .................... updates an existing cordova-amazon-fireos project to the version of the framework
+
+Project Commands
+
+These commands live in a generated Cordova Amazon Fire OS project. Emulator support is currently not available.
+
+    ./cordova/clean ........................ cleans the project
+    ./cordova/build ........................ calls `clean` then compiles the project
+    ./cordova/log   ........................ stream device logs to stdout
+    ./cordova/run   ........................ calls `build` then deploys to a connected Amazon device. 
+    ./cordova/version ...................... returns the cordova-amazon-fireos version of the current project
+
+Importing a Cordova Amazon Fire OS Project into Eclipse
+----
+
+1. File > New > Project...
+2. Android > Android Project
+3. Create project from existing source (point to the generated app found in platforms/amazon-fireos)
+4. Right click on libs/cordova.jar and add to build path
+5  Right click on libs/awv_interface.jar and add to build path
+6. Right click on the project root: Run as > Run Configurations
+7. Click on the Target tab and select Manual (this way you can choose the device to build to)
+
+Building without the Tooling
+---
+Note: The Developer Tools handle this.  This is only to be done if the tooling fails, or if 
+you are developing directly against the framework.
+
+
+To create your `cordova.jar` file, run in the framework directory:
+
+    android update project -p . -t android-17
+    ant jar
+
+Further Reading
+---
+- [https://developer.amazon.com/sdk/fire.html] (https://developer.amazon.com/sdk/fire.html)
+- [http://developer.android.com](http://developer.android.com)
+- [http://cordova.apache.org/](http://cordova.apache.org)
+- [http://wiki.apache.org/cordova/](http://wiki.apache.org/cordova/)
diff --git a/RELEASENOTES.md b/RELEASENOTES.md
new file mode 100644
index 0000000..915d563
--- /dev/null
+++ b/RELEASENOTES.md
@@ -0,0 +1,52 @@
+<!--
+#
+# 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.
+#
+-->
+## Release Notes for Cordova (Android) ##
+
+### 3.1.0 (Sept 2013) ###
+
+55 commits from 9 authors. Highlights include:
+
+* [CB-4817] Remove unused assets in project template.
+* Fail fast in create script if package name is not com.foo.bar.
+* [CB-4782] Convert ApplicationInfo.java -> appinfo.js
+* [CB-4766] Deprecated JSONUtils.java (moved into plugins)
+* [CB-4765] Deprecated ExifHelper.java (moved into plugins)
+* [CB-4764] Deprecated DirectoryManager.java (moved into plugins)
+* [CB-4763] Deprecated FileHelper.java (moved into plugins), Move getMimeType() into CordovaResourceApi.
+* [CB-4725] Add CordovaWebView.CORDOVA_VERSION constant
+* Incremeting version check for Android 4.3 API Level 18
+* [CB-3542] rewrote cli tooling scripts in node
+* Allow CordovaChromeClient subclasses access to CordovaInterface and CordovaWebView members
+* Refactor CordovaActivity.init so that subclasses can easily override factory methods for webview objects
+* [CB-4652] Allow default project template to be overridden on create
+* Tweak the online bridge to not send excess online events.
+* [CB-4495] Modify start-emulator script to exit immediately on a fatal emulator error.
+* Log WebView IOExceptions only when they are not 404s
+* Use a higher threshold for slow exec() warnings when debugger is attached.
+* Fix data URI decoding in CordovaResourceApi
+* [CB-3819] Made it easier to set SplashScreen delay.
+* [CB-4013] Fixed loadUrlTimeoutValue preference.
+* Upgrading project to Android 4.3
+* [CB-4198] bin/create script should be better at handling non-word characters in activity name. Patched windows script as well.
+* [CB-4198] bin/create should handle spaces in activity better.
+* [CB-4096] Implemented new unified whitelist for android
+* [CB-3384] Fix thread assertion when plugins remap URIs
+
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..df4a767
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+3.2.0-dev
diff --git a/bin/check_reqs b/bin/check_reqs
new file mode 100755
index 0000000..4a8abee
--- /dev/null
+++ b/bin/check_reqs
@@ -0,0 +1,27 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var check_reqs = require('./lib/check_reqs');
+
+if(!check_reqs.run()) {
+      process.exit(2);
+}
+
diff --git a/bin/check_reqs.bat b/bin/check_reqs.bat
new file mode 100644
index 0000000..cb2c6f5
--- /dev/null
+++ b/bin/check_reqs.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0check_reqs"
+IF EXIST %script_path% (
+        node "%script_path%" %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'check_reqs' script in 'bin' folder, aborting...>&2
+    EXIT /B 1
+)
diff --git a/bin/create b/bin/create
new file mode 100755
index 0000000..020bf86
--- /dev/null
+++ b/bin/create
@@ -0,0 +1,36 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+var path = require('path');
+var create = require('./lib/create');
+var args = process.argv;
+
+// Support basic help commands
+if(args.length < 3 || (args[2] == '--help' || args[2] == '/?' || args[2] == '-h' ||
+                    args[2] == 'help' || args[2] == '-help' || args[2] == '/help')) {
+    console.log('Usage: ' + path.relative(process.cwd(), path.join(__dirname, 'create')) + ' <path_to_new_project> <package_name> <project_name>');
+    console.log('    <path_to_new_project>: Path to your new Cordova Android project');
+    console.log('    <package_name>: Package name, following reverse-domain style convention');
+    console.log('    <project_name>: Project name');
+    process.exit(1);
+} else {
+    create.createProject(args[2], args[3], args[4], args[5]);
+}
+
diff --git a/bin/create.bat b/bin/create.bat
new file mode 100644
index 0000000..4b475a2
--- /dev/null
+++ b/bin/create.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0create"
+IF EXIST %script_path% (
+    node %script_path% %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'create' script in 'bin' folder, aborting...>&2
+    EXIT /B 1
+)
\ No newline at end of file
diff --git a/bin/lib/check_reqs.js b/bin/lib/check_reqs.js
new file mode 100644
index 0000000..c064499
--- /dev/null
+++ b/bin/lib/check_reqs.js
@@ -0,0 +1,78 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var shell = require('shelljs'),
+    path  = require('path'),
+    fs    = require('fs'),
+    ROOT  = path.join(__dirname, '..', '..');
+
+// Get valid target from framework/project.properties
+module.exports.get_target = function() {
+    if(fs.existsSync(path.join(ROOT, 'framework', 'project.properties'))) {
+        var target = shell.grep(/target=android-[\d+]/, path.join(ROOT, 'framework', 'project.properties'));
+        return target.split('=')[1].replace('\n', '').replace('\r', '').replace(' ', '');
+    } else if (fs.existsSync(path.join(ROOT, 'project.properties'))) {
+        // if no target found, we're probably in a project and project.properties is in ROOT.
+        var target = shell.grep(/target=android-[\d+]/, path.join(ROOT, 'project.properties'));
+        return target.split('=')[1].replace('\n', '').replace('\r', '').replace(' ', '');
+    }
+}
+
+module.exports.check_ant = function() {
+    var test = shell.exec('ant -version', {silent:true, async:false});
+    if(test.code > 0) {
+        console.error('ERROR : executing command \'ant\', make sure you have ant installed and added to your path.');
+        return false;
+    }
+    return true;
+}
+
+module.exports.check_java = function() {
+    if(process.env.JAVA_HOME) {
+        var test = shell.exec('java', {silent:true, async:false});
+        if(test.code > 0) {
+            console.error('ERROR : executing command \'java\', make sure you java environment is set up. Including your JDK and JRE.');
+            return false;
+        }
+        return true;
+    } else {
+        console.error('ERROR : Make sure JAVA_HOME is set, as well as paths to your JDK and JRE for java.');
+        return false;
+    }
+}
+
+module.exports.check_android = function() {
+    var valid_target = this.get_target();
+    var targets = shell.exec('android list targets', {silent:true, async:false});
+
+    if(targets.code > 0 && targets.output.match(/command\snot\sfound/)) {
+        console.error('The command \"android\" failed. Make sure you have the latest Android SDK installed, and the \"android\" command (inside the tools/ folder) is added to your path.');
+        return false;
+    } else if(!targets.output.match(valid_target)) {
+        console.error('Please install Android target ' + valid_target.split('-')[1] + ' (the Android newest SDK). Make sure you have the latest Android tools installed as well. Run \"android\" from your command-line to install/update any missing SDKs or tools.');
+        return false;
+    }
+    return true;
+}
+
+module.exports.run = function() {
+    return this.check_ant() && this.check_java && this.check_android();
+}
diff --git a/bin/lib/create.js b/bin/lib/create.js
new file mode 100755
index 0000000..221092b
--- /dev/null
+++ b/bin/lib/create.js
@@ -0,0 +1,195 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+var shell = require('shelljs'),
+    path  = require('path'),
+    fs    = require('fs'),
+    check_reqs = require('./check_reqs'),
+    ROOT    = path.join(__dirname, '..', '..');
+
+function exec(command) {
+    var result;
+    try {
+        result = shell.exec(command, {silent:false, async:false});
+    } catch(e) {
+        console.error('Command error on execuation : ' + command);
+        console.error(e);
+        process.exit(2);
+    }
+    if(result && result.code > 0) {
+        console.error('Command failed to execute : ' + command);
+        console.error(result.output);
+        process.exit(2);
+    } else {
+        return result;
+    }
+}
+
+function setShellFatal(value, func) {
+    var oldVal = shell.config.fatal;
+    shell.config.fatal = value;
+    func();
+    shell.config.fatal = oldVal;
+}
+
+function ensureJarIsBuilt(version, target_api) {
+    var isDevVersion = /-dev$/.test(version);
+    if (isDevVersion || !fs.existsSync(path.join(ROOT, 'framework', 'cordova-' + version + '.jar')) && fs.existsSync(path.join(ROOT, 'framework'))) {
+        var valid_target = check_reqs.get_target();
+        console.log('Building cordova-' + version + '.jar');
+        // update the cordova-android framework for the desired target
+        exec('android --silent update lib-project --target "' + target_api + '" --path "' + path.join(ROOT, 'framework') + '"');
+        // compile cordova.js and cordova.jar
+        var cwd = process.cwd();
+        process.chdir(path.join(ROOT, 'framework'));
+        exec('ant jar');
+        process.chdir(cwd);
+    }
+}
+
+function copyJsAndJar(projectPath, version) {
+    shell.cp('-f', path.join(ROOT, 'framework', 'assets', 'www', 'cordova.js'), path.join(projectPath, 'assets', 'www', 'cordova.js'));
+    // Don't fail if there are no old jars.
+    setShellFatal(false, function() {
+        shell.ls(path.join(projectPath, 'libs', 'cordova-*.jar')).forEach(function(oldJar) {
+            console.log("Deleting " + oldJar);
+            shell.rm('-f', path.join(oldJar));
+        });
+    });
+    shell.cp('-f', path.join(ROOT, 'framework', 'cordova-' + version + '.jar'), path.join(projectPath, 'libs', 'cordova-' + version + '.jar'));
+    shell.cp('-f', path.join(ROOT, 'framework', 'libs','awv_interface.jar'), path.join(projectPath, 'libs', 'awv_interface.jar'));
+}
+
+function copyScripts(projectPath) {
+    var srcScriptsDir = path.join(ROOT, 'bin', 'templates', 'cordova');
+    var destScriptsDir = path.join(projectPath, 'cordova');
+    // Delete old scripts directory if this is an update.
+    shell.rm('-rf', destScriptsDir);
+    // Copy in the new ones.
+    shell.cp('-r', srcScriptsDir, projectPath);
+    shell.cp('-r', path.join(ROOT, 'bin', 'node_modules'), destScriptsDir);
+    shell.cp(path.join(ROOT, 'bin', 'check_reqs'), path.join(destScriptsDir, 'check_reqs'));
+    shell.cp(path.join(ROOT, 'bin', 'lib', 'check_reqs.js'), path.join(projectPath, 'cordova', 'lib', 'check_reqs.js'));
+
+}
+
+/**
+ * $ create [options]
+ *
+ * Creates an android application with the given options.
+ *
+ * Options:
+ *
+ *   - `project_path` 	{String} Path to the new Cordova android project.
+ *   - `package_name`{String} Package name, following reverse-domain style convention.
+ *   - `project_name` 	{String} Project name.
+ *   - 'project_template_dir' {String} Path to project template (override).
+ */
+
+exports.createProject = function(project_path, package_name, project_name, project_template_dir) {
+    var VERSION = fs.readFileSync(path.join(ROOT, 'VERSION'), 'utf-8').trim();
+
+    // Set default values for path, package and name
+    project_path = typeof project_path !== 'undefined' ? project_path : "CordovaExample";
+    project_path = path.relative(process.cwd(), project_path);
+    package_name = typeof package_name !== 'undefined' ? package_name : 'my.cordova.project';
+    project_name = typeof project_name !== 'undefined' ? project_name : 'CordovaExample';
+    project_template_dir = typeof project_template_dir !== 'undefined' ? 
+                           project_template_dir : 
+                           path.join(ROOT, 'bin', 'templates', 'project');
+
+    var safe_activity_name = project_name.replace(/\W/g, '');
+    var package_as_path = package_name.replace(/\./g, path.sep);
+    var activity_dir    = path.join(project_path, 'src', package_as_path);
+    var activity_path   = path.join(activity_dir, safe_activity_name + '.java');
+    var target_api      = check_reqs.get_target();
+    var manifest_path   = path.join(project_path, 'AndroidManifest.xml');
+
+    // Check if project already exists
+    if(fs.existsSync(project_path)) {
+        console.error('Project already exists! Delete and recreate');
+        process.exit(2);
+    }
+
+    if (!/[a-zA-Z0-9_]+\.[a-zA-Z0-9_](.[a-zA-Z0-9_])*/.test(package_name)) {
+        console.error('Package name must look like: com.company.Name');
+        process.exit(2);
+    }
+
+    // Check that requirements are met and proper targets are installed
+    if(!check_reqs.run()) {
+        process.exit(2);
+    }
+
+    // Log the given values for the project
+    console.log('Creating Cordova project for the Android platform:');
+    console.log('\tPath: ' + project_path);
+    console.log('\tPackage: ' + package_name);
+    console.log('\tName: ' + project_name);
+    console.log('\tAndroid target: ' + target_api);
+
+    // build from source. distro should have these files
+    ensureJarIsBuilt(VERSION, target_api);
+
+    console.log('Copying template files...');
+
+    setShellFatal(true, function() {
+        // copy project template
+        shell.cp('-r', path.join(project_template_dir, 'assets'), project_path);
+        shell.cp('-r', path.join(project_template_dir, 'res'), project_path);
+        // Manually create directories that would be empty within the template (since git doesn't track directories).
+        shell.mkdir(path.join(project_path, 'libs'));
+
+        // copy cordova.js, cordova.jar and res/xml
+        shell.cp('-r', path.join(ROOT, 'framework', 'res', 'xml'), path.join(project_path, 'res'));
+        copyJsAndJar(project_path, VERSION);
+
+        // interpolate the activity name and package
+        shell.mkdir('-p', activity_dir);
+        shell.cp('-f', path.join(project_template_dir, 'Activity.java'), activity_path);
+        shell.sed('-i', /__ACTIVITY__/, safe_activity_name, activity_path);
+        shell.sed('-i', /__NAME__/, project_name, path.join(project_path, 'res', 'values', 'strings.xml'));
+        shell.sed('-i', /__ID__/, package_name, activity_path);
+
+        shell.cp('-f', path.join(project_template_dir, 'AndroidManifest.xml'), manifest_path);
+        shell.sed('-i', /__ACTIVITY__/, safe_activity_name, manifest_path);
+        shell.sed('-i', /__PACKAGE__/, package_name, manifest_path);
+        shell.sed('-i', /__APILEVEL__/, target_api.split('-')[1], manifest_path);
+        copyScripts(project_path);
+    });
+    // Link it to local android install.
+    console.log('Running "android update project"');
+    exec('android --silent update project --target "'+target_api+'" --path "'+ project_path+'"');
+    console.log('Project successfully created.');
+}
+
+exports.updateProject = function(projectPath) {
+    // Check that requirements are met and proper targets are installed
+    if (!check_reqs.run()) {
+        process.exit(2);
+    }
+    var version = fs.readFileSync(path.join(ROOT, 'VERSION'), 'utf-8').trim();
+    var target_api = check_reqs.get_target();
+    ensureJarIsBuilt(version, target_api);
+    copyJsAndJar(projectPath, version);
+    copyScripts(projectPath);
+    console.log('Android project is now at version ' + version);
+};
+
diff --git a/bin/node_modules/.bin/shjs b/bin/node_modules/.bin/shjs
new file mode 120000
index 0000000..a044997
--- /dev/null
+++ b/bin/node_modules/.bin/shjs
@@ -0,0 +1 @@
+../shelljs/bin/shjs
\ No newline at end of file
diff --git a/bin/node_modules/shelljs/.documentup.json b/bin/node_modules/shelljs/.documentup.json
new file mode 100644
index 0000000..57fe301
--- /dev/null
+++ b/bin/node_modules/shelljs/.documentup.json
@@ -0,0 +1,6 @@
+{
+  "name": "ShellJS",
+  "twitter": [
+    "r2r"
+  ]
+}
diff --git a/bin/node_modules/shelljs/.npmignore b/bin/node_modules/shelljs/.npmignore
new file mode 100644
index 0000000..c2658d7
--- /dev/null
+++ b/bin/node_modules/shelljs/.npmignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/bin/node_modules/shelljs/.travis.yml b/bin/node_modules/shelljs/.travis.yml
new file mode 100644
index 0000000..5caf599
--- /dev/null
+++ b/bin/node_modules/shelljs/.travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+node_js:
+  - 0.6
+  - 0.8
+
diff --git a/bin/node_modules/shelljs/LICENSE b/bin/node_modules/shelljs/LICENSE
new file mode 100644
index 0000000..1b35ee9
--- /dev/null
+++ b/bin/node_modules/shelljs/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2012, Artur Adib <aadib@mozilla.com>
+All rights reserved.
+
+You may use this project under the terms of the New BSD license as follows:
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of Artur Adib nor the
+      names of the contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
+ARE DISCLAIMED. IN NO EVENT SHALL ARTUR ADIB BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/bin/node_modules/shelljs/README.md b/bin/node_modules/shelljs/README.md
new file mode 100644
index 0000000..8b45592
--- /dev/null
+++ b/bin/node_modules/shelljs/README.md
@@ -0,0 +1,513 @@
+# ShellJS - Unix shell commands for Node.js [![Build Status](https://secure.travis-ci.org/arturadib/shelljs.png)](http://travis-ci.org/arturadib/shelljs)
+
+ShellJS is a portable **(Windows/Linux/OS X)** implementation of Unix shell commands on top of the Node.js API. You can use it to eliminate your shell script's dependency on Unix while still keeping its familiar and powerful commands. You can also install it globally so you can run it from outside Node projects - say goodbye to those gnarly Bash scripts!
+
+The project is [unit-tested](http://travis-ci.org/arturadib/shelljs) and battled-tested in projects like:
+
++ [PDF.js](http://github.com/mozilla/pdf.js) - Firefox's next-gen PDF reader
++ [Firebug](http://getfirebug.com/) - Firefox's infamous debugger
++ [JSHint](http://jshint.com) - Most popular JavaScript linter
++ [Zepto](http://zeptojs.com) - jQuery-compatible JavaScript library for modern browsers
++ [Yeoman](http://yeoman.io/) - Web application stack and development tool
++ [Deployd.com](http://deployd.com) - Open source PaaS for quick API backend generation
+
+and [many more](https://npmjs.org/browse/depended/shelljs).
+
+## Installing
+
+Via npm:
+
+```bash
+$ npm install [-g] shelljs
+```
+
+If the global option `-g` is specified, the binary `shjs` will be installed. This makes it possible to
+run ShellJS scripts much like any shell script from the command line, i.e. without requiring a `node_modules` folder:
+
+```bash
+$ shjs my_script
+```
+
+You can also just copy `shell.js` into your project's directory, and `require()` accordingly.
+
+
+## Examples
+
+### JavaScript
+
+```javascript
+require('shelljs/global');
+
+if (!which('git')) {
+  echo('Sorry, this script requires git');
+  exit(1);
+}
+
+// Copy files to release dir
+mkdir('-p', 'out/Release');
+cp('-R', 'stuff/*', 'out/Release');
+
+// Replace macros in each .js file
+cd('lib');
+ls('*.js').forEach(function(file) {
+  sed('-i', 'BUILD_VERSION', 'v0.1.2', file);
+  sed('-i', /.*REMOVE_THIS_LINE.*\n/, '', file);
+  sed('-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, cat('macro.js'), file);
+});
+cd('..');
+
+// Run external tool synchronously
+if (exec('git commit -am "Auto-commit"').code !== 0) {
+  echo('Error: Git commit failed');
+  exit(1);
+}
+```
+
+### CoffeeScript
+
+```coffeescript
+require 'shelljs/global'
+
+if not which 'git'
+  echo 'Sorry, this script requires git'
+  exit 1
+
+# Copy files to release dir
+mkdir '-p', 'out/Release'
+cp '-R', 'stuff/*', 'out/Release'
+
+# Replace macros in each .js file
+cd 'lib'
+for file in ls '*.js'
+  sed '-i', 'BUILD_VERSION', 'v0.1.2', file
+  sed '-i', /.*REMOVE_THIS_LINE.*\n/, '', file
+  sed '-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, cat 'macro.js', file
+cd '..'
+
+# Run external tool synchronously
+if (exec 'git commit -am "Auto-commit"').code != 0
+  echo 'Error: Git commit failed'
+  exit 1
+```
+
+## Global vs. Local
+
+The example above uses the convenience script `shelljs/global` to reduce verbosity. If polluting your global namespace is not desirable, simply require `shelljs`.
+
+Example:
+
+```javascript
+var shell = require('shelljs');
+shell.echo('hello world');
+```
+
+## Make tool
+
+A convenience script `shelljs/make` is also provided to mimic the behavior of a Unix Makefile. In this case all shell objects are global, and command line arguments will cause the script to execute only the corresponding function in the global `target` object. To avoid redundant calls, target functions are executed only once per script.
+
+Example (CoffeeScript):
+
+```coffeescript
+require 'shelljs/make'
+
+target.all = ->
+  target.bundle()
+  target.docs()
+
+target.bundle = ->
+  cd __dirname
+  mkdir 'build'
+  cd 'lib'
+  (cat '*.js').to '../build/output.js'
+
+target.docs = ->
+  cd __dirname
+  mkdir 'docs'
+  cd 'lib'
+  for file in ls '*.js'
+    text = grep '//@', file     # extract special comments
+    text.replace '//@', ''      # remove comment tags
+    text.to 'docs/my_docs.md'
+```
+
+To run the target `all`, call the above script without arguments: `$ node make`. To run the target `docs`: `$ node make docs`, and so on.
+
+
+
+<!-- 
+
+  DO NOT MODIFY BEYOND THIS POINT - IT'S AUTOMATICALLY GENERATED
+
+-->
+
+
+## Command reference
+
+
+All commands run synchronously, unless otherwise stated.
+
+
+### cd('dir')
+Changes to directory `dir` for the duration of the script
+
+### pwd()
+Returns the current directory.
+
+### ls([options ,] path [,path ...])
+### ls([options ,] path_array)
+Available options:
+
++ `-R`: recursive
++ `-A`: all files (include files beginning with `.`, except for `.` and `..`)
+
+Examples:
+
+```javascript
+ls('projs/*.js');
+ls('-R', '/users/me', '/tmp');
+ls('-R', ['/users/me', '/tmp']); // same as above
+```
+
+Returns array of files in the given path, or in current directory if no path provided.
+
+### find(path [,path ...])
+### find(path_array)
+Examples:
+
+```javascript
+find('src', 'lib');
+find(['src', 'lib']); // same as above
+find('.').filter(function(file) { return file.match(/\.js$/); });
+```
+
+Returns array of all files (however deep) in the given paths.
+
+The main difference from `ls('-R', path)` is that the resulting file names
+include the base directories, e.g. `lib/resources/file1` instead of just `file1`.
+
+### cp([options ,] source [,source ...], dest)
+### cp([options ,] source_array, dest)
+Available options:
+
++ `-f`: force
++ `-r, -R`: recursive
+
+Examples:
+
+```javascript
+cp('file1', 'dir1');
+cp('-Rf', '/tmp/*', '/usr/local/*', '/home/tmp');
+cp('-Rf', ['/tmp/*', '/usr/local/*'], '/home/tmp'); // same as above
+```
+
+Copies files. The wildcard `*` is accepted.
+
+### rm([options ,] file [, file ...])
+### rm([options ,] file_array)
+Available options:
+
++ `-f`: force
++ `-r, -R`: recursive
+
+Examples:
+
+```javascript
+rm('-rf', '/tmp/*');
+rm('some_file.txt', 'another_file.txt');
+rm(['some_file.txt', 'another_file.txt']); // same as above
+```
+
+Removes files. The wildcard `*` is accepted.
+
+### mv(source [, source ...], dest')
+### mv(source_array, dest')
+Available options:
+
++ `f`: force
+
+Examples:
+
+```javascript
+mv('-f', 'file', 'dir/');
+mv('file1', 'file2', 'dir/');
+mv(['file1', 'file2'], 'dir/'); // same as above
+```
+
+Moves files. The wildcard `*` is accepted.
+
+### mkdir([options ,] dir [, dir ...])
+### mkdir([options ,] dir_array)
+Available options:
+
++ `p`: full path (will create intermediate dirs if necessary)
+
+Examples:
+
+```javascript
+mkdir('-p', '/tmp/a/b/c/d', '/tmp/e/f/g');
+mkdir('-p', ['/tmp/a/b/c/d', '/tmp/e/f/g']); // same as above
+```
+
+Creates directories.
+
+### test(expression)
+Available expression primaries:
+
++ `'-b', 'path'`: true if path is a block device
++ `'-c', 'path'`: true if path is a character device
++ `'-d', 'path'`: true if path is a directory
++ `'-e', 'path'`: true if path exists
++ `'-f', 'path'`: true if path is a regular file
++ `'-L', 'path'`: true if path is a symboilc link
++ `'-p', 'path'`: true if path is a pipe (FIFO)
++ `'-S', 'path'`: true if path is a socket
+
+Examples:
+
+```javascript
+if (test('-d', path)) { /* do something with dir */ };
+if (!test('-f', path)) continue; // skip if it's a regular file
+```
+
+Evaluates expression using the available primaries and returns corresponding value.
+
+### cat(file [, file ...])
+### cat(file_array)
+
+Examples:
+
+```javascript
+var str = cat('file*.txt');
+var str = cat('file1', 'file2');
+var str = cat(['file1', 'file2']); // same as above
+```
+
+Returns a string containing the given file, or a concatenated string
+containing the files if more than one file is given (a new line character is
+introduced between each file). Wildcard `*` accepted.
+
+### 'string'.to(file)
+
+Examples:
+
+```javascript
+cat('input.txt').to('output.txt');
+```
+
+Analogous to the redirection operator `>` in Unix, but works with JavaScript strings (such as
+those returned by `cat`, `grep`, etc). _Like Unix redirections, `to()` will overwrite any existing file!_
+
+### sed([options ,] search_regex, replace_str, file)
+Available options:
+
++ `-i`: Replace contents of 'file' in-place. _Note that no backups will be created!_
+
+Examples:
+
+```javascript
+sed('-i', 'PROGRAM_VERSION', 'v0.1.3', 'source.js');
+sed(/.*DELETE_THIS_LINE.*\n/, '', 'source.js');
+```
+
+Reads an input string from `file` and performs a JavaScript `replace()` on the input
+using the given search regex and replacement string. Returns the new string after replacement.
+
+### grep([options ,] regex_filter, file [, file ...])
+### grep([options ,] regex_filter, file_array)
+Available options:
+
++ `-v`: Inverse the sense of the regex and print the lines not matching the criteria.
+
+Examples:
+
+```javascript
+grep('-v', 'GLOBAL_VARIABLE', '*.js');
+grep('GLOBAL_VARIABLE', '*.js');
+```
+
+Reads input string from given files and returns a string containing all lines of the
+file that match the given `regex_filter`. Wildcard `*` accepted.
+
+### which(command)
+
+Examples:
+
+```javascript
+var nodeExec = which('node');
+```
+
+Searches for `command` in the system's PATH. On Windows looks for `.exe`, `.cmd`, and `.bat` extensions.
+Returns string containing the absolute path to the command.
+
+### echo(string [,string ...])
+
+Examples:
+
+```javascript
+echo('hello world');
+var str = echo('hello world');
+```
+
+Prints string to stdout, and returns string with additional utility methods
+like `.to()`.
+
+### dirs([options | '+N' | '-N'])
+
+Available options:
+
++ `-c`: Clears the directory stack by deleting all of the elements.
+
+Arguments:
+
++ `+N`: Displays the Nth directory (counting from the left of the list printed by dirs when invoked without options), starting with zero.
++ `-N`: Displays the Nth directory (counting from the right of the list printed by dirs when invoked without options), starting with zero.
+
+Display the list of currently remembered directories. Returns an array of paths in the stack, or a single path if +N or -N was specified.
+
+See also: pushd, popd
+
+### pushd([options,] [dir | '-N' | '+N'])
+
+Available options:
+
++ `-n`: Suppresses the normal change of directory when adding directories to the stack, so that only the stack is manipulated.
+
+Arguments:
+
++ `dir`: Makes the current working directory be the top of the stack, and then executes the equivalent of `cd dir`.
++ `+N`: Brings the Nth directory (counting from the left of the list printed by dirs, starting with zero) to the top of the list by rotating the stack.
++ `-N`: Brings the Nth directory (counting from the right of the list printed by dirs, starting with zero) to the top of the list by rotating the stack.
+
+Examples:
+
+```javascript
+// process.cwd() === '/usr'
+pushd('/etc'); // Returns /etc /usr
+pushd('+1');   // Returns /usr /etc
+```
+
+Save the current directory on the top of the directory stack and then cd to `dir`. With no arguments, pushd exchanges the top two directories. Returns an array of paths in the stack.
+
+### popd([options,] ['-N' | '+N'])
+
+Available options:
+
++ `-n`: Suppresses the normal change of directory when removing directories from the stack, so that only the stack is manipulated.
+
+Arguments:
+
++ `+N`: Removes the Nth directory (counting from the left of the list printed by dirs), starting with zero.
++ `-N`: Removes the Nth directory (counting from the right of the list printed by dirs), starting with zero.
+
+Examples:
+
+```javascript
+echo(process.cwd()); // '/usr'
+pushd('/etc');       // '/etc /usr'
+echo(process.cwd()); // '/etc'
+popd();              // '/usr'
+echo(process.cwd()); // '/usr'
+```
+
+When no arguments are given, popd removes the top directory from the stack and performs a cd to the new top directory. The elements are numbered from 0 starting at the first directory listed with dirs; i.e., popd is equivalent to popd +0. Returns an array of paths in the stack.
+
+### exit(code)
+Exits the current process with the given exit code.
+
+### env['VAR_NAME']
+Object containing environment variables (both getter and setter). Shortcut to process.env.
+
+### exec(command [, options] [, callback])
+Available options (all `false` by default):
+
++ `async`: Asynchronous execution. Defaults to true if a callback is provided.
++ `silent`: Do not echo program output to console.
+
+Examples:
+
+```javascript
+var version = exec('node --version', {silent:true}).output;
+
+var child = exec('some_long_running_process', {async:true});
+child.stdout.on('data', function(data) {
+  /* ... do something with data ... */
+});
+
+exec('some_long_running_process', function(code, output) {
+  console.log('Exit code:', code);
+  console.log('Program output:', output);
+});
+```
+
+Executes the given `command` _synchronously_, unless otherwise specified.
+When in synchronous mode returns the object `{ code:..., output:... }`, containing the program's
+`output` (stdout + stderr)  and its exit `code`. Otherwise returns the child process object, and
+the `callback` gets the arguments `(code, output)`.
+
+**Note:** For long-lived processes, it's best to run `exec()` asynchronously as
+the current synchronous implementation uses a lot of CPU. This should be getting
+fixed soon.
+
+### chmod(octal_mode || octal_string, file)
+### chmod(symbolic_mode, file)
+
+Available options:
+
++ `-v`: output a diagnostic for every file processed
++ `-c`: like verbose but report only when a change is made
++ `-R`: change files and directories recursively
+
+Examples:
+
+```javascript
+chmod(755, '/Users/brandon');
+chmod('755', '/Users/brandon'); // same as above 
+chmod('u+x', '/Users/brandon');
+```
+
+Alters the permissions of a file or directory by either specifying the
+absolute permissions in octal form or expressing the changes in symbols.
+This command tries to mimic the POSIX behavior as much as possible.
+Notable exceptions:
+
++ In symbolic modes, 'a-r' and '-r' are identical.  No consideration is
+  given to the umask.
++ There is no "quiet" option since default behavior is to run silent.
+
+## Configuration
+
+
+### config.silent
+Example:
+
+```javascript
+var silentState = config.silent; // save old silent state
+config.silent = true;
+/* ... */
+config.silent = silentState; // restore old silent state
+```
+
+Suppresses all command output if `true`, except for `echo()` calls.
+Default is `false`.
+
+### config.fatal
+Example:
+
+```javascript
+config.fatal = true;
+cp('this_file_does_not_exist', '/dev/null'); // dies here
+/* more commands... */
+```
+
+If `true` the script will die on errors. Default is `false`.
+
+## Non-Unix commands
+
+
+### tempdir()
+Searches and returns string containing a writeable, platform-dependent temporary directory.
+Follows Python's [tempfile algorithm](http://docs.python.org/library/tempfile.html#tempfile.tempdir).
+
+### error()
+Tests if error occurred in the last command. Returns `null` if no error occurred,
+otherwise returns string explaining the error
diff --git a/bin/node_modules/shelljs/bin/shjs b/bin/node_modules/shelljs/bin/shjs
new file mode 100755
index 0000000..d239a7a
--- /dev/null
+++ b/bin/node_modules/shelljs/bin/shjs
@@ -0,0 +1,51 @@
+#!/usr/bin/env node
+require('../global');
+
+if (process.argv.length < 3) {
+  console.log('ShellJS: missing argument (script name)');
+  console.log();
+  process.exit(1);
+}
+
+var args,
+  scriptName = process.argv[2];
+env['NODE_PATH'] = __dirname + '/../..';
+
+if (!scriptName.match(/\.js/) && !scriptName.match(/\.coffee/)) {
+  if (test('-f', scriptName + '.js'))
+    scriptName += '.js';
+  if (test('-f', scriptName + '.coffee'))
+    scriptName += '.coffee';
+}
+
+if (!test('-f', scriptName)) {
+  console.log('ShellJS: script not found ('+scriptName+')');
+  console.log();
+  process.exit(1);
+}
+
+args = process.argv.slice(3);
+
+for (var i = 0, l = args.length; i < l; i++) {
+  if (args[i][0] !== "-"){
+    args[i] = '"' + args[i] + '"'; // fixes arguments with multiple words
+  }
+}
+
+if (scriptName.match(/\.coffee$/)) {
+  //
+  // CoffeeScript
+  //
+  if (which('coffee')) {
+    exec('coffee ' + scriptName + ' ' + args.join(' '), { async: true });
+  } else {
+    console.log('ShellJS: CoffeeScript interpreter not found');
+    console.log();
+    process.exit(1);
+  }
+} else {
+  //
+  // JavaScript
+  //
+  exec('node ' + scriptName + ' ' + args.join(' '), { async: true });
+}
diff --git a/bin/node_modules/shelljs/global.js b/bin/node_modules/shelljs/global.js
new file mode 100644
index 0000000..97f0033
--- /dev/null
+++ b/bin/node_modules/shelljs/global.js
@@ -0,0 +1,3 @@
+var shell = require('./shell.js');
+for (var cmd in shell)
+  global[cmd] = shell[cmd];
diff --git a/bin/node_modules/shelljs/jshint.json b/bin/node_modules/shelljs/jshint.json
new file mode 100644
index 0000000..205ed9c
--- /dev/null
+++ b/bin/node_modules/shelljs/jshint.json
@@ -0,0 +1,4 @@
+{
+  "loopfunc": true,
+  "sub": true
+}
\ No newline at end of file
diff --git a/bin/node_modules/shelljs/make.js b/bin/node_modules/shelljs/make.js
new file mode 100644
index 0000000..b636447
--- /dev/null
+++ b/bin/node_modules/shelljs/make.js
@@ -0,0 +1,48 @@
+require('./global');
+config.fatal = true;
+
+global.target = {};
+
+// This ensures we only execute the script targets after the entire script has
+// been evaluated
+var args = process.argv.slice(2);
+setTimeout(function() {
+  var t;
+
+  if (args.length === 1 && args[0] === '--help') {
+    console.log('Available targets:');
+    for (t in target)
+      console.log('  ' + t);
+    return;
+  }
+
+  // Wrap targets to prevent duplicate execution
+  for (t in target) {
+    (function(t, oldTarget){
+
+      // Wrap it
+      target[t] = function(force) {
+        if (oldTarget.done && !force)
+          return;
+        oldTarget.done = true;
+        return oldTarget.apply(oldTarget, arguments);
+      };
+
+    })(t, target[t]);
+  }
+
+  // Execute desired targets
+  if (args.length > 0) {
+    args.forEach(function(arg) {
+      if (arg in target)
+        target[arg]();
+      else {
+        console.log('no such target: ' + arg);
+        exit(1);
+      }
+    });
+  } else if ('all' in target) {
+    target.all();
+  }
+
+}, 0);
diff --git a/bin/node_modules/shelljs/package.json b/bin/node_modules/shelljs/package.json
new file mode 100644
index 0000000..6f545b9
--- /dev/null
+++ b/bin/node_modules/shelljs/package.json
@@ -0,0 +1,48 @@
+{
+  "name": "shelljs",
+  "version": "0.1.4",
+  "author": {
+    "name": "Artur Adib",
+    "email": "aadib@mozilla.com"
+  },
+  "description": "Portable Unix shell commands for Node.js",
+  "keywords": [
+    "unix",
+    "shell",
+    "makefile",
+    "make",
+    "jake",
+    "synchronous"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/arturadib/shelljs.git"
+  },
+  "homepage": "http://github.com/arturadib/shelljs",
+  "main": "./shell.js",
+  "scripts": {
+    "test": "node scripts/run-tests"
+  },
+  "bin": {
+    "shjs": "./bin/shjs"
+  },
+  "dependencies": {},
+  "devDependencies": {
+    "jshint": "~1.1.0"
+  },
+  "optionalDependencies": {},
+  "engines": {
+    "node": "*"
+  },
+  "readme": "# ShellJS - Unix shell commands for Node.js [![Build Status](https://secure.travis-ci.org/arturadib/shelljs.png)](http://travis-ci.org/arturadib/shelljs)\n\nShellJS is a portable **(Windows/Linux/OS X)** implementation of Unix shell commands on top of the Node.js API. You can use it to eliminate your shell script's dependency on Unix while still keeping its familiar and powerful commands. You can also install it globally so you can run it from outside Node projects - say goodbye to those gnarly Bash scripts!\n\nThe project is [unit-tested](http://travis-ci.org/arturadib/shelljs) and battled-tested in projects like:\n\n+ [PDF.js](http://github.com/mozilla/pdf.js) - Firefox's next-gen PDF reader\n+ [Firebug](http://getfirebug.com/) - Firefox's infamous debugger\n+ [JSHint](http://jshint.com) - Most popular JavaScript linter\n+ [Zepto](http://zeptojs.com) - jQuery-compatible JavaScript library for modern browsers\n+ [Yeoman](http://yeoman.io/) - Web application stack and development tool\n+ [Deployd.com](http://deployd.com) - Open source PaaS for quick API backend generation\n\nand [many more](https://npmjs.org/browse/depended/shelljs).\n\n## Installing\n\nVia npm:\n\n```bash\n$ npm install [-g] shelljs\n```\n\nIf the global option `-g` is specified, the binary `shjs` will be installed. This makes it possible to\nrun ShellJS scripts much like any shell script from the command line, i.e. without requiring a `node_modules` folder:\n\n```bash\n$ shjs my_script\n```\n\nYou can also just copy `shell.js` into your project's directory, and `require()` accordingly.\n\n\n## Examples\n\n### JavaScript\n\n```javascript\nrequire('shelljs/global');\n\nif (!which('git')) {\n  echo('Sorry, this script requires git');\n  exit(1);\n}\n\n// Copy files to release dir\nmkdir('-p', 'out/Release');\ncp('-R', 'stuff/*', 'out/Release');\n\n// Replace macros in each .js file\ncd('lib');\nls('*.js').forEach(function(file) {\n  sed('-i', 'BUILD_VERSION', 'v0.1.2', file);\n  sed('-i', /.*REMOVE_THIS_LINE.*\\n/, '', file);\n  sed('-i', /.*REPLACE_LINE_WITH_MACRO.*\\n/, cat('macro.js'), file);\n});\ncd('..');\n\n// Run external tool synchronously\nif (exec('git commit -am \"Auto-commit\"').code !== 0) {\n  echo('Error: Git commit failed');\n  exit(1);\n}\n```\n\n### CoffeeScript\n\n```coffeescript\nrequire 'shelljs/global'\n\nif not which 'git'\n  echo 'Sorry, this script requires git'\n  exit 1\n\n# Copy files to release dir\nmkdir '-p', 'out/Release'\ncp '-R', 'stuff/*', 'out/Release'\n\n# Replace macros in each .js file\ncd 'lib'\nfor file in ls '*.js'\n  sed '-i', 'BUILD_VERSION', 'v0.1.2', file\n  sed '-i', /.*REMOVE_THIS_LINE.*\\n/, '', file\n  sed '-i', /.*REPLACE_LINE_WITH_MACRO.*\\n/, cat 'macro.js', file\ncd '..'\n\n# Run external tool synchronously\nif (exec 'git commit -am \"Auto-commit\"').code != 0\n  echo 'Error: Git commit failed'\n  exit 1\n```\n\n## Global vs. Local\n\nThe example above uses the convenience script `shelljs/global` to reduce verbosity. If polluting your global namespace is not desirable, simply require `shelljs`.\n\nExample:\n\n```javascript\nvar shell = require('shelljs');\nshell.echo('hello world');\n```\n\n## Make tool\n\nA convenience script `shelljs/make` is also provided to mimic the behavior of a Unix Makefile. In this case all shell objects are global, and command line arguments will cause the script to execute only the corresponding function in the global `target` object. To avoid redundant calls, target functions are executed only once per script.\n\nExample (CoffeeScript):\n\n```coffeescript\nrequire 'shelljs/make'\n\ntarget.all = ->\n  target.bundle()\n  target.docs()\n\ntarget.bundle = ->\n  cd __dirname\n  mkdir 'build'\n  cd 'lib'\n  (cat '*.js').to '../build/output.js'\n\ntarget.docs = ->\n  cd __dirname\n  mkdir 'docs'\n  cd 'lib'\n  for file in ls '*.js'\n    text = grep '//@', file     # extract special comments\n    text.replace '//@', ''      # remove comment tags\n    text.to 'docs/my_docs.md'\n```\n\nTo run the target `all`, call the above script without arguments: `$ node make`. To run the target `docs`: `$ node make docs`, and so on.\n\n\n\n<!-- \n\n  DO NOT MODIFY BEYOND THIS POINT - IT'S AUTOMATICALLY GENERATED\n\n-->\n\n\n## Command reference\n\n\nAll commands run synchronously, unless otherwise stated.\n\n\n### cd('dir')\nChanges to directory `dir` for the duration of the script\n\n### pwd()\nReturns the current directory.\n\n### ls([options ,] path [,path ...])\n### ls([options ,] path_array)\nAvailable options:\n\n+ `-R`: recursive\n+ `-A`: all files (include files beginning with `.`, except for `.` and `..`)\n\nExamples:\n\n```javascript\nls('projs/*.js');\nls('-R', '/users/me', '/tmp');\nls('-R', ['/users/me', '/tmp']); // same as above\n```\n\nReturns array of files in the given path, or in current directory if no path provided.\n\n### find(path [,path ...])\n### find(path_array)\nExamples:\n\n```javascript\nfind('src', 'lib');\nfind(['src', 'lib']); // same as above\nfind('.').filter(function(file) { return file.match(/\\.js$/); });\n```\n\nReturns array of all files (however deep) in the given paths.\n\nThe main difference from `ls('-R', path)` is that the resulting file names\ninclude the base directories, e.g. `lib/resources/file1` instead of just `file1`.\n\n### cp([options ,] source [,source ...], dest)\n### cp([options ,] source_array, dest)\nAvailable options:\n\n+ `-f`: force\n+ `-r, -R`: recursive\n\nExamples:\n\n```javascript\ncp('file1', 'dir1');\ncp('-Rf', '/tmp/*', '/usr/local/*', '/home/tmp');\ncp('-Rf', ['/tmp/*', '/usr/local/*'], '/home/tmp'); // same as above\n```\n\nCopies files. The wildcard `*` is accepted.\n\n### rm([options ,] file [, file ...])\n### rm([options ,] file_array)\nAvailable options:\n\n+ `-f`: force\n+ `-r, -R`: recursive\n\nExamples:\n\n```javascript\nrm('-rf', '/tmp/*');\nrm('some_file.txt', 'another_file.txt');\nrm(['some_file.txt', 'another_file.txt']); // same as above\n```\n\nRemoves files. The wildcard `*` is accepted.\n\n### mv(source [, source ...], dest')\n### mv(source_array, dest')\nAvailable options:\n\n+ `f`: force\n\nExamples:\n\n```javascript\nmv('-f', 'file', 'dir/');\nmv('file1', 'file2', 'dir/');\nmv(['file1', 'file2'], 'dir/'); // same as above\n```\n\nMoves files. The wildcard `*` is accepted.\n\n### mkdir([options ,] dir [, dir ...])\n### mkdir([options ,] dir_array)\nAvailable options:\n\n+ `p`: full path (will create intermediate dirs if necessary)\n\nExamples:\n\n```javascript\nmkdir('-p', '/tmp/a/b/c/d', '/tmp/e/f/g');\nmkdir('-p', ['/tmp/a/b/c/d', '/tmp/e/f/g']); // same as above\n```\n\nCreates directories.\n\n### test(expression)\nAvailable expression primaries:\n\n+ `'-b', 'path'`: true if path is a block device\n+ `'-c', 'path'`: true if path is a character device\n+ `'-d', 'path'`: true if path is a directory\n+ `'-e', 'path'`: true if path exists\n+ `'-f', 'path'`: true if path is a regular file\n+ `'-L', 'path'`: true if path is a symboilc link\n+ `'-p', 'path'`: true if path is a pipe (FIFO)\n+ `'-S', 'path'`: true if path is a socket\n\nExamples:\n\n```javascript\nif (test('-d', path)) { /* do something with dir */ };\nif (!test('-f', path)) continue; // skip if it's a regular file\n```\n\nEvaluates expression using the available primaries and returns corresponding value.\n\n### cat(file [, file ...])\n### cat(file_array)\n\nExamples:\n\n```javascript\nvar str = cat('file*.txt');\nvar str = cat('file1', 'file2');\nvar str = cat(['file1', 'file2']); // same as above\n```\n\nReturns a string containing the given file, or a concatenated string\ncontaining the files if more than one file is given (a new line character is\nintroduced between each file). Wildcard `*` accepted.\n\n### 'string'.to(file)\n\nExamples:\n\n```javascript\ncat('input.txt').to('output.txt');\n```\n\nAnalogous to the redirection operator `>` in Unix, but works with JavaScript strings (such as\nthose returned by `cat`, `grep`, etc). _Like Unix redirections, `to()` will overwrite any existing file!_\n\n### sed([options ,] search_regex, replace_str, file)\nAvailable options:\n\n+ `-i`: Replace contents of 'file' in-place. _Note that no backups will be created!_\n\nExamples:\n\n```javascript\nsed('-i', 'PROGRAM_VERSION', 'v0.1.3', 'source.js');\nsed(/.*DELETE_THIS_LINE.*\\n/, '', 'source.js');\n```\n\nReads an input string from `file` and performs a JavaScript `replace()` on the input\nusing the given search regex and replacement string. Returns the new string after replacement.\n\n### grep([options ,] regex_filter, file [, file ...])\n### grep([options ,] regex_filter, file_array)\nAvailable options:\n\n+ `-v`: Inverse the sense of the regex and print the lines not matching the criteria.\n\nExamples:\n\n```javascript\ngrep('-v', 'GLOBAL_VARIABLE', '*.js');\ngrep('GLOBAL_VARIABLE', '*.js');\n```\n\nReads input string from given files and returns a string containing all lines of the\nfile that match the given `regex_filter`. Wildcard `*` accepted.\n\n### which(command)\n\nExamples:\n\n```javascript\nvar nodeExec = which('node');\n```\n\nSearches for `command` in the system's PATH. On Windows looks for `.exe`, `.cmd`, and `.bat` extensions.\nReturns string containing the absolute path to the command.\n\n### echo(string [,string ...])\n\nExamples:\n\n```javascript\necho('hello world');\nvar str = echo('hello world');\n```\n\nPrints string to stdout, and returns string with additional utility methods\nlike `.to()`.\n\n### dirs([options | '+N' | '-N'])\n\nAvailable options:\n\n+ `-c`: Clears the directory stack by deleting all of the elements.\n\nArguments:\n\n+ `+N`: Displays the Nth directory (counting from the left of the list printed by dirs when invoked without options), starting with zero.\n+ `-N`: Displays the Nth directory (counting from the right of the list printed by dirs when invoked without options), starting with zero.\n\nDisplay the list of currently remembered directories. Returns an array of paths in the stack, or a single path if +N or -N was specified.\n\nSee also: pushd, popd\n\n### pushd([options,] [dir | '-N' | '+N'])\n\nAvailable options:\n\n+ `-n`: Suppresses the normal change of directory when adding directories to the stack, so that only the stack is manipulated.\n\nArguments:\n\n+ `dir`: Makes the current working directory be the top of the stack, and then executes the equivalent of `cd dir`.\n+ `+N`: Brings the Nth directory (counting from the left of the list printed by dirs, starting with zero) to the top of the list by rotating the stack.\n+ `-N`: Brings the Nth directory (counting from the right of the list printed by dirs, starting with zero) to the top of the list by rotating the stack.\n\nExamples:\n\n```javascript\n// process.cwd() === '/usr'\npushd('/etc'); // Returns /etc /usr\npushd('+1');   // Returns /usr /etc\n```\n\nSave the current directory on the top of the directory stack and then cd to `dir`. With no arguments, pushd exchanges the top two directories. Returns an array of paths in the stack.\n\n### popd([options,] ['-N' | '+N'])\n\nAvailable options:\n\n+ `-n`: Suppresses the normal change of directory when removing directories from the stack, so that only the stack is manipulated.\n\nArguments:\n\n+ `+N`: Removes the Nth directory (counting from the left of the list printed by dirs), starting with zero.\n+ `-N`: Removes the Nth directory (counting from the right of the list printed by dirs), starting with zero.\n\nExamples:\n\n```javascript\necho(process.cwd()); // '/usr'\npushd('/etc');       // '/etc /usr'\necho(process.cwd()); // '/etc'\npopd();              // '/usr'\necho(process.cwd()); // '/usr'\n```\n\nWhen no arguments are given, popd removes the top directory from the stack and performs a cd to the new top directory. The elements are numbered from 0 starting at the first directory listed with dirs; i.e., popd is equivalent to popd +0. Returns an array of paths in the stack.\n\n### exit(code)\nExits the current process with the given exit code.\n\n### env['VAR_NAME']\nObject containing environment variables (both getter and setter). Shortcut to process.env.\n\n### exec(command [, options] [, callback])\nAvailable options (all `false` by default):\n\n+ `async`: Asynchronous execution. Defaults to true if a callback is provided.\n+ `silent`: Do not echo program output to console.\n\nExamples:\n\n```javascript\nvar version = exec('node --version', {silent:true}).output;\n\nvar child = exec('some_long_running_process', {async:true});\nchild.stdout.on('data', function(data) {\n  /* ... do something with data ... */\n});\n\nexec('some_long_running_process', function(code, output) {\n  console.log('Exit code:', code);\n  console.log('Program output:', output);\n});\n```\n\nExecutes the given `command` _synchronously_, unless otherwise specified.\nWhen in synchronous mode returns the object `{ code:..., output:... }`, containing the program's\n`output` (stdout + stderr)  and its exit `code`. Otherwise returns the child process object, and\nthe `callback` gets the arguments `(code, output)`.\n\n**Note:** For long-lived processes, it's best to run `exec()` asynchronously as\nthe current synchronous implementation uses a lot of CPU. This should be getting\nfixed soon.\n\n### chmod(octal_mode || octal_string, file)\n### chmod(symbolic_mode, file)\n\nAvailable options:\n\n+ `-v`: output a diagnostic for every file processed\n+ `-c`: like verbose but report only when a change is made\n+ `-R`: change files and directories recursively\n\nExamples:\n\n```javascript\nchmod(755, '/Users/brandon');\nchmod('755', '/Users/brandon'); // same as above \nchmod('u+x', '/Users/brandon');\n```\n\nAlters the permissions of a file or directory by either specifying the\nabsolute permissions in octal form or expressing the changes in symbols.\nThis command tries to mimic the POSIX behavior as much as possible.\nNotable exceptions:\n\n+ In symbolic modes, 'a-r' and '-r' are identical.  No consideration is\n  given to the umask.\n+ There is no \"quiet\" option since default behavior is to run silent.\n\n## Configuration\n\n\n### config.silent\nExample:\n\n```javascript\nvar silentState = config.silent; // save old silent state\nconfig.silent = true;\n/* ... */\nconfig.silent = silentState; // restore old silent state\n```\n\nSuppresses all command output if `true`, except for `echo()` calls.\nDefault is `false`.\n\n### config.fatal\nExample:\n\n```javascript\nconfig.fatal = true;\ncp('this_file_does_not_exist', '/dev/null'); // dies here\n/* more commands... */\n```\n\nIf `true` the script will die on errors. Default is `false`.\n\n## Non-Unix commands\n\n\n### tempdir()\nSearches and returns string containing a writeable, platform-dependent temporary directory.\nFollows Python's [tempfile algorithm](http://docs.python.org/library/tempfile.html#tempfile.tempdir).\n\n### error()\nTests if error occurred in the last command. Returns `null` if no error occurred,\notherwise returns string explaining the error\n",
+  "readmeFilename": "README.md",
+  "bugs": {
+    "url": "https://github.com/arturadib/shelljs/issues"
+  },
+  "_id": "shelljs@0.1.4",
+  "dist": {
+    "shasum": "7a8aeaa3dc3c0be2e59d83168e83b4c4bc4dac95"
+  },
+  "_from": "shelljs@0.1.4",
+  "_resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.1.4.tgz"
+}
diff --git a/bin/node_modules/shelljs/scripts/docs.js b/bin/node_modules/shelljs/scripts/docs.js
new file mode 100755
index 0000000..68a2138
--- /dev/null
+++ b/bin/node_modules/shelljs/scripts/docs.js
@@ -0,0 +1,15 @@
+#!/usr/bin/env node
+require('../global');
+
+echo('Appending docs to README.md');
+
+cd(__dirname + '/..');
+
+// Extract docs from shell.js
+var docs = grep('//@', 'shell.js');
+// Remove '//@'
+docs = docs.replace(/\/\/\@ ?/g, '');
+// Append docs to README
+sed('-i', /## Command reference(.|\n)*/, '## Command reference\n\n' + docs, 'README.md');
+
+echo('All done.');
diff --git a/bin/node_modules/shelljs/scripts/run-tests.js b/bin/node_modules/shelljs/scripts/run-tests.js
new file mode 100755
index 0000000..a9d32fc
--- /dev/null
+++ b/bin/node_modules/shelljs/scripts/run-tests.js
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+require('../global');
+
+var path = require('path');
+
+var failed = false;
+
+//
+// Lint
+//
+JSHINT_BIN = './node_modules/jshint/bin/jshint';
+cd(__dirname + '/..');
+
+if (!test('-f', JSHINT_BIN)) {
+  echo('JSHint not found. Run `npm install` in the root dir first.');
+  exit(1);
+}
+
+if (exec(JSHINT_BIN + ' --config jshint.json *.js test/*.js').code !== 0) {
+  failed = true;
+  echo('*** JSHINT FAILED! (return code != 0)');
+  echo();
+} else {
+  echo('All JSHint tests passed');
+  echo();
+}
+
+//
+// Unit tests
+//
+cd(__dirname + '/../test');
+ls('*.js').forEach(function(file) {
+  echo('Running test:', file);
+  if (exec('node ' + file).code !== 123) { // 123 avoids false positives (e.g. premature exit)
+    failed = true;
+    echo('*** TEST FAILED! (missing exit code "123")');
+    echo();
+  }
+});
+
+if (failed) {
+  echo();
+  echo('*******************************************************');
+  echo('WARNING: Some tests did not pass!');
+  echo('*******************************************************');
+  exit(1);
+} else {
+  echo();
+  echo('All tests passed.');
+}
diff --git a/bin/node_modules/shelljs/shell.js b/bin/node_modules/shelljs/shell.js
new file mode 100644
index 0000000..7a4f4c8
--- /dev/null
+++ b/bin/node_modules/shelljs/shell.js
@@ -0,0 +1,1901 @@
+//
+// ShellJS
+// Unix shell commands on top of Node's API
+//
+// Copyright (c) 2012 Artur Adib
+// http://github.com/arturadib/shelljs
+//
+
+var fs = require('fs'),
+    path = require('path'),
+    util = require('util'),
+    vm = require('vm'),
+    child = require('child_process'),
+    os = require('os');
+
+// Node shims for < v0.7
+fs.existsSync = fs.existsSync || path.existsSync;
+
+var config = {
+  silent: false,
+  fatal: false
+};
+
+var state = {
+      error: null,
+      currentCmd: 'shell.js',
+      tempDir: null
+    },
+    platform = os.type().match(/^Win/) ? 'win' : 'unix';
+
+
+//@
+//@ All commands run synchronously, unless otherwise stated.
+//@
+
+
+//@
+//@ ### cd('dir')
+//@ Changes to directory `dir` for the duration of the script
+function _cd(options, dir) {
+  if (!dir)
+    error('directory not specified');
+
+  if (!fs.existsSync(dir))
+    error('no such file or directory: ' + dir);
+
+  if (!fs.statSync(dir).isDirectory())
+    error('not a directory: ' + dir);
+
+  process.chdir(dir);
+}
+exports.cd = wrap('cd', _cd);
+
+//@
+//@ ### pwd()
+//@ Returns the current directory.
+function _pwd(options) {
+  var pwd = path.resolve(process.cwd());
+  return ShellString(pwd);
+}
+exports.pwd = wrap('pwd', _pwd);
+
+
+//@
+//@ ### ls([options ,] path [,path ...])
+//@ ### ls([options ,] path_array)
+//@ Available options:
+//@
+//@ + `-R`: recursive
+//@ + `-A`: all files (include files beginning with `.`, except for `.` and `..`)
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ ls('projs/*.js');
+//@ ls('-R', '/users/me', '/tmp');
+//@ ls('-R', ['/users/me', '/tmp']); // same as above
+//@ ```
+//@
+//@ Returns array of files in the given path, or in current directory if no path provided.
+function _ls(options, paths) {
+  options = parseOptions(options, {
+    'R': 'recursive',
+    'A': 'all',
+    'a': 'all_deprecated'
+  });
+
+  if (options.all_deprecated) {
+    // We won't support the -a option as it's hard to image why it's useful
+    // (it includes '.' and '..' in addition to '.*' files)
+    // For backwards compatibility we'll dump a deprecated message and proceed as before
+    log('ls: Option -a is deprecated. Use -A instead');
+    options.all = true;
+  }
+
+  if (!paths)
+    paths = ['.'];
+  else if (typeof paths === 'object')
+    paths = paths; // assume array
+  else if (typeof paths === 'string')
+    paths = [].slice.call(arguments, 1);
+
+  var list = [];
+
+  // Conditionally pushes file to list - returns true if pushed, false otherwise
+  // (e.g. prevents hidden files to be included unless explicitly told so)
+  function pushFile(file, query) {
+    // hidden file?
+    if (path.basename(file)[0] === '.') {
+      // not explicitly asking for hidden files?
+      if (!options.all && !(path.basename(query)[0] === '.' && path.basename(query).length > 1))
+        return false;
+    }
+
+    if (platform === 'win')
+      file = file.replace(/\\/g, '/');
+
+    list.push(file);
+    return true;
+  }
+
+  paths.forEach(function(p) {
+    if (fs.existsSync(p)) {
+      var stats = fs.statSync(p);
+      // Simple file?
+      if (stats.isFile()) {
+        pushFile(p, p);
+        return; // continue
+      }
+
+      // Simple dir?
+      if (stats.isDirectory()) {
+        // Iterate over p contents
+        fs.readdirSync(p).forEach(function(file) {
+          if (!pushFile(file, p))
+            return;
+
+          // Recursive?
+          if (options.recursive) {
+            var oldDir = _pwd();
+            _cd('', p);
+            if (fs.statSync(file).isDirectory())
+              list = list.concat(_ls('-R'+(options.all?'A':''), file+'/*'));
+            _cd('', oldDir);
+          }
+        });
+        return; // continue
+      }
+    }
+
+    // p does not exist - possible wildcard present
+
+    var basename = path.basename(p);
+    var dirname = path.dirname(p);
+    // Wildcard present on an existing dir? (e.g. '/tmp/*.js')
+    if (basename.search(/\*/) > -1 && fs.existsSync(dirname) && fs.statSync(dirname).isDirectory) {
+      // Escape special regular expression chars
+      var regexp = basename.replace(/(\^|\$|\(|\)|<|>|\[|\]|\{|\}|\.|\+|\?)/g, '\\$1');
+      // Translates wildcard into regex
+      regexp = '^' + regexp.replace(/\*/g, '.*') + '$';
+      // Iterate over directory contents
+      fs.readdirSync(dirname).forEach(function(file) {
+        if (file.match(new RegExp(regexp))) {
+          if (!pushFile(path.normalize(dirname+'/'+file), basename))
+            return;
+
+          // Recursive?
+          if (options.recursive) {
+            var pp = dirname + '/' + file;
+            if (fs.lstatSync(pp).isDirectory())
+              list = list.concat(_ls('-R'+(options.all?'A':''), pp+'/*'));
+          } // recursive
+        } // if file matches
+      }); // forEach
+      return;
+    }
+
+    error('no such file or directory: ' + p, true);
+  });
+
+  return list;
+}
+exports.ls = wrap('ls', _ls);
+
+
+//@
+//@ ### find(path [,path ...])
+//@ ### find(path_array)
+//@ Examples:
+//@
+//@ ```javascript
+//@ find('src', 'lib');
+//@ find(['src', 'lib']); // same as above
+//@ find('.').filter(function(file) { return file.match(/\.js$/); });
+//@ ```
+//@
+//@ Returns array of all files (however deep) in the given paths.
+//@
+//@ The main difference from `ls('-R', path)` is that the resulting file names
+//@ include the base directories, e.g. `lib/resources/file1` instead of just `file1`.
+function _find(options, paths) {
+  if (!paths)
+    error('no path specified');
+  else if (typeof paths === 'object')
+    paths = paths; // assume array
+  else if (typeof paths === 'string')
+    paths = [].slice.call(arguments, 1);
+
+  var list = [];
+
+  function pushFile(file) {
+    if (platform === 'win')
+      file = file.replace(/\\/g, '/');
+    list.push(file);
+  }
+
+  // why not simply do ls('-R', paths)? because the output wouldn't give the base dirs
+  // to get the base dir in the output, we need instead ls('-R', 'dir/*') for every directory
+
+  paths.forEach(function(file) {
+    pushFile(file);
+
+    if (fs.statSync(file).isDirectory()) {
+      _ls('-RA', file+'/*').forEach(function(subfile) {
+        pushFile(subfile);
+      });
+    }
+  });
+
+  return list;
+}
+exports.find = wrap('find', _find);
+
+
+//@
+//@ ### cp([options ,] source [,source ...], dest)
+//@ ### cp([options ,] source_array, dest)
+//@ Available options:
+//@
+//@ + `-f`: force
+//@ + `-r, -R`: recursive
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ cp('file1', 'dir1');
+//@ cp('-Rf', '/tmp/*', '/usr/local/*', '/home/tmp');
+//@ cp('-Rf', ['/tmp/*', '/usr/local/*'], '/home/tmp'); // same as above
+//@ ```
+//@
+//@ Copies files. The wildcard `*` is accepted.
+function _cp(options, sources, dest) {
+  options = parseOptions(options, {
+    'f': 'force',
+    'R': 'recursive',
+    'r': 'recursive'
+  });
+
+  // Get sources, dest
+  if (arguments.length < 3) {
+    error('missing <source> and/or <dest>');
+  } else if (arguments.length > 3) {
+    sources = [].slice.call(arguments, 1, arguments.length - 1);
+    dest = arguments[arguments.length - 1];
+  } else if (typeof sources === 'string') {
+    sources = [sources];
+  } else if ('length' in sources) {
+    sources = sources; // no-op for array
+  } else {
+    error('invalid arguments');
+  }
+
+  var exists = fs.existsSync(dest),
+      stats = exists && fs.statSync(dest);
+
+  // Dest is not existing dir, but multiple sources given
+  if ((!exists || !stats.isDirectory()) && sources.length > 1)
+    error('dest is not a directory (too many sources)');
+
+  // Dest is an existing file, but no -f given
+  if (exists && stats.isFile() && !options.force)
+    error('dest file already exists: ' + dest);
+
+  if (options.recursive) {
+    // Recursive allows the shortcut syntax "sourcedir/" for "sourcedir/*"
+    // (see Github issue #15)
+    sources.forEach(function(src, i) {
+      if (src[src.length - 1] === '/')
+        sources[i] += '*';
+    });
+
+    // Create dest
+    try {
+      fs.mkdirSync(dest, parseInt('0777', 8));
+    } catch (e) {
+      // like Unix's cp, keep going even if we can't create dest dir
+    }
+  }
+
+  sources = expand(sources);
+
+  sources.forEach(function(src) {
+    if (!fs.existsSync(src)) {
+      error('no such file or directory: '+src, true);
+      return; // skip file
+    }
+
+    // If here, src exists
+    if (fs.statSync(src).isDirectory()) {
+      if (!options.recursive) {
+        // Non-Recursive
+        log(src + ' is a directory (not copied)');
+      } else {
+        // Recursive
+        // 'cp /a/source dest' should create 'source' in 'dest'
+        var newDest = path.join(dest, path.basename(src)),
+            checkDir = fs.statSync(src);
+        try {
+          fs.mkdirSync(newDest, checkDir.mode);
+        } catch (e) {
+          //if the directory already exists, that's okay
+          if (e.code !== 'EEXIST') throw e;
+        }
+
+        cpdirSyncRecursive(src, newDest, {force: options.force});
+      }
+      return; // done with dir
+    }
+
+    // If here, src is a file
+
+    // When copying to '/path/dir':
+    //    thisDest = '/path/dir/file1'
+    var thisDest = dest;
+    if (fs.existsSync(dest) && fs.statSync(dest).isDirectory())
+      thisDest = path.normalize(dest + '/' + path.basename(src));
+
+    if (fs.existsSync(thisDest) && !options.force) {
+      error('dest file already exists: ' + thisDest, true);
+      return; // skip file
+    }
+
+    copyFileSync(src, thisDest);
+  }); // forEach(src)
+}
+exports.cp = wrap('cp', _cp);
+
+//@
+//@ ### rm([options ,] file [, file ...])
+//@ ### rm([options ,] file_array)
+//@ Available options:
+//@
+//@ + `-f`: force
+//@ + `-r, -R`: recursive
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ rm('-rf', '/tmp/*');
+//@ rm('some_file.txt', 'another_file.txt');
+//@ rm(['some_file.txt', 'another_file.txt']); // same as above
+//@ ```
+//@
+//@ Removes files. The wildcard `*` is accepted.
+function _rm(options, files) {
+  options = parseOptions(options, {
+    'f': 'force',
+    'r': 'recursive',
+    'R': 'recursive'
+  });
+  if (!files)
+    error('no paths given');
+
+  if (typeof files === 'string')
+    files = [].slice.call(arguments, 1);
+  // if it's array leave it as it is
+
+  files = expand(files);
+
+  files.forEach(function(file) {
+    if (!fs.existsSync(file)) {
+      // Path does not exist, no force flag given
+      if (!options.force)
+        error('no such file or directory: '+file, true);
+
+      return; // skip file
+    }
+
+    // If here, path exists
+
+    var stats = fs.statSync(file);
+    // Remove simple file
+    if (stats.isFile()) {
+
+      // Do not check for file writing permissions
+      if (options.force) {
+        _unlinkSync(file);
+        return;
+      }
+
+      if (isWriteable(file))
+        _unlinkSync(file);
+      else
+        error('permission denied: '+file, true);
+
+      return;
+    } // simple file
+
+    // Path is an existing directory, but no -r flag given
+    if (stats.isDirectory() && !options.recursive) {
+      error('path is a directory', true);
+      return; // skip path
+    }
+
+    // Recursively remove existing directory
+    if (stats.isDirectory() && options.recursive) {
+      rmdirSyncRecursive(file, options.force);
+    }
+  }); // forEach(file)
+} // rm
+exports.rm = wrap('rm', _rm);
+
+//@
+//@ ### mv(source [, source ...], dest')
+//@ ### mv(source_array, dest')
+//@ Available options:
+//@
+//@ + `f`: force
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ mv('-f', 'file', 'dir/');
+//@ mv('file1', 'file2', 'dir/');
+//@ mv(['file1', 'file2'], 'dir/'); // same as above
+//@ ```
+//@
+//@ Moves files. The wildcard `*` is accepted.
+function _mv(options, sources, dest) {
+  options = parseOptions(options, {
+    'f': 'force'
+  });
+
+  // Get sources, dest
+  if (arguments.length < 3) {
+    error('missing <source> and/or <dest>');
+  } else if (arguments.length > 3) {
+    sources = [].slice.call(arguments, 1, arguments.length - 1);
+    dest = arguments[arguments.length - 1];
+  } else if (typeof sources === 'string') {
+    sources = [sources];
+  } else if ('length' in sources) {
+    sources = sources; // no-op for array
+  } else {
+    error('invalid arguments');
+  }
+
+  sources = expand(sources);
+
+  var exists = fs.existsSync(dest),
+      stats = exists && fs.statSync(dest);
+
+  // Dest is not existing dir, but multiple sources given
+  if ((!exists || !stats.isDirectory()) && sources.length > 1)
+    error('dest is not a directory (too many sources)');
+
+  // Dest is an existing file, but no -f given
+  if (exists && stats.isFile() && !options.force)
+    error('dest file already exists: ' + dest);
+
+  sources.forEach(function(src) {
+    if (!fs.existsSync(src)) {
+      error('no such file or directory: '+src, true);
+      return; // skip file
+    }
+
+    // If here, src exists
+
+    // When copying to '/path/dir':
+    //    thisDest = '/path/dir/file1'
+    var thisDest = dest;
+    if (fs.existsSync(dest) && fs.statSync(dest).isDirectory())
+      thisDest = path.normalize(dest + '/' + path.basename(src));
+
+    if (fs.existsSync(thisDest) && !options.force) {
+      error('dest file already exists: ' + thisDest, true);
+      return; // skip file
+    }
+
+    if (path.resolve(src) === path.dirname(path.resolve(thisDest))) {
+      error('cannot move to self: '+src, true);
+      return; // skip file
+    }
+
+    fs.renameSync(src, thisDest);
+  }); // forEach(src)
+} // mv
+exports.mv = wrap('mv', _mv);
+
+//@
+//@ ### mkdir([options ,] dir [, dir ...])
+//@ ### mkdir([options ,] dir_array)
+//@ Available options:
+//@
+//@ + `p`: full path (will create intermediate dirs if necessary)
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ mkdir('-p', '/tmp/a/b/c/d', '/tmp/e/f/g');
+//@ mkdir('-p', ['/tmp/a/b/c/d', '/tmp/e/f/g']); // same as above
+//@ ```
+//@
+//@ Creates directories.
+function _mkdir(options, dirs) {
+  options = parseOptions(options, {
+    'p': 'fullpath'
+  });
+  if (!dirs)
+    error('no paths given');
+
+  if (typeof dirs === 'string')
+    dirs = [].slice.call(arguments, 1);
+  // if it's array leave it as it is
+
+  dirs.forEach(function(dir) {
+    if (fs.existsSync(dir)) {
+      if (!options.fullpath)
+          error('path already exists: ' + dir, true);
+      return; // skip dir
+    }
+
+    // Base dir does not exist, and no -p option given
+    var baseDir = path.dirname(dir);
+    if (!fs.existsSync(baseDir) && !options.fullpath) {
+      error('no such file or directory: ' + baseDir, true);
+      return; // skip dir
+    }
+
+    if (options.fullpath)
+      mkdirSyncRecursive(dir);
+    else
+      fs.mkdirSync(dir, parseInt('0777', 8));
+  });
+} // mkdir
+exports.mkdir = wrap('mkdir', _mkdir);
+
+//@
+//@ ### test(expression)
+//@ Available expression primaries:
+//@
+//@ + `'-b', 'path'`: true if path is a block device
+//@ + `'-c', 'path'`: true if path is a character device
+//@ + `'-d', 'path'`: true if path is a directory
+//@ + `'-e', 'path'`: true if path exists
+//@ + `'-f', 'path'`: true if path is a regular file
+//@ + `'-L', 'path'`: true if path is a symboilc link
+//@ + `'-p', 'path'`: true if path is a pipe (FIFO)
+//@ + `'-S', 'path'`: true if path is a socket
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ if (test('-d', path)) { /* do something with dir */ };
+//@ if (!test('-f', path)) continue; // skip if it's a regular file
+//@ ```
+//@
+//@ Evaluates expression using the available primaries and returns corresponding value.
+function _test(options, path) {
+  if (!path)
+    error('no path given');
+
+  // hack - only works with unary primaries
+  options = parseOptions(options, {
+    'b': 'block',
+    'c': 'character',
+    'd': 'directory',
+    'e': 'exists',
+    'f': 'file',
+    'L': 'link',
+    'p': 'pipe',
+    'S': 'socket'
+  });
+
+  var canInterpret = false;
+  for (var key in options)
+    if (options[key] === true) {
+      canInterpret = true;
+      break;
+    }
+
+  if (!canInterpret)
+    error('could not interpret expression');
+
+  if (options.link) {
+    try {
+      return fs.lstatSync(path).isSymbolicLink();
+    } catch(e) {
+      return false;
+    }
+  }
+
+  if (!fs.existsSync(path))
+    return false;
+
+  if (options.exists)
+    return true;
+
+  var stats = fs.statSync(path);
+
+  if (options.block)
+    return stats.isBlockDevice();
+
+  if (options.character)
+    return stats.isCharacterDevice();
+
+  if (options.directory)
+    return stats.isDirectory();
+
+  if (options.file)
+    return stats.isFile();
+
+  if (options.pipe)
+    return stats.isFIFO();
+
+  if (options.socket)
+    return stats.isSocket();
+} // test
+exports.test = wrap('test', _test);
+
+
+//@
+//@ ### cat(file [, file ...])
+//@ ### cat(file_array)
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ var str = cat('file*.txt');
+//@ var str = cat('file1', 'file2');
+//@ var str = cat(['file1', 'file2']); // same as above
+//@ ```
+//@
+//@ Returns a string containing the given file, or a concatenated string
+//@ containing the files if more than one file is given (a new line character is
+//@ introduced between each file). Wildcard `*` accepted.
+function _cat(options, files) {
+  var cat = '';
+
+  if (!files)
+    error('no paths given');
+
+  if (typeof files === 'string')
+    files = [].slice.call(arguments, 1);
+  // if it's array leave it as it is
+
+  files = expand(files);
+
+  files.forEach(function(file) {
+    if (!fs.existsSync(file))
+      error('no such file or directory: ' + file);
+
+    cat += fs.readFileSync(file, 'utf8') + '\n';
+  });
+
+  if (cat[cat.length-1] === '\n')
+    cat = cat.substring(0, cat.length-1);
+
+  return ShellString(cat);
+}
+exports.cat = wrap('cat', _cat);
+
+//@
+//@ ### 'string'.to(file)
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ cat('input.txt').to('output.txt');
+//@ ```
+//@
+//@ Analogous to the redirection operator `>` in Unix, but works with JavaScript strings (such as
+//@ those returned by `cat`, `grep`, etc). _Like Unix redirections, `to()` will overwrite any existing file!_
+function _to(options, file) {
+  if (!file)
+    error('wrong arguments');
+
+  if (!fs.existsSync( path.dirname(file) ))
+      error('no such file or directory: ' + path.dirname(file));
+
+  try {
+    fs.writeFileSync(file, this.toString(), 'utf8');
+  } catch(e) {
+    error('could not write to file (code '+e.code+'): '+file, true);
+  }
+}
+// In the future, when Proxies are default, we can add methods like `.to()` to primitive strings.
+// For now, this is a dummy function to bookmark places we need such strings
+function ShellString(str) {
+  return str;
+}
+String.prototype.to = wrap('to', _to);
+
+//@
+//@ ### sed([options ,] search_regex, replace_str, file)
+//@ Available options:
+//@
+//@ + `-i`: Replace contents of 'file' in-place. _Note that no backups will be created!_
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ sed('-i', 'PROGRAM_VERSION', 'v0.1.3', 'source.js');
+//@ sed(/.*DELETE_THIS_LINE.*\n/, '', 'source.js');
+//@ ```
+//@
+//@ Reads an input string from `file` and performs a JavaScript `replace()` on the input
+//@ using the given search regex and replacement string. Returns the new string after replacement.
+function _sed(options, regex, replacement, file) {
+  options = parseOptions(options, {
+    'i': 'inplace'
+  });
+
+  if (typeof replacement === 'string')
+    replacement = replacement; // no-op
+  else if (typeof replacement === 'number')
+    replacement = replacement.toString(); // fallback
+  else
+    error('invalid replacement string');
+
+  if (!file)
+    error('no file given');
+
+  if (!fs.existsSync(file))
+    error('no such file or directory: ' + file);
+
+  var result = fs.readFileSync(file, 'utf8').replace(regex, replacement);
+  if (options.inplace)
+    fs.writeFileSync(file, result, 'utf8');
+
+  return ShellString(result);
+}
+exports.sed = wrap('sed', _sed);
+
+//@
+//@ ### grep([options ,] regex_filter, file [, file ...])
+//@ ### grep([options ,] regex_filter, file_array)
+//@ Available options:
+//@
+//@ + `-v`: Inverse the sense of the regex and print the lines not matching the criteria.
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ grep('-v', 'GLOBAL_VARIABLE', '*.js');
+//@ grep('GLOBAL_VARIABLE', '*.js');
+//@ ```
+//@
+//@ Reads input string from given files and returns a string containing all lines of the
+//@ file that match the given `regex_filter`. Wildcard `*` accepted.
+function _grep(options, regex, files) {
+  options = parseOptions(options, {
+    'v': 'inverse'
+  });
+
+  if (!files)
+    error('no paths given');
+
+  if (typeof files === 'string')
+    files = [].slice.call(arguments, 2);
+  // if it's array leave it as it is
+
+  files = expand(files);
+
+  var grep = '';
+  files.forEach(function(file) {
+    if (!fs.existsSync(file)) {
+      error('no such file or directory: ' + file, true);
+      return;
+    }
+
+    var contents = fs.readFileSync(file, 'utf8'),
+        lines = contents.split(/\r*\n/);
+    lines.forEach(function(line) {
+      var matched = line.match(regex);
+      if ((options.inverse && !matched) || (!options.inverse && matched))
+        grep += line + '\n';
+    });
+  });
+
+  return ShellString(grep);
+}
+exports.grep = wrap('grep', _grep);
+
+
+//@
+//@ ### which(command)
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ var nodeExec = which('node');
+//@ ```
+//@
+//@ Searches for `command` in the system's PATH. On Windows looks for `.exe`, `.cmd`, and `.bat` extensions.
+//@ Returns string containing the absolute path to the command.
+function _which(options, cmd) {
+  if (!cmd)
+    error('must specify command');
+
+  var pathEnv = process.env.path || process.env.Path || process.env.PATH,
+      pathArray = splitPath(pathEnv),
+      where = null;
+
+  // No relative/absolute paths provided?
+  if (cmd.search(/\//) === -1) {
+    // Search for command in PATH
+    pathArray.forEach(function(dir) {
+      if (where)
+        return; // already found it
+
+      var attempt = path.resolve(dir + '/' + cmd);
+      if (fs.existsSync(attempt)) {
+        where = attempt;
+        return;
+      }
+
+      if (platform === 'win') {
+        var baseAttempt = attempt;
+        attempt = baseAttempt + '.exe';
+        if (fs.existsSync(attempt)) {
+          where = attempt;
+          return;
+        }
+        attempt = baseAttempt + '.cmd';
+        if (fs.existsSync(attempt)) {
+          where = attempt;
+          return;
+        }
+        attempt = baseAttempt + '.bat';
+        if (fs.existsSync(attempt)) {
+          where = attempt;
+          return;
+        }
+      } // if 'win'
+    });
+  }
+
+  // Command not found anywhere?
+  if (!fs.existsSync(cmd) && !where)
+    return null;
+
+  where = where || path.resolve(cmd);
+
+  return ShellString(where);
+}
+exports.which = wrap('which', _which);
+
+//@
+//@ ### echo(string [,string ...])
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ echo('hello world');
+//@ var str = echo('hello world');
+//@ ```
+//@
+//@ Prints string to stdout, and returns string with additional utility methods
+//@ like `.to()`.
+function _echo() {
+  var messages = [].slice.call(arguments, 0);
+  console.log.apply(this, messages);
+  return ShellString(messages.join(' '));
+}
+exports.echo = _echo; // don't wrap() as it could parse '-options'
+
+// Pushd/popd/dirs internals
+var _dirStack = [];
+
+function _isStackIndex(index) {
+  return (/^[\-+]\d+$/).test(index);
+}
+
+function _parseStackIndex(index) {
+  if (_isStackIndex(index)) {
+    if (Math.abs(index) < _dirStack.length + 1) { // +1 for pwd
+      return (/^-/).test(index) ? Number(index) - 1 : Number(index);
+    } else {
+      error(index + ': directory stack index out of range');
+    }
+  } else {
+    error(index + ': invalid number');
+  }
+}
+
+function _actualDirStack() {
+  return [process.cwd()].concat(_dirStack);
+}
+
+//@
+//@ ### dirs([options | '+N' | '-N'])
+//@
+//@ Available options:
+//@
+//@ + `-c`: Clears the directory stack by deleting all of the elements.
+//@
+//@ Arguments:
+//@
+//@ + `+N`: Displays the Nth directory (counting from the left of the list printed by dirs when invoked without options), starting with zero.
+//@ + `-N`: Displays the Nth directory (counting from the right of the list printed by dirs when invoked without options), starting with zero.
+//@
+//@ Display the list of currently remembered directories. Returns an array of paths in the stack, or a single path if +N or -N was specified.
+//@
+//@ See also: pushd, popd
+function _dirs(options, index) {
+  if (_isStackIndex(options)) {
+    index = options;
+    options = '';
+  }
+
+  options = parseOptions(options, {
+    'c' : 'clear'
+  });
+
+  if (options['clear']) {
+    _dirStack = [];
+    return _dirStack;
+  }
+
+  var stack = _actualDirStack();
+
+  if (index) {
+    index = _parseStackIndex(index);
+
+    if (index < 0) {
+      index = stack.length + index;
+    }
+
+    log(stack[index]);
+    return stack[index];
+  }
+
+  log(stack.join(' '));
+
+  return stack;
+}
+exports.dirs = wrap("dirs", _dirs);
+
+//@
+//@ ### pushd([options,] [dir | '-N' | '+N'])
+//@
+//@ Available options:
+//@
+//@ + `-n`: Suppresses the normal change of directory when adding directories to the stack, so that only the stack is manipulated.
+//@
+//@ Arguments:
+//@
+//@ + `dir`: Makes the current working directory be the top of the stack, and then executes the equivalent of `cd dir`.
+//@ + `+N`: Brings the Nth directory (counting from the left of the list printed by dirs, starting with zero) to the top of the list by rotating the stack.
+//@ + `-N`: Brings the Nth directory (counting from the right of the list printed by dirs, starting with zero) to the top of the list by rotating the stack.
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ // process.cwd() === '/usr'
+//@ pushd('/etc'); // Returns /etc /usr
+//@ pushd('+1');   // Returns /usr /etc
+//@ ```
+//@
+//@ Save the current directory on the top of the directory stack and then cd to `dir`. With no arguments, pushd exchanges the top two directories. Returns an array of paths in the stack.
+function _pushd(options, dir) {
+  if (_isStackIndex(options)) {
+    dir = options;
+    options = '';
+  }
+
+  options = parseOptions(options, {
+    'n' : 'no-cd'
+  });
+
+  var dirs = _actualDirStack();
+
+  if (dir === '+0') {
+    return dirs; // +0 is a noop
+  } else if (!dir) {
+    if (dirs.length > 1) {
+      dirs = dirs.splice(1, 1).concat(dirs);
+    } else {
+      return error('no other directory');
+    }
+  } else if (_isStackIndex(dir)) {
+    var n = _parseStackIndex(dir);
+    dirs = dirs.slice(n).concat(dirs.slice(0, n));
+  } else {
+    if (options['no-cd']) {
+      dirs.splice(1, 0, dir);
+    } else {
+      dirs.unshift(dir);
+    }
+  }
+
+  if (options['no-cd']) {
+    dirs = dirs.slice(1);
+  } else {
+    dir = path.resolve(dirs.shift());
+    _cd('', dir);
+  }
+
+  _dirStack = dirs;
+  return _dirs('');
+}
+exports.pushd = wrap('pushd', _pushd);
+
+//@
+//@ ### popd([options,] ['-N' | '+N'])
+//@
+//@ Available options:
+//@
+//@ + `-n`: Suppresses the normal change of directory when removing directories from the stack, so that only the stack is manipulated.
+//@
+//@ Arguments:
+//@
+//@ + `+N`: Removes the Nth directory (counting from the left of the list printed by dirs), starting with zero.
+//@ + `-N`: Removes the Nth directory (counting from the right of the list printed by dirs), starting with zero.
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ echo(process.cwd()); // '/usr'
+//@ pushd('/etc');       // '/etc /usr'
+//@ echo(process.cwd()); // '/etc'
+//@ popd();              // '/usr'
+//@ echo(process.cwd()); // '/usr'
+//@ ```
+//@
+//@ When no arguments are given, popd removes the top directory from the stack and performs a cd to the new top directory. The elements are numbered from 0 starting at the first directory listed with dirs; i.e., popd is equivalent to popd +0. Returns an array of paths in the stack.
+function _popd(options, index) {
+  if (_isStackIndex(options)) {
+    index = options;
+    options = '';
+  }
+
+  options = parseOptions(options, {
+    'n' : 'no-cd'
+  });
+
+  if (!_dirStack.length) {
+    return error('directory stack empty');
+  }
+
+  index = _parseStackIndex(index || '+0');
+
+  if (options['no-cd'] || index > 0 || _dirStack.length + index === 0) {
+    index = index > 0 ? index - 1 : index;
+    _dirStack.splice(index, 1);
+  } else {
+    var dir = path.resolve(_dirStack.shift());
+    _cd('', dir);
+  }
+
+  return _dirs('');
+}
+exports.popd = wrap("popd", _popd);
+
+//@
+//@ ### exit(code)
+//@ Exits the current process with the given exit code.
+exports.exit = process.exit;
+
+//@
+//@ ### env['VAR_NAME']
+//@ Object containing environment variables (both getter and setter). Shortcut to process.env.
+exports.env = process.env;
+
+//@
+//@ ### exec(command [, options] [, callback])
+//@ Available options (all `false` by default):
+//@
+//@ + `async`: Asynchronous execution. Defaults to true if a callback is provided.
+//@ + `silent`: Do not echo program output to console.
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ var version = exec('node --version', {silent:true}).output;
+//@
+//@ var child = exec('some_long_running_process', {async:true});
+//@ child.stdout.on('data', function(data) {
+//@   /* ... do something with data ... */
+//@ });
+//@
+//@ exec('some_long_running_process', function(code, output) {
+//@   console.log('Exit code:', code);
+//@   console.log('Program output:', output);
+//@ });
+//@ ```
+//@
+//@ Executes the given `command` _synchronously_, unless otherwise specified.
+//@ When in synchronous mode returns the object `{ code:..., output:... }`, containing the program's
+//@ `output` (stdout + stderr)  and its exit `code`. Otherwise returns the child process object, and
+//@ the `callback` gets the arguments `(code, output)`.
+//@
+//@ **Note:** For long-lived processes, it's best to run `exec()` asynchronously as
+//@ the current synchronous implementation uses a lot of CPU. This should be getting
+//@ fixed soon.
+function _exec(command, options, callback) {
+  if (!command)
+    error('must specify command');
+
+  // Callback is defined instead of options.
+  if (typeof options === 'function') {
+    callback = options;
+    options = { async: true };
+  }
+
+  // Callback is defined with options.
+  if (typeof options === 'object' && typeof callback === 'function') {
+    options.async = true;
+  }
+
+  options = extend({
+    silent: config.silent,
+    async: false
+  }, options);
+
+  if (options.async)
+    return execAsync(command, options, callback);
+  else
+    return execSync(command, options);
+}
+exports.exec = wrap('exec', _exec, {notUnix:true});
+
+var PERMS = (function (base) {
+  return {
+    OTHER_EXEC  : base.EXEC,
+    OTHER_WRITE : base.WRITE,
+    OTHER_READ  : base.READ,
+
+    GROUP_EXEC  : base.EXEC  << 3,
+    GROUP_WRITE : base.WRITE << 3,
+    GROUP_READ  : base.READ << 3,
+
+    OWNER_EXEC  : base.EXEC << 6,
+    OWNER_WRITE : base.WRITE << 6,
+    OWNER_READ  : base.READ << 6,
+
+    // Literal octal numbers are apparently not allowed in "strict" javascript.  Using parseInt is
+    // the preferred way, else a jshint warning is thrown.
+    STICKY      : parseInt('01000', 8),
+    SETGID      : parseInt('02000', 8),
+    SETUID      : parseInt('04000', 8),
+
+    TYPE_MASK   : parseInt('0770000', 8)
+  };
+})({
+  EXEC  : 1,
+  WRITE : 2,
+  READ  : 4
+});
+
+
+//@
+//@ ### chmod(octal_mode || octal_string, file)
+//@ ### chmod(symbolic_mode, file)
+//@
+//@ Available options:
+//@
+//@ + `-v`: output a diagnostic for every file processed//@
+//@ + `-c`: like verbose but report only when a change is made//@
+//@ + `-R`: change files and directories recursively//@
+//@
+//@ Examples:
+//@
+//@ ```javascript
+//@ chmod(755, '/Users/brandon');
+//@ chmod('755', '/Users/brandon'); // same as above
+//@ chmod('u+x', '/Users/brandon');
+//@ ```
+//@
+//@ Alters the permissions of a file or directory by either specifying the
+//@ absolute permissions in octal form or expressing the changes in symbols.
+//@ This command tries to mimic the POSIX behavior as much as possible.
+//@ Notable exceptions:
+//@
+//@ + In symbolic modes, 'a-r' and '-r' are identical.  No consideration is
+//@   given to the umask.
+//@ + There is no "quiet" option since default behavior is to run silent.
+function _chmod(options, mode, filePattern) {
+  if (!filePattern) {
+    if (options.length > 0 && options.charAt(0) === '-') {
+      // Special case where the specified file permissions started with - to subtract perms, which
+      // get picked up by the option parser as command flags.
+      // If we are down by one argument and options starts with -, shift everything over.
+      filePattern = mode;
+      mode = options;
+      options = '';
+    }
+    else {
+      error('You must specify a file.');
+    }
+  }
+
+  options = parseOptions(options, {
+    'R': 'recursive',
+    'c': 'changes',
+    'v': 'verbose'
+  });
+
+  if (typeof filePattern === 'string') {
+    filePattern = [ filePattern ];
+  }
+
+  var files;
+
+  if (options.recursive) {
+    files = [];
+    expand(filePattern).forEach(function addFile(expandedFile) {
+      var stat = fs.lstatSync(expandedFile);
+
+      if (!stat.isSymbolicLink()) {
+        files.push(expandedFile);
+
+        if (stat.isDirectory()) {  // intentionally does not follow symlinks.
+          fs.readdirSync(expandedFile).forEach(function (child) {
+            addFile(expandedFile + '/' + child);
+          });
+        }
+      }
+    });
+  }
+  else {
+    files = expand(filePattern);
+  }
+
+  files.forEach(function innerChmod(file) {
+    file = path.resolve(file);
+    if (!fs.existsSync(file)) {
+      error('File not found: ' + file);
+    }
+
+    // When recursing, don't follow symlinks.
+    if (options.recursive && fs.lstatSync(file).isSymbolicLink()) {
+      return;
+    }
+
+    var perms = fs.statSync(file).mode;
+    var type = perms & PERMS.TYPE_MASK;
+
+    var newPerms = perms;
+
+    if (isNaN(parseInt(mode, 8))) {
+      // parse options
+      mode.split(',').forEach(function (symbolicMode) {
+        /*jshint regexdash:true */
+        var pattern = /([ugoa]*)([=\+-])([rwxXst]*)/i;
+        var matches = pattern.exec(symbolicMode);
+
+        if (matches) {
+          var applyTo = matches[1];
+          var operator = matches[2];
+          var change = matches[3];
+
+          var changeOwner = applyTo.indexOf('u') != -1 || applyTo === 'a' || applyTo === '';
+          var changeGroup = applyTo.indexOf('g') != -1 || applyTo === 'a' || applyTo === '';
+          var changeOther = applyTo.indexOf('o') != -1 || applyTo === 'a' || applyTo === '';
+
+          var changeRead   = change.indexOf('r') != -1;
+          var changeWrite  = change.indexOf('w') != -1;
+          var changeExec   = change.indexOf('x') != -1;
+          var changeSticky = change.indexOf('t') != -1;
+          var changeSetuid = change.indexOf('s') != -1;
+
+          var mask = 0;
+          if (changeOwner) {
+            mask |= (changeRead ? PERMS.OWNER_READ : 0) + (changeWrite ? PERMS.OWNER_WRITE : 0) + (changeExec ? PERMS.OWNER_EXEC : 0) + (changeSetuid ? PERMS.SETUID : 0);
+          }
+          if (changeGroup) {
+            mask |= (changeRead ? PERMS.GROUP_READ : 0) + (changeWrite ? PERMS.GROUP_WRITE : 0) + (changeExec ? PERMS.GROUP_EXEC : 0) + (changeSetuid ? PERMS.SETGID : 0);
+          }
+          if (changeOther) {
+            mask |= (changeRead ? PERMS.OTHER_READ : 0) + (changeWrite ? PERMS.OTHER_WRITE : 0) + (changeExec ? PERMS.OTHER_EXEC : 0);
+          }
+
+          // Sticky bit is special - it's not tied to user, group or other.
+          if (changeSticky) {
+            mask |= PERMS.STICKY;
+          }
+
+          switch (operator) {
+            case '+':
+              newPerms |= mask;
+              break;
+
+            case '-':
+              newPerms &= ~mask;
+              break;
+
+            case '=':
+              newPerms = type + mask;
+
+              // According to POSIX, when using = to explicitly set the permissions, setuid and setgid can never be cleared.
+              if (fs.statSync(file).isDirectory()) {
+                newPerms |= (PERMS.SETUID + PERMS.SETGID) & perms;
+              }
+              break;
+          }
+
+          if (options.verbose) {
+            log(file + ' -> ' + newPerms.toString(8));
+          }
+
+          if (perms != newPerms) {
+            if (!options.verbose && options.changes) {
+              log(file + ' -> ' + newPerms.toString(8));
+            }
+            fs.chmodSync(file, newPerms);
+          }
+        }
+        else {
+          error('Invalid symbolic mode change: ' + symbolicMode);
+        }
+      });
+    }
+    else {
+      // they gave us a full number
+      newPerms = type + parseInt(mode, 8);
+
+      // POSIX rules are that setuid and setgid can only be added using numeric form, but not cleared.
+      if (fs.statSync(file).isDirectory()) {
+        newPerms |= (PERMS.SETUID + PERMS.SETGID) & perms;
+      }
+
+      fs.chmodSync(file, newPerms);
+    }
+  });
+}
+exports.chmod = wrap('chmod', _chmod);
+
+
+//@
+//@ ## Configuration
+//@
+
+
+
+exports.config = config;
+
+//@
+//@ ### config.silent
+//@ Example:
+//@
+//@ ```javascript
+//@ var silentState = config.silent; // save old silent state
+//@ config.silent = true;
+//@ /* ... */
+//@ config.silent = silentState; // restore old silent state
+//@ ```
+//@
+//@ Suppresses all command output if `true`, except for `echo()` calls.
+//@ Default is `false`.
+
+//@
+//@ ### config.fatal
+//@ Example:
+//@
+//@ ```javascript
+//@ config.fatal = true;
+//@ cp('this_file_does_not_exist', '/dev/null'); // dies here
+//@ /* more commands... */
+//@ ```
+//@
+//@ If `true` the script will die on errors. Default is `false`.
+
+
+
+
+//@
+//@ ## Non-Unix commands
+//@
+
+
+
+
+
+
+//@
+//@ ### tempdir()
+//@ Searches and returns string containing a writeable, platform-dependent temporary directory.
+//@ Follows Python's [tempfile algorithm](http://docs.python.org/library/tempfile.html#tempfile.tempdir).
+exports.tempdir = wrap('tempdir', tempDir);
+
+
+//@
+//@ ### error()
+//@ Tests if error occurred in the last command. Returns `null` if no error occurred,
+//@ otherwise returns string explaining the error
+exports.error = function() {
+  return state.error;
+};
+
+
+
+
+
+////////////////////////////////////////////////////////////////////////////////////////////////
+//
+// Auxiliary functions (internal use only)
+//
+
+function log() {
+  if (!config.silent)
+    console.log.apply(this, arguments);
+}
+
+function deprecate(what, msg) {
+  console.log('*** ShellJS.'+what+': This function is deprecated.', msg);
+}
+
+function write(msg) {
+  if (!config.silent)
+    process.stdout.write(msg);
+}
+
+// Shows error message. Throws unless _continue or config.fatal are true
+function error(msg, _continue) {
+  if (state.error === null)
+    state.error = '';
+  state.error += state.currentCmd + ': ' + msg + '\n';
+
+  log(state.error);
+
+  if (config.fatal)
+    process.exit(1);
+
+  if (!_continue)
+    throw '';
+}
+
+// Returns {'alice': true, 'bob': false} when passed:
+//   parseOptions('-a', {'a':'alice', 'b':'bob'});
+function parseOptions(str, map) {
+  if (!map)
+    error('parseOptions() internal error: no map given');
+
+  // All options are false by default
+  var options = {};
+  for (var letter in map)
+    options[map[letter]] = false;
+
+  if (!str)
+    return options; // defaults
+
+  if (typeof str !== 'string')
+    error('parseOptions() internal error: wrong str');
+
+  // e.g. match[1] = 'Rf' for str = '-Rf'
+  var match = str.match(/^\-(.+)/);
+  if (!match)
+    return options;
+
+  // e.g. chars = ['R', 'f']
+  var chars = match[1].split('');
+
+  chars.forEach(function(c) {
+    if (c in map)
+      options[map[c]] = true;
+    else
+      error('option not recognized: '+c);
+  });
+
+  return options;
+}
+
+// Common wrapper for all Unix-like commands
+function wrap(cmd, fn, options) {
+  return function() {
+    var retValue = null;
+
+    state.currentCmd = cmd;
+    state.error = null;
+
+    try {
+      var args = [].slice.call(arguments, 0);
+
+      if (options && options.notUnix) {
+        retValue = fn.apply(this, args);
+      } else {
+        if (args.length === 0 || typeof args[0] !== 'string' || args[0][0] !== '-')
+          args.unshift(''); // only add dummy option if '-option' not already present
+        retValue = fn.apply(this, args);
+      }
+    } catch (e) {
+      if (!state.error) {
+        // If state.error hasn't been set it's an error thrown by Node, not us - probably a bug...
+        console.log('shell.js: internal error');
+        console.log(e.stack || e);
+        process.exit(1);
+      }
+      if (config.fatal)
+        throw e;
+    }
+
+    state.currentCmd = 'shell.js';
+    return retValue;
+  };
+} // wrap
+
+// Buffered file copy, synchronous
+// (Using readFileSync() + writeFileSync() could easily cause a memory overflow
+//  with large files)
+function copyFileSync(srcFile, destFile) {
+  if (!fs.existsSync(srcFile))
+    error('copyFileSync: no such file or directory: ' + srcFile);
+
+  var BUF_LENGTH = 64*1024,
+      buf = new Buffer(BUF_LENGTH),
+      bytesRead = BUF_LENGTH,
+      pos = 0,
+      fdr = null,
+      fdw = null;
+
+  try {
+    fdr = fs.openSync(srcFile, 'r');
+  } catch(e) {
+    error('copyFileSync: could not read src file ('+srcFile+')');
+  }
+
+  try {
+    fdw = fs.openSync(destFile, 'w');
+  } catch(e) {
+    error('copyFileSync: could not write to dest file (code='+e.code+'):'+destFile);
+  }
+
+  while (bytesRead === BUF_LENGTH) {
+    bytesRead = fs.readSync(fdr, buf, 0, BUF_LENGTH, pos);
+    fs.writeSync(fdw, buf, 0, bytesRead);
+    pos += bytesRead;
+  }
+
+  fs.closeSync(fdr);
+  fs.closeSync(fdw);
+}
+
+// Recursively copies 'sourceDir' into 'destDir'
+// Adapted from https://github.com/ryanmcgrath/wrench-js
+//
+// Copyright (c) 2010 Ryan McGrath
+// Copyright (c) 2012 Artur Adib
+//
+// Licensed under the MIT License
+// http://www.opensource.org/licenses/mit-license.php
+function cpdirSyncRecursive(sourceDir, destDir, opts) {
+  if (!opts) opts = {};
+
+  /* Create the directory where all our junk is moving to; read the mode of the source directory and mirror it */
+  var checkDir = fs.statSync(sourceDir);
+  try {
+    fs.mkdirSync(destDir, checkDir.mode);
+  } catch (e) {
+    //if the directory already exists, that's okay
+    if (e.code !== 'EEXIST') throw e;
+  }
+
+  var files = fs.readdirSync(sourceDir);
+
+  for(var i = 0; i < files.length; i++) {
+    var currFile = fs.lstatSync(sourceDir + "/" + files[i]);
+
+    if (currFile.isDirectory()) {
+      /* recursion this thing right on back. */
+      cpdirSyncRecursive(sourceDir + "/" + files[i], destDir + "/" + files[i], opts);
+    } else if (currFile.isSymbolicLink()) {
+      var symlinkFull = fs.readlinkSync(sourceDir + "/" + files[i]);
+      fs.symlinkSync(symlinkFull, destDir + "/" + files[i]);
+    } else {
+      /* At this point, we've hit a file actually worth copying... so copy it on over. */
+      if (fs.existsSync(destDir + "/" + files[i]) && !opts.force) {
+        log('skipping existing file: ' + files[i]);
+      } else {
+        copyFileSync(sourceDir + "/" + files[i], destDir + "/" + files[i]);
+      }
+    }
+
+  } // for files
+} // cpdirSyncRecursive
+
+// Recursively removes 'dir'
+// Adapted from https://github.com/ryanmcgrath/wrench-js
+//
+// Copyright (c) 2010 Ryan McGrath
+// Copyright (c) 2012 Artur Adib
+//
+// Licensed under the MIT License
+// http://www.opensource.org/licenses/mit-license.php
+function rmdirSyncRecursive(dir, force) {
+  var files;
+
+  files = fs.readdirSync(dir);
+
+  // Loop through and delete everything in the sub-tree after checking it
+  for(var i = 0; i < files.length; i++) {
+    var file = dir + "/" + files[i],
+        currFile = fs.lstatSync(file);
+
+    if(currFile.isDirectory()) { // Recursive function back to the beginning
+      rmdirSyncRecursive(file, force);
+    }
+
+    else if(currFile.isSymbolicLink()) { // Unlink symlinks
+      if (force || isWriteable(file)) {
+        try {
+          _unlinkSync(file);
+        } catch (e) {
+          error('could not remove file (code '+e.code+'): ' + file, true);
+        }
+      }
+    }
+
+    else // Assume it's a file - perhaps a try/catch belongs here?
+      if (force || isWriteable(file)) {
+        try {
+          _unlinkSync(file);
+        } catch (e) {
+          error('could not remove file (code '+e.code+'): ' + file, true);
+        }
+      }
+  }
+
+  // Now that we know everything in the sub-tree has been deleted, we can delete the main directory.
+  // Huzzah for the shopkeep.
+
+  var result;
+  try {
+    result = fs.rmdirSync(dir);
+  } catch(e) {
+    error('could not remove directory (code '+e.code+'): ' + dir, true);
+  }
+
+  return result;
+} // rmdirSyncRecursive
+
+// Recursively creates 'dir'
+function mkdirSyncRecursive(dir) {
+  var baseDir = path.dirname(dir);
+
+  // Base dir exists, no recursion necessary
+  if (fs.existsSync(baseDir)) {
+    fs.mkdirSync(dir, parseInt('0777', 8));
+    return;
+  }
+
+  // Base dir does not exist, go recursive
+  mkdirSyncRecursive(baseDir);
+
+  // Base dir created, can create dir
+  fs.mkdirSync(dir, parseInt('0777', 8));
+}
+
+// e.g. 'shelljs_a5f185d0443ca...'
+function randomFileName() {
+  function randomHash(count) {
+    if (count === 1)
+      return parseInt(16*Math.random(), 10).toString(16);
+    else {
+      var hash = '';
+      for (var i=0; i<count; i++)
+        hash += randomHash(1);
+      return hash;
+    }
+  }
+
+  return 'shelljs_'+randomHash(20);
+}
+
+// Returns false if 'dir' is not a writeable directory, 'dir' otherwise
+function writeableDir(dir) {
+  if (!dir || !fs.existsSync(dir))
+    return false;
+
+  if (!fs.statSync(dir).isDirectory())
+    return false;
+
+  var testFile = dir+'/'+randomFileName();
+  try {
+    fs.writeFileSync(testFile, ' ');
+    _unlinkSync(testFile);
+    return dir;
+  } catch (e) {
+    return false;
+  }
+}
+
+// Cross-platform method for getting an available temporary directory.
+// Follows the algorithm of Python's tempfile.tempdir
+// http://docs.python.org/library/tempfile.html#tempfile.tempdir
+function tempDir() {
+  if (state.tempDir)
+    return state.tempDir; // from cache
+
+  state.tempDir = writeableDir(process.env['TMPDIR']) ||
+                  writeableDir(process.env['TEMP']) ||
+                  writeableDir(process.env['TMP']) ||
+                  writeableDir(process.env['Wimp$ScrapDir']) || // RiscOS
+                  writeableDir('C:\\TEMP') || // Windows
+                  writeableDir('C:\\TMP') || // Windows
+                  writeableDir('\\TEMP') || // Windows
+                  writeableDir('\\TMP') || // Windows
+                  writeableDir('/tmp') ||
+                  writeableDir('/var/tmp') ||
+                  writeableDir('/usr/tmp') ||
+                  writeableDir('.'); // last resort
+
+  return state.tempDir;
+}
+
+// Wrapper around exec() to enable echoing output to console in real time
+function execAsync(cmd, opts, callback) {
+  var output = '';
+
+  var options = extend({
+    silent: config.silent
+  }, opts);
+
+  var c = child.exec(cmd, {env: process.env, maxBuffer: 20*1024*1024}, function(err) {
+    if (callback)
+      callback(err ? err.code : 0, output);
+  });
+
+  c.stdout.on('data', function(data) {
+    output += data;
+    if (!options.silent)
+      process.stdout.write(data);
+  });
+
+  c.stderr.on('data', function(data) {
+    output += data;
+    if (!options.silent)
+      process.stdout.write(data);
+  });
+
+  return c;
+}
+
+// Hack to run child_process.exec() synchronously (sync avoids callback hell)
+// Uses a custom wait loop that checks for a flag file, created when the child process is done.
+// (Can't do a wait loop that checks for internal Node variables/messages as
+// Node is single-threaded; callbacks and other internal state changes are done in the
+// event loop).
+function execSync(cmd, opts) {
+  var stdoutFile = path.resolve(tempDir()+'/'+randomFileName()),
+      codeFile = path.resolve(tempDir()+'/'+randomFileName()),
+      scriptFile = path.resolve(tempDir()+'/'+randomFileName()),
+      sleepFile = path.resolve(tempDir()+'/'+randomFileName());
+
+  var options = extend({
+    silent: config.silent
+  }, opts);
+
+  var previousStdoutContent = '';
+  // Echoes stdout changes from running process, if not silent
+  function updateStdout() {
+    if (options.silent || !fs.existsSync(stdoutFile))
+      return;
+
+    var stdoutContent = fs.readFileSync(stdoutFile, 'utf8');
+    // No changes since last time?
+    if (stdoutContent.length <= previousStdoutContent.length)
+      return;
+
+    process.stdout.write(stdoutContent.substr(previousStdoutContent.length));
+    previousStdoutContent = stdoutContent;
+  }
+
+  function escape(str) {
+    return (str+'').replace(/([\\"'])/g, "\\$1").replace(/\0/g, "\\0");
+  }
+
+  cmd += ' > '+stdoutFile+' 2>&1'; // works on both win/unix
+
+  var script =
+   "var child = require('child_process')," +
+   "     fs = require('fs');" +
+   "child.exec('"+escape(cmd)+"', {env: process.env, maxBuffer: 20*1024*1024}, function(err) {" +
+   "  fs.writeFileSync('"+escape(codeFile)+"', err ? err.code.toString() : '0');" +
+   "});";
+
+  if (fs.existsSync(scriptFile)) _unlinkSync(scriptFile);
+  if (fs.existsSync(stdoutFile)) _unlinkSync(stdoutFile);
+  if (fs.existsSync(codeFile)) _unlinkSync(codeFile);
+
+  fs.writeFileSync(scriptFile, script);
+  child.exec('"'+process.execPath+'" '+scriptFile, {
+    env: process.env,
+    cwd: exports.pwd(),
+    maxBuffer: 20*1024*1024
+  });
+
+  // The wait loop
+  // sleepFile is used as a dummy I/O op to mitigate unnecessary CPU usage
+  // (tried many I/O sync ops, writeFileSync() seems to be only one that is effective in reducing
+  // CPU usage, though apparently not so much on Windows)
+  while (!fs.existsSync(codeFile)) { updateStdout(); fs.writeFileSync(sleepFile, 'a'); }
+  while (!fs.existsSync(stdoutFile)) { updateStdout(); fs.writeFileSync(sleepFile, 'a'); }
+
+  // At this point codeFile exists, but it's not necessarily flushed yet.
+  // Keep reading it until it is.
+  var code = parseInt('', 10);
+  while (isNaN(code)) {
+    code = parseInt(fs.readFileSync(codeFile, 'utf8'), 10);
+  }
+
+  var stdout = fs.readFileSync(stdoutFile, 'utf8');
+
+  // No biggie if we can't erase the files now -- they're in a temp dir anyway
+  try { _unlinkSync(scriptFile); } catch(e) {}
+  try { _unlinkSync(stdoutFile); } catch(e) {}
+  try { _unlinkSync(codeFile); } catch(e) {}
+  try { _unlinkSync(sleepFile); } catch(e) {}
+
+  // True if successful, false if not
+  var obj = {
+    code: code,
+    output: stdout
+  };
+  return obj;
+} // execSync()
+
+// Expands wildcards with matching file names. For a given array of file names 'list', returns
+// another array containing all file names as per ls(list[i]).
+// For example:
+//   expand(['file*.js']) = ['file1.js', 'file2.js', ...]
+//   (if the files 'file1.js', 'file2.js', etc, exist in the current dir)
+function expand(list) {
+  var expanded = [];
+  list.forEach(function(listEl) {
+    // Wildcard present?
+    if (listEl.search(/\*/) > -1) {
+      _ls('', listEl).forEach(function(file) {
+        expanded.push(file);
+      });
+    } else {
+      expanded.push(listEl);
+    }
+  });
+  return expanded;
+}
+
+// Cross-platform method for splitting environment PATH variables
+function splitPath(p) {
+  if (!p)
+    return [];
+
+  if (platform === 'win')
+    return p.split(';');
+  else
+    return p.split(':');
+}
+
+// extend(target_obj, source_obj1 [, source_obj2 ...])
+// Shallow extend, e.g.:
+//    extend({A:1}, {b:2}, {c:3}) returns {A:1, b:2, c:3}
+function extend(target) {
+  var sources = [].slice.call(arguments, 1);
+  sources.forEach(function(source) {
+    for (var key in source)
+      target[key] = source[key];
+  });
+
+  return target;
+}
+
+// Normalizes _unlinkSync() across platforms to match Unix behavior, i.e.
+// file can be unlinked even if it's read-only, see joyent/node#3006
+function _unlinkSync(file) {
+  try {
+    fs.unlinkSync(file);
+  } catch(e) {
+    // Try to override file permission
+    if (e.code === 'EPERM') {
+      fs.chmodSync(file, '0666');
+      fs.unlinkSync(file);
+    } else {
+      throw e;
+    }
+  }
+}
+
+// Hack to determine if file has write permissions for current user
+// Avoids having to check user, group, etc, but it's probably slow
+function isWriteable(file) {
+  var writePermission = true;
+  try {
+    var __fd = fs.openSync(file, 'a');
+    fs.closeSync(__fd);
+  } catch(e) {
+    writePermission = false;
+  }
+
+  return writePermission;
+}
diff --git a/bin/package.json b/bin/package.json
new file mode 100644
index 0000000..cceb151
--- /dev/null
+++ b/bin/package.json
@@ -0,0 +1,32 @@
+{
+    "name": "cordova-android",
+    "description": "Cordova tooling for the android platform.",
+    "version": "0.0.0",
+    "homepage": "http://github.com/apache/cordova-android",
+    "repository": {
+        "type": "git",
+        "url": "https://git-wip-us.apache.org/repos/asf/cordova-andorid.git"
+    },
+    "keywords": [
+        "cli",
+        "cordova",
+        "tooling"
+    ],
+    "engineStrict": "true",
+    "engines": {
+        "node": ">=0.10.0"
+    },
+    "dependencies": {
+        "shelljs" : "0.1.4"
+    },
+    "devDependencies": {
+    },
+    "optionalDependencies": {
+    },
+    "author": {
+        "name": "Benn Mapes",
+        "email": "bennmapes@gmail.com"
+    },
+    "contributors": [
+    ]
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/build b/bin/templates/cordova/build
new file mode 100755
index 0000000..752945f
--- /dev/null
+++ b/bin/templates/cordova/build
@@ -0,0 +1,35 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var build = require('./lib/build'),
+    reqs  = require('./lib/check_reqs'),
+    args  = process.argv;
+
+// Support basic help commands
+if(args[2] == '--help' || args[2] == '/?' || args[2] == '-h' ||
+                    args[2] == 'help' || args[2] == '-help' || args[2] == '/help') {
+    build.help();
+} else if(reqs.run()) {
+    build.run(args[2]);
+    process.exit(0);
+} else {
+    process.exit(2);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/build.bat b/bin/templates/cordova/build.bat
new file mode 100644
index 0000000..2f317e3
--- /dev/null
+++ b/bin/templates/cordova/build.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0build"
+IF EXIST %script_path% (
+        node "%script_path%" %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'build' script in 'cordova' folder, aborting...>&2
+    EXIT /B 1
+)
\ No newline at end of file
diff --git a/bin/templates/cordova/clean b/bin/templates/cordova/clean
new file mode 100755
index 0000000..6b72e71
--- /dev/null
+++ b/bin/templates/cordova/clean
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var clean = require('./lib/clean'),
+    reqs  = require('./lib/check_reqs'),
+    args  = process.argv;
+
+// Usage support for when args are given
+if(args.length > 2) {
+    clean.help();
+} else if(reqs.run()) {
+    clean.run();
+    process.exit(0);
+} else {
+    process.exit(2);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/clean.bat b/bin/templates/cordova/clean.bat
new file mode 100644
index 0000000..fa1f669
--- /dev/null
+++ b/bin/templates/cordova/clean.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0clean"
+IF EXIST %script_path% (
+        node "%script_path%" %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'clean' script in 'cordova' folder, aborting...>&2
+    EXIT /B 1
+)
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/appinfo.js b/bin/templates/cordova/lib/appinfo.js
new file mode 100644
index 0000000..1f8ebe2
--- /dev/null
+++ b/bin/templates/cordova/lib/appinfo.js
@@ -0,0 +1,41 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var path = require('path');
+var fs = require('fs');
+var cachedAppInfo = null;
+
+function readAppInfoFromManifest() {
+    var manifestPath = path.join(__dirname, '..', '..', 'AndroidManifest.xml');
+    var manifestData = fs.readFileSync(manifestPath, {encoding:'utf8'});
+    var packageName = /\bpackage\s*=\s*"(.+?)"/.exec(manifestData);
+    if (!packageName) throw new Error('Could not find package name within ' + manifestPath);
+    var activityTag = /<activity\b[\s\S]*<\/activity>/.exec(manifestData);
+    if (!activityTag) throw new Error('Could not find <activity> within ' + manifestPath);
+    var activityName = /\bandroid:name\s*=\s*"(.+?)"/.exec(activityTag);
+    if (!activityName) throw new Error('Could not find android:name within ' + manifestPath);
+
+    return packageName[1] + '/.' + activityName[1];
+}
+
+exports.getActivityName = function() {
+    return cachedAppInfo = cachedAppInfo || readAppInfoFromManifest();
+};
diff --git a/bin/templates/cordova/lib/build.js b/bin/templates/cordova/lib/build.js
new file mode 100644
index 0000000..84e4e02
--- /dev/null
+++ b/bin/templates/cordova/lib/build.js
@@ -0,0 +1,89 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var shell   = require('shelljs'),
+    clean   = require('./clean'),
+    path    = require('path'),
+    fs      = require('fs'),
+    ROOT    = path.join(__dirname, '..', '..');
+
+/*
+ * Builds the project with ant.
+ */
+module.exports.run = function(build_type) {
+    //default build type
+    build_type = typeof build_type !== 'undefined' ? build_type : "--debug";
+    var cmd;
+    switch(build_type) {
+        case '--debug' :
+            clean.run();
+            cmd = 'ant debug -f ' + path.join(ROOT, 'build.xml');
+            break;
+        case '--release' :
+            clean.run();
+            cmd = 'ant release -f ' + path.join(ROOT, 'build.xml');
+            break;
+        case '--nobuild' :
+            console.log('Skipping build...');
+            break;
+        default :
+           console.error('Build option \'' + build_type + '\' not recognized.');
+           process.exit(2);
+           break;
+    }
+    if(cmd) {
+        var result = shell.exec(cmd, {silent:false, async:false});
+        if(result.code > 0) {
+            console.error('ERROR: Failed to build android project.');
+            console.error(result.output);
+            process.exit(2);
+        }
+    }
+}
+
+/*
+ * Gets the path to the apk file, if not such file exists then
+ * the script will error out. (should we error or just return undefined?)
+ */
+module.exports.get_apk = function() {
+    if(fs.existsSync(path.join(ROOT, 'bin'))) {
+        var bin_files = fs.readdirSync(path.join(ROOT, 'bin'));
+        for (file in bin_files) {
+            if(path.extname(bin_files[file]) == '.apk') {
+                return path.join(ROOT, 'bin', bin_files[file]);
+            }
+        }
+        console.error('ERROR : No .apk found in \'bin\' folder');
+        process.exit(2);
+    } else {
+        console.error('ERROR : unable to find project bin folder, could not locate .apk');
+        process.exit(2);
+    }
+}
+
+module.exports.help = function() {
+    console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'corodva', 'build')) + ' [build_type]');
+    console.log('Build Types : ');
+    console.log('    \'--debug\': Default build, will build project in using ant debug');
+    console.log('    \'--release\': will build project using ant release');
+    console.log('    \'--nobuild\': will skip build process (can be used with run command)');
+    process.exit(0);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/clean.js b/bin/templates/cordova/lib/clean.js
new file mode 100644
index 0000000..579a5fa
--- /dev/null
+++ b/bin/templates/cordova/lib/clean.js
@@ -0,0 +1,43 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var shell = require('shelljs'),
+    path  = require('path'),
+    ROOT = path.join(__dirname, '..', '..');
+
+/*
+ * Cleans the project using ant
+ */
+module.exports.run = function() {
+    var cmd = 'ant clean -f ' + path.join(ROOT, 'build.xml');
+    var result = shell.exec(cmd, {silent:false, async:false});
+    if (result.code > 0) {
+        console.error('ERROR: Failed to clean android project.');
+        console.error(result.output);
+        process.exit(2);
+    }
+}
+
+module.exports.help = function() {
+    console.log('Usage: ' + path.relative(process.cwd(), process.argv[1]));
+    console.log('Cleans the project directory.');
+    process.exit(0);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/device.js b/bin/templates/cordova/lib/device.js
new file mode 100644
index 0000000..46686b6
--- /dev/null
+++ b/bin/templates/cordova/lib/device.js
@@ -0,0 +1,95 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var shell = require('shelljs'),
+    path  = require('path'),
+    build = require('./build'),
+    appinfo = require('./appinfo'),
+    exec  = require('child_process').exec,
+    ROOT = path.join(__dirname, '..', '..');
+
+/**
+ * Returns a list of the device ID's found
+ */
+module.exports.list = function() {
+    var cmd = 'adb devices';
+    var result = shell.exec(cmd, {silent:true, async:false});
+    if (result.code > 0) {
+        console.error('Failed to execute android command \'' + cmd + '\'.');
+        process.exit(2);
+    } else {
+        var response = result.output.split('\n');
+        var device_list = [];
+        for (var i = 1; i < response.length; i++) {
+            if (response[i].match(/\w+\tdevice/) && !response[i].match(/emulator/)) {
+                device_list.push(response[i].replace(/\tdevice/, '').replace('\r', ''));
+            }
+        }
+        return device_list;
+    }
+}
+
+/*
+ * Installs a previously built application on the device
+ * and launches it.
+ */
+module.exports.install = function(target) {
+    var device_list = this.list();
+    if (device_list.length > 0) {
+        // default device
+        target = typeof target !== 'undefined' ? target : device_list[0];
+        if (device_list.indexOf(target) > -1) {
+            var apk_path = build.get_apk();
+            var launchName = appinfo.getActivityName();
+            console.log('Installing app on device...');
+            cmd = 'adb -s ' + target + ' install -r ' + apk_path;
+            var install = shell.exec(cmd, {silent:false, async:false});
+            if (install.error || install.output.match(/Failure/)) {
+                console.error('ERROR : Failed to install apk to device : ');
+                console.error(install.output);
+                process.exit(2);
+            }
+
+            //unlock screen
+            cmd = 'adb -s ' + target + ' shell input keyevent 82';
+            shell.exec(cmd, {silent:true, async:false});
+
+            // launch the application
+            console.log('Launching application...');
+            cmd = 'adb -s ' + target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName;
+            var launch = shell.exec(cmd, {silent:true, async:false});
+            if(launch.code > 0) {
+                console.error('ERROR : Failed to launch application on emulator : ' + launch.error);
+                console.error(launch.output);
+                process.exit(2);
+            } else {
+                console.log('LANCH SUCCESS');
+            }
+        } else {
+            console.error('ERROR : Unable to find target \'' + target + '\'.');
+            console.error('Failed to deploy to device.');
+            process.exit(2);
+        }
+    } else {
+        console.error('ERROR : Failed to deploy to device, no devices found.');
+        process.exit(2);
+    }
+}
diff --git a/bin/templates/cordova/lib/emulator.js b/bin/templates/cordova/lib/emulator.js
new file mode 100644
index 0000000..6f8a7dd
--- /dev/null
+++ b/bin/templates/cordova/lib/emulator.js
@@ -0,0 +1,337 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var shell = require('shelljs'),
+    path  = require('path'),
+    appinfo = require('./appinfo'),
+    build = require('./build'),
+    ROOT  = path.join(__dirname, '..', '..'),
+    new_emulator = 'cordova_emulator';
+
+/**
+ * Returns a list of emulator images in the form of objects
+ * {
+       name   : <emulator_name>,
+       path   : <path_to_emulator_image>,
+       target : <api_target>,
+       abi    : <cpu>,
+       skin   : <skin>
+   }
+ */
+module.exports.list_images = function() {
+    var cmd = 'android list avds';
+    var result = shell.exec(cmd, {silent:true, async:false});
+    if (result.code > 0) {
+        console.error('Failed to execute android command \'' + cmd + '\'.');
+        process.exit(2);
+    } else {
+        var response = result.output.split('\n');
+        var emulator_list = [];
+        for (var i = 1; i < response.length; i++) {
+            // To return more detailed information use img_obj
+            var img_obj = {};
+            if (response[i].match(/Name:\s/)) {
+                img_obj['name'] = response[i].split('Name: ')[1].replace('\r', '');
+                if (response[i + 1].match(/Path:\s/)) {
+                    i++;
+                    img_obj['path'] = response[i].split('Path: ')[1].replace('\r', '');
+                }
+                if (response[i + 1].match(/\(API\slevel\s/)) {
+                    i++;
+                    img_obj['target'] = response[i].replace('\r', '');
+                }
+                if (response[i + 1].match(/ABI:\s/)) {
+                    i++;
+                    img_obj['abi'] = response[i].split('ABI: ')[1].replace('\r', '');
+                }
+                if (response[i + 1].match(/Skin:\s/)) {
+                    i++;
+                    img_obj['skin'] = response[i].split('Skin: ')[1].replace('\r', '');
+                }
+
+                emulator_list.push(img_obj);
+            }
+            /* To just return a list of names use this
+            if (response[i].match(/Name:\s/)) {
+                emulator_list.push(response[i].split('Name: ')[1].replace('\r', '');
+            }*/
+
+        }
+        return emulator_list;
+    }
+}
+
+/**
+ * Will return the closest avd to the projects target
+ * or undefined if no avds exist.
+ */
+module.exports.best_image = function() {
+    var project_target = this.get_target().replace('android-', '');
+    var images = this.list_images();
+    var closest = 9999;
+    var best = images[0];
+    for (i in images) {
+        var target = images[i].target;
+        if(target) {
+            var num = target.split('(API level ')[1].replace(')', '');
+            if (num == project_target) {
+                return images[i];
+            } else if (project_target - num < closest && project_target > num) {
+                var closest = project_target - num;
+                best = images[i];
+            }
+        }
+    }
+    return best;
+}
+
+module.exports.list_started = function() {
+    var cmd = 'adb devices';
+    var result = shell.exec(cmd, {silent:true, async:false});
+    if (result.code > 0) {
+        console.error('Failed to execute android command \'' + cmd + '\'.');
+        process.exit(2);
+    } else {
+        var response = result.output.split('\n');
+        var started_emulator_list = [];
+        for (var i = 1; i < response.length; i++) {
+            if (response[i].match(/device/) && response[i].match(/emulator/)) {
+                started_emulator_list.push(response[i].replace(/\tdevice/, '').replace('\r', ''));
+            }
+        }
+        return started_emulator_list;
+    }
+}
+
+module.exports.get_target = function() {
+    var target = shell.grep(/target=android-[\d+]/, path.join(ROOT, 'project.properties'));
+    return target.split('=')[1].replace('\n', '').replace('\r', '').replace(' ', '');
+}
+
+module.exports.list_targets = function() {
+    var target_out = shell.exec('android list targets', {silent:true, async:false}).output.split('\n');
+    var targets = [];
+    for (var i = target_out.length; i >= 0; i--) {
+        if(target_out[i].match(/id:/)) {
+            targets.push(targets[i].split(' ')[1]);
+        }
+    }
+    return targets;
+}
+
+/*
+ * Starts an emulator with the given ID,
+ * and returns the started ID of that emulator.
+ * If no ID is given it will used the first image availible,
+ * if no image is availible it will error out (maybe create one?).
+ */
+module.exports.start = function(emulator_ID) {
+    var started_emulators = this.list_started();
+    var num_started = started_emulators.length;
+    if (typeof emulator_ID === 'undefined') {
+        var emulator_list = this.list_images();
+        if (emulator_list.length > 0) {
+            emulator_ID = this.best_image().name;
+            console.log('WARNING : no emulator specified, defaulting to ' + emulator_ID);
+        } else {
+            console.error('ERROR : No emulator images (avds) found, if you would like to create an');
+            console.error(' avd follow the instructions provided here : ');
+            console.error(' http://developer.android.com/tools/devices/index.html')
+            console.error(' Or run \'android create avd --name <name> --target <targetID>\' ');
+            console.error(' in on the command line.');
+            process.exit(2);
+            /*console.log('WARNING : no emulators availible, creating \'' + new_emulator + '\'.');
+            this.create_image(new_emulator, this.get_target());
+            emulator_ID = new_emulator;*/
+        }
+    }
+
+    var pipe_null = (process.platform == 'win32' || process.platform == 'win64'? '> NUL' : '> /dev/null');
+    var cmd = 'emulator -avd ' + emulator_ID + ' ' + pipe_null + ' &';
+    if(process.platform == 'win32' || process.platform == 'win64') {
+        cmd = '%comspec% /c start cmd /c ' + cmd;
+    }
+    var result = shell.exec(cmd, {silent:true, async:false}, function(code, output) {
+        if (code > 0) {
+            console.error('Failed to execute android command \'' + cmd + '\'.');
+            console.error(output);
+            process.exit(2);
+        }
+    });
+
+    // wait for emulator to start
+    console.log('Waiting for emulator...');
+    var new_started = this.wait_for_emulator(num_started);
+    var emulator_id;
+    if (new_started.length > 1) {
+        for (i in new_started) {
+            console.log(new_started[i]);
+            console.log(started_emulators.indexOf(new_started[i]));
+            if (started_emulators.indexOf(new_started[i]) < 0) {
+                emulator_id = new_started[i];
+            }
+        }
+    } else {
+        emulator_id = new_started[0];
+    }
+    if (!emulator_id) {
+        console.error('ERROR :  Failed to start emulator, could not find new emulator');
+        process.exit(2);
+    }
+
+    //wait for emulator to boot up
+    process.stdout.write('Booting up emulator (this may take a while)...');
+    this.wait_for_boot(emulator_id);
+    console.log('BOOT COMPLETE');
+
+    //unlock screen
+    cmd = 'adb -s ' + emulator_id + ' shell input keyevent 82';
+    shell.exec(cmd, {silent:false, async:false});
+
+    //return the new emulator id for the started emulators
+    return emulator_id;
+}
+
+/*
+ * Waits for the new emulator to apear on the started-emulator list.
+ */
+module.exports.wait_for_emulator = function(num_running) {
+    var new_started = this.list_started();
+    if (new_started.length > num_running) {
+        return new_started;
+    } else {
+        this.sleep(1);
+        return this.wait_for_emulator(num_running);
+    }
+}
+
+/*
+ * Waits for the boot animation property of the emulator to switch to 'stopped'
+ */
+module.exports.wait_for_boot = function(emulator_id) {
+    var cmd;
+    // ShellJS opens a lot of file handles, and the default on OS X is too small.
+    // TODO : This is not working, need to find a better way to increese the ulimit.
+    if(process.platform == 'win32' || process.platform == 'win64') {
+        cmd = 'adb -s ' + emulator_id + ' shell getprop init.svc.bootanim';
+    } else {
+        cmd = 'ulimit -S -n 4096; adb -s ' + emulator_id + ' shell getprop init.svc.bootanim';
+    }
+    var boot_anim = shell.exec(cmd, {silent:true, async:false});
+    if (boot_anim.output.match(/stopped/)) {
+        return;
+    } else {
+        process.stdout.write('.');
+        this.sleep(3);
+        return this.wait_for_boot(emulator_id);
+    }
+}
+
+/*
+ * TODO : find a better way to wait for the emulator (maybe using async methods?)
+ */
+module.exports.sleep = function(time_sec) {
+    if (process.platform == 'win32' || process.platform == 'win64') {
+        shell.exec('ping 127.0.0.1 -n ' + time_sec, {silent:true, async:false});
+    } else {
+        shell.exec('sleep ' + time_sec, {silent:true, async:false});
+    }
+}
+
+/*
+ * Create avd
+ * TODO : Enter the stdin input required to complete the creation of an avd.
+ */
+module.exports.create_image = function(name, target) {
+    console.log('Creating avd named ' + name);
+    if (target) {
+        var cmd = 'android create avd --name ' + name + ' --target ' + target;
+        var create = shell.exec(cmd, {sient:false, async:false});
+        if (create.error) {
+            console.error('ERROR : Failed to create emulator image : ');
+            console.error(' Do you have the latest android targets including ' + target + '?');
+            console.error(create.output);
+            process.exit(2);
+        }
+    } else {
+        console.log('WARNING : Project target not found, creating avd with a different target but the project may fail to install.');
+        var cmd = 'android create avd --name ' + name + ' --target ' + this.list_targets()[0];
+        var create = shell.exec(cmd, {sient:false, async:false});
+        if (create.error) {
+            console.error('ERROR : Failed to create emulator image : ');
+            console.error(create.output);
+            process.exit(2);
+        }
+        console.error('ERROR : Unable to create an avd emulator, no targets found.');
+        console.error('Please insure you have targets availible by runing the "android" command').
+        process.exit(2);
+    }
+}
+
+/*
+ * Installs a previously built application on the emulator and launches it.
+ * If no target is specified, then it picks one.
+ * If no started emulators are found, error out.
+ */
+module.exports.install = function(target) {
+    var emulator_list = this.list_started();
+    if (emulator_list.length < 1) {
+        console.error('ERROR : No started emulators found, please start an emultor before deploying your project.');
+        process.exit(2);
+        /*console.log('WARNING : No started emulators found, attemting to start an avd...');
+        this.start(this.best_image().name);*/
+    }
+    // default emulator
+    target = typeof target !== 'undefined' ? target : emulator_list[0];
+    if (emulator_list.indexOf(target) > -1) {
+        console.log('Installing app on emulator...');
+        var apk_path = build.get_apk();
+        var cmd = 'adb -s ' + target + ' install -r ' + apk_path;
+        var install = shell.exec(cmd, {sient:false, async:false});
+        if (install.error || install.output.match(/Failure/)) {
+            console.error('ERROR : Failed to install apk to emulator : ');
+            console.error(install.output);
+            process.exit(2);
+        }
+
+        //unlock screen
+        cmd = 'adb -s ' + target + ' shell input keyevent 82';
+        shell.exec(cmd, {silent:true, async:false});
+
+        // launch the application
+        console.log('Launching application...');
+        var launchName = appinfo.getActivityName();
+        cmd = 'adb -s ' + target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName;
+        console.log(cmd);
+        var launch = shell.exec(cmd, {silent:false, async:false});
+        if(launch.code > 0) {
+            console.error('ERROR : Failed to launch application on emulator : ' + launch.error);
+            console.error(launch.output);
+            process.exit(2);
+        } else {
+            console.log('LANCH SUCCESS');
+        }
+    } else {
+        console.error('ERROR : Unable to find target \'' + target + '\'.');
+        console.error('Failed to deploy to emulator.');
+        process.exit(2);
+    }
+}
diff --git a/bin/templates/cordova/lib/install-device b/bin/templates/cordova/lib/install-device
new file mode 100755
index 0000000..cf53918
--- /dev/null
+++ b/bin/templates/cordova/lib/install-device
@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var device = require('./device'),
+    args   = process.argv;
+
+if(args.length > 2) {
+    var install_target;
+    if (args[2].substring(0, 9) == '--target=') {
+        install_target = args[2].substring(9, args[2].length);
+        device.install(install_target);
+        process.exit(0);
+     } else {
+        console.error('ERROR : argument \'' + args[2] + '\' not recognized.');
+        process.exit(2);
+     }
+} else {
+    device.install();
+    process.exit(0);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/install-device.bat b/bin/templates/cordova/lib/install-device.bat
new file mode 100644
index 0000000..ac7214a
--- /dev/null
+++ b/bin/templates/cordova/lib/install-device.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0install-device"
+IF EXIST %script_path% (
+        node "%script_path%" %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'install-device' script in 'cordova\lib' folder, aborting...>&2
+    EXIT /B 1
+)
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/install-emulator b/bin/templates/cordova/lib/install-emulator
new file mode 100755
index 0000000..70421be
--- /dev/null
+++ b/bin/templates/cordova/lib/install-emulator
@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var emulator = require('./emulator'),
+    args     = process.argv;
+
+if(args.length > 2) {
+    var install_target;
+    if (args[2].substring(0, 9) == '--target=') {
+        install_target = args[2].substring(9, args[2].length);
+        emulator.install(install_target);
+        process.exit(0);
+     } else {
+        console.error('ERROR : argument \'' + args[2] + '\' not recognized.');
+        process.exit(2);
+     }
+} else {
+    emulator.install();
+    process.exit(0);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/install-emulator.bat b/bin/templates/cordova/lib/install-emulator.bat
new file mode 100644
index 0000000..1ec6779
--- /dev/null
+++ b/bin/templates/cordova/lib/install-emulator.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0install-emulator"
+IF EXIST %script_path% (
+        node "%script_path%" %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'install-emulator' script in 'cordova\lib' folder, aborting...>&2
+    EXIT /B 1
+)
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/list-devices b/bin/templates/cordova/lib/list-devices
new file mode 100755
index 0000000..bdd0abd
--- /dev/null
+++ b/bin/templates/cordova/lib/list-devices
@@ -0,0 +1,28 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var devices = require('./device');
+
+// Usage support for when args are given
+var device_list = devices.list();
+for(device in device_list) {
+    console.log(device_list[device]);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/list-devices.bat b/bin/templates/cordova/lib/list-devices.bat
new file mode 100644
index 0000000..c0bcdd9
--- /dev/null
+++ b/bin/templates/cordova/lib/list-devices.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0list-devices"
+IF EXIST %script_path% (
+        node "%script_path%" %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'list-devices' script in 'cordova\lib' folder, aborting...>&2
+    EXIT /B 1
+)
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/list-emulator-images b/bin/templates/cordova/lib/list-emulator-images
new file mode 100755
index 0000000..69f4789
--- /dev/null
+++ b/bin/templates/cordova/lib/list-emulator-images
@@ -0,0 +1,29 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var emulators = require('./emulator');
+
+// Usage support for when args are given
+var emulator_list = emulators.list_images();
+for(emulator in emulator_list) {
+    console.log(emulator_list[emulator].name);
+    process.exit(0);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/list-emulator-images.bat b/bin/templates/cordova/lib/list-emulator-images.bat
new file mode 100644
index 0000000..661cbf9
--- /dev/null
+++ b/bin/templates/cordova/lib/list-emulator-images.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0list-emulator-images"
+IF EXIST %script_path% (
+        node "%script_path%" %*
+) ELSE (
+    ECHO. 
+    ECHO ERROR: Could not find 'list-emulator-images' script in 'cordova\lib' folder, aborting...>&2
+    EXIT /B 1
+)
diff --git a/bin/templates/cordova/lib/list-started-emulators b/bin/templates/cordova/lib/list-started-emulators
new file mode 100755
index 0000000..3e69b2f
--- /dev/null
+++ b/bin/templates/cordova/lib/list-started-emulators
@@ -0,0 +1,29 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var emulators = require('./emulator');
+
+// Usage support for when args are given
+var emulator_list = emulators.list_started();
+for(emulator in emulator_list) {
+    console.log(emulator_list[emulator]);
+    process.exit(0);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/list-started-emulators.bat b/bin/templates/cordova/lib/list-started-emulators.bat
new file mode 100644
index 0000000..a4e88f7
--- /dev/null
+++ b/bin/templates/cordova/lib/list-started-emulators.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0list-started-emulators"
+IF EXIST %script_path% (
+        node "%script_path%" %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'list-started-emulators' script in 'cordova\lib' folder, aborting...>&2
+    EXIT /B 1
+)
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/log.js b/bin/templates/cordova/lib/log.js
new file mode 100644
index 0000000..ff14e46
--- /dev/null
+++ b/bin/templates/cordova/lib/log.js
@@ -0,0 +1,43 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var shell = require('shelljs'),
+    path  = require('path'),
+    ROOT = path.join(__dirname, '..', '..');
+
+/*
+ * Starts running logcat in the shell.
+ */
+module.exports.run = function() {
+    var cmd = 'adb logcat | grep -v nativeGetEnabledTags';
+    var result = shell.exec(cmd, {silent:false, async:false});
+    if (result.code > 0) {
+        console.error('ERROR: Failed to run logcat command.');
+        console.error(result.output);
+        process.exit(2);
+    }
+}
+
+module.exports.help = function() {
+    console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'corodva', 'log')));
+    console.log('Gives the logcat output on the command line.');
+    process.exit(0);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/run.js b/bin/templates/cordova/lib/run.js
new file mode 100644
index 0000000..b1c8b2b
--- /dev/null
+++ b/bin/templates/cordova/lib/run.js
@@ -0,0 +1,123 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var path  = require('path'),
+    build = require('./build'),
+    emulator = require('./emulator'),
+    device   = require('./device');
+
+/*
+ * Runs the application on a device if availible.
+ * If not device is found, it will use a started emulator.
+ * If no started emulators are found it will attempt to start an avd.
+ * If no avds are found it will error out.
+ */
+ module.exports.run = function(args) {
+    var build_type;
+    var install_target;
+
+    for (var i=2; i<args.length; i++) {
+        if (args[i] == '--debug') {
+            build_type = '--debug';
+        } else if (args[i] == '--release') {
+            build_type = '--release';
+        } else if (args[i] == '--nobuild') {
+            build_type = '--nobuild';
+        } else if (args[i] == '--device') {
+            install_target = '--device';
+        } else if (args[i] == '--emulator') {
+            install_target = '--emulator';
+        } else if (args[i].substring(0, 9) == '--target=') {
+            install_target = args[i].substring(9, args[i].length);
+        } else {
+            console.error('ERROR : Run option \'' + args[i] + '\' not recognized.');
+            process.exit(2);
+        }
+    }
+    build.run(build_type);
+    if (install_target == '--device') {
+        device.install();
+    } else if (install_target == '--emulator') {
+        if (emulator.list_started() == 0) {
+            emulator.start();
+        }
+        emulator.install();
+    } else if (install_target) {
+        var devices = device.list();
+        var started_emulators = emulator.list_started();
+        var avds = emulator.list_images();
+        if (devices.indexOf(install_target) > -1) {
+            device.install(install_target);
+        } else if (started_emulators.indexOf(install_target) > -1) {
+            emulator.install(install_target);
+        } else {
+            // if target emulator isn't started, then start it.
+            var emulator_ID;
+            for(avd in avds) {
+                if(avds[avd].name == install_target) {
+                    emulator_ID = emulator.start(install_target);
+                    emulator.install(emulator_ID);
+                    break;
+                }
+            }
+            if(!emulator_ID) {
+                console.error('ERROR : Target \'' + install_target + '\' not found, unalbe to run project');
+                process.exit(2);
+            }
+        }
+    } else {
+        // no target given, deploy to device if availible, otherwise use the emulator.
+        var device_list = device.list();
+        if (device_list.length > 0) {
+            console.log('WARNING : No target specified, deploying to device \'' + device_list[0] + '\'.');
+            device.install(device_list[0])
+        } else {
+            var emulator_list = emulator.list_started();
+            if (emulator_list.length > 0) {
+                console.log('WARNING : No target specified, deploying to emulator \'' + emulator_list[0] + '\'.');
+                emulator.install(emulator_list[0]);
+            } else {
+                console.log('WARNING : No started emulators found, starting an emulator.');
+                var best_avd = emulator.best_image();
+                if(best_avd) {
+                    var emulator_ID = emulator.start(best_avd.name);
+                    console.log('WARNING : No target specified, deploying to emulator \'' + emulator_ID + '\'.');
+                    emulator.install(emulator_ID);
+                } else {
+                    emulator.start();
+                }
+            }
+        }
+    }
+}
+
+module.exports.help = function() {
+    console.log('Usage: ' + path.relative(process.cwd(), args[0]) + ' [options]');
+    console.log('Build options :');
+    console.log('    --debug : Builds project in debug mode');
+    console.log('    --release : Builds project in release mode');
+    console.log('    --nobuild : Runs the currently built project without recompiling');
+    console.log('Deploy options :');
+    console.log('    --device : Will deploy the built project to a device');
+    console.log('    --emulator : Will deploy the built project to an emulator if one exists');
+    console.log('    --target=<target_id> : Installs to the target with the specified id.');
+    process.exit(0);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/start-emulator b/bin/templates/cordova/lib/start-emulator
new file mode 100755
index 0000000..8ea6d3f
--- /dev/null
+++ b/bin/templates/cordova/lib/start-emulator
@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var emulator = require('./emulator'),
+      args   = process.argv;
+
+if(args.length > 2) {
+    var install_target;
+    if (args[2].substring(0, 9) == '--target=') {
+        install_target = args[2].substring(9, args[2].length);
+        emulator.start(install_target);
+        process.exit(0);
+     } else {
+        console.error('ERROR : argument \'' + args[2] + '\' not recognized.');
+        process.exit(2);
+     }
+} else {
+    emulator.start();
+    process.exit(0);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/lib/start-emulator.bat b/bin/templates/cordova/lib/start-emulator.bat
new file mode 100644
index 0000000..9329d95
--- /dev/null
+++ b/bin/templates/cordova/lib/start-emulator.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0start-emulator"
+IF EXIST %script_path% (
+        node "%script_path%" %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'start-emulator' script in 'cordova\lib' folder, aborting...>&2
+    EXIT /B 1
+)
\ No newline at end of file
diff --git a/bin/templates/cordova/log b/bin/templates/cordova/log
new file mode 100755
index 0000000..514f69c
--- /dev/null
+++ b/bin/templates/cordova/log
@@ -0,0 +1,33 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var log  = require('./lib/log'),
+    reqs = require('./lib/check_reqs'),
+    args = process.argv;
+
+// Usage support for when args are given
+if(args.length > 2) {
+    log.help();
+} else if(reqs.run()) {
+    log.run();
+} else {
+    process.exit(2);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/log.bat b/bin/templates/cordova/log.bat
new file mode 100644
index 0000000..875982f
--- /dev/null
+++ b/bin/templates/cordova/log.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0log"
+IF EXIST %script_path% (
+        node "%script_path%" %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'log' script in 'cordova' folder, aborting...>&2
+    EXIT /B 1
+)
\ No newline at end of file
diff --git a/bin/templates/cordova/run b/bin/templates/cordova/run
new file mode 100755
index 0000000..c3a5772
--- /dev/null
+++ b/bin/templates/cordova/run
@@ -0,0 +1,35 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+var run  = require('./lib/run'),
+    reqs = require('./lib/check_reqs'),
+    args = process.argv;
+
+// Support basic help commands
+if (args[2] == '--help' || args[2] == '/?' || args[2] == '-h' ||
+                    args[2] == 'help' || args[2] == '-help' || args[2] == '/help') {
+    run.help();
+} else if(reqs.run()) {
+    run.run(args);
+    process.exit(0);
+} else {
+    process.exit(2);
+}
\ No newline at end of file
diff --git a/bin/templates/cordova/run.bat b/bin/templates/cordova/run.bat
new file mode 100644
index 0000000..0aad853
--- /dev/null
+++ b/bin/templates/cordova/run.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0run"
+IF EXIST %script_path% (
+        node "%script_path%" %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'run' script in 'cordova' folder, aborting...>&2
+    EXIT /B 1
+)
\ No newline at end of file
diff --git a/bin/templates/cordova/version b/bin/templates/cordova/version
new file mode 100755
index 0000000..36c3388
--- /dev/null
+++ b/bin/templates/cordova/version
@@ -0,0 +1,25 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+
+// Coho updates this line:
+var VERSION = "3.2.0-dev";
+
+console.log(VERSION);
diff --git a/bin/templates/cordova/version.bat b/bin/templates/cordova/version.bat
new file mode 100644
index 0000000..d589002
--- /dev/null
+++ b/bin/templates/cordova/version.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0version"
+IF EXIST %script_path% (
+        node "%script_path%" %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'version' script in 'cordova' folder, aborting...>&2
+    EXIT /B 1
+)
diff --git a/bin/templates/project/Activity.java b/bin/templates/project/Activity.java
new file mode 100644
index 0000000..00248b7
--- /dev/null
+++ b/bin/templates/project/Activity.java
@@ -0,0 +1,37 @@
+/*
+       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 __ID__;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class __ACTIVITY__ extends CordovaActivity 
+{
+    @Override
+    public void onCreate(Bundle savedInstanceState)
+    {
+        super.onCreate(savedInstanceState);
+        super.init();
+        // Set by <content src="index.html" /> in config.xml
+        super.loadUrl(Config.getStartUrl());
+        //super.loadUrl("file:///android_asset/www/index.html")
+    }
+}
+
diff --git a/bin/templates/project/AndroidManifest.xml b/bin/templates/project/AndroidManifest.xml
new file mode 100644
index 0000000..3a3b6a6
--- /dev/null
+++ b/bin/templates/project/AndroidManifest.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+       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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:windowSoftInputMode="adjustPan"
+      package="__PACKAGE__" android:versionName="1.0" android:versionCode="1" android:hardwareAccelerated="true">
+    <supports-screens
+        android:largeScreens="true"
+        android:normalScreens="true"
+        android:smallScreens="true"
+        android:xlargeScreens="true"
+        android:resizeable="true"
+        android:anyDensity="true"
+        />
+
+    <uses-permission android:name="android.permission.INTERNET" />
+
+    <application android:icon="@drawable/icon" android:label="@string/app_name"
+        android:hardwareAccelerated="true"
+        android:debuggable="true">
+        <activity android:name="__ACTIVITY__" android:label="@string/app_name"
+                android:theme="@android:style/Theme.Black.NoTitleBar"
+                android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <uses-library android:name="com.amazon.webview" android:required="false"/>
+        <service android:name="org.chromium.content.app.SandboxedProcessService0"
+            android:process=":sandboxed_process0"
+            android:permission="org.chromium.content_shell.permission.SANDBOX"
+            android:exported="false"
+            />
+        <service android:name="org.chromium.content.app.SandboxedProcessService1"
+            android:process=":sandboxed_process1"
+            android:permission="org.chromium.content_shell.permission.SANDBOX"
+            android:exported="false"
+            />
+        <service android:name="org.chromium.content.app.SandboxedProcessService2"
+            android:process=":sandboxed_process2"
+            android:permission="org.chromium.content_shell.permission.SANDBOX"
+            android:exported="false"
+            />
+        <service android:name="org.chromium.content.app.SandboxedProcessService3"
+            android:process=":sandboxed_process3"
+            android:permission="org.chromium.content_shell.permission.SANDBOX"
+            android:exported="false"
+            />
+        <service android:name="org.chromium.content.app.SandboxedProcessService4"
+            android:process=":sandboxed_process4"
+            android:permission="org.chromium.content_shell.permission.SANDBOX"
+            android:exported="false"
+            />
+        <service android:name="org.chromium.content.app.SandboxedProcessService5"
+            android:process=":sandboxed_process5"
+            android:permission="org.chromium.content_shell.permission.SANDBOX"
+            android:exported="false"
+            />
+    </application>
+
+    <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="__APILEVEL__"/>
+</manifest> 
diff --git a/bin/templates/project/assets/www/css/index.css b/bin/templates/project/assets/www/css/index.css
new file mode 100644
index 0000000..51daa79
--- /dev/null
+++ b/bin/templates/project/assets/www/css/index.css
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+* {
+    -webkit-tap-highlight-color: rgba(0,0,0,0); /* make transparent link selection, adjust last value opacity 0 to 1.0 */
+}
+
+body {
+    -webkit-touch-callout: none;                /* prevent callout to copy image, etc when tap to hold */
+    -webkit-text-size-adjust: none;             /* prevent webkit from resizing text to fit */
+    -webkit-user-select: none;                  /* prevent copy paste, to allow, change 'none' to 'text' */
+    background-color:#E4E4E4;
+    background-image:linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%);
+    background-image:-webkit-linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%);
+    background-image:-ms-linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%);
+    background-image:-webkit-gradient(
+        linear,
+        left top,
+        left bottom,
+        color-stop(0, #A7A7A7),
+        color-stop(0.51, #E4E4E4)
+    );
+    background-attachment:fixed;
+    font-family:'HelveticaNeue-Light', 'HelveticaNeue', Helvetica, Arial, sans-serif;
+    font-size:12px;
+    height:100%;
+    margin:0px;
+    padding:0px;
+    text-transform:uppercase;
+    width:100%;
+}
+
+/* Portrait layout (default) */
+.app {
+    background:url(../img/logo.png) no-repeat center top; /* 170px x 200px */
+    position:absolute;             /* position in the center of the screen */
+    left:50%;
+    top:50%;
+    height:50px;                   /* text area height */
+    width:225px;                   /* text area width */
+    text-align:center;
+    padding:180px 0px 0px 0px;     /* image height is 200px (bottom 20px are overlapped with text) */
+    margin:-115px 0px 0px -112px;  /* offset vertical: half of image height and text area height */
+                                   /* offset horizontal: half of text area width */
+}
+
+/* Landscape layout (with min-width) */
+@media screen and (min-aspect-ratio: 1/1) and (min-width:400px) {
+    .app {
+        background-position:left center;
+        padding:75px 0px 75px 170px;  /* padding-top + padding-bottom + text area = image height */
+        margin:-90px 0px 0px -198px;  /* offset vertical: half of image height */
+                                      /* offset horizontal: half of image width and text area width */
+    }
+}
+
+h1 {
+    font-size:24px;
+    font-weight:normal;
+    margin:0px;
+    overflow:visible;
+    padding:0px;
+    text-align:center;
+}
+
+.event {
+    border-radius:4px;
+    -webkit-border-radius:4px;
+    color:#FFFFFF;
+    font-size:12px;
+    margin:0px 30px;
+    padding:2px 0px;
+}
+
+.event.listening {
+    background-color:#333333;
+    display:block;
+}
+
+.event.received {
+    background-color:#4B946A;
+    display:none;
+}
+
+@keyframes fade {
+    from { opacity: 1.0; }
+    50% { opacity: 0.4; }
+    to { opacity: 1.0; }
+}
+ 
+@-webkit-keyframes fade {
+    from { opacity: 1.0; }
+    50% { opacity: 0.4; }
+    to { opacity: 1.0; }
+}
+ 
+.blink {
+    animation:fade 3000ms infinite;
+    -webkit-animation:fade 3000ms infinite;
+}
diff --git a/bin/templates/project/assets/www/img/cordova.png b/bin/templates/project/assets/www/img/cordova.png
new file mode 100644
index 0000000..e8169cf
--- /dev/null
+++ b/bin/templates/project/assets/www/img/cordova.png
Binary files differ
diff --git a/bin/templates/project/assets/www/img/logo.png b/bin/templates/project/assets/www/img/logo.png
new file mode 100644
index 0000000..9519e7d
--- /dev/null
+++ b/bin/templates/project/assets/www/img/logo.png
Binary files differ
diff --git a/bin/templates/project/assets/www/index.html b/bin/templates/project/assets/www/index.html
new file mode 100644
index 0000000..e84fbd7
--- /dev/null
+++ b/bin/templates/project/assets/www/index.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+    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.
+-->
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <meta name="format-detection" content="telephone=no" />
+        <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />
+        <link rel="stylesheet" type="text/css" href="css/index.css" />
+        <title>Hello World</title>
+    </head>
+    <body>
+        <div class="app">
+            <h1>Apache Cordova</h1>
+            <div id="deviceready" class="blink">
+                <p class="event listening">Connecting to Device</p>
+                <p class="event received">Device is Ready</p>
+            </div>
+        </div>
+        <script type="text/javascript" src="cordova.js"></script>
+        <script type="text/javascript" src="js/index.js"></script>
+        <script type="text/javascript">
+            app.initialize();
+        </script>
+    </body>
+</html>
diff --git a/bin/templates/project/assets/www/js/index.js b/bin/templates/project/assets/www/js/index.js
new file mode 100644
index 0000000..31d9064
--- /dev/null
+++ b/bin/templates/project/assets/www/js/index.js
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+var app = {
+    // Application Constructor
+    initialize: function() {
+        this.bindEvents();
+    },
+    // Bind Event Listeners
+    //
+    // Bind any events that are required on startup. Common events are:
+    // 'load', 'deviceready', 'offline', and 'online'.
+    bindEvents: function() {
+        document.addEventListener('deviceready', this.onDeviceReady, false);
+    },
+    // deviceready Event Handler
+    //
+    // The scope of 'this' is the event. In order to call the 'receivedEvent'
+    // function, we must explicity call 'app.receivedEvent(...);'
+    onDeviceReady: function() {
+        app.receivedEvent('deviceready');
+    },
+    // Update DOM on a Received Event
+    receivedEvent: function(id) {
+        var parentElement = document.getElementById(id);
+        var listeningElement = parentElement.querySelector('.listening');
+        var receivedElement = parentElement.querySelector('.received');
+
+        listeningElement.setAttribute('style', 'display:none;');
+        receivedElement.setAttribute('style', 'display:block;');
+
+        console.log('Received Event: ' + id);
+    }
+};
diff --git a/bin/templates/project/assets/www/main.js b/bin/templates/project/assets/www/main.js
new file mode 100644
index 0000000..3a8b04a
--- /dev/null
+++ b/bin/templates/project/assets/www/main.js
@@ -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.
+*/
+
+var deviceInfo = function() {
+    document.getElementById("platform").innerHTML = device.platform;
+    document.getElementById("version").innerHTML = device.version;
+    document.getElementById("uuid").innerHTML = device.uuid;
+    document.getElementById("name").innerHTML = device.name;
+    document.getElementById("width").innerHTML = screen.width;
+    document.getElementById("height").innerHTML = screen.height;
+    document.getElementById("colorDepth").innerHTML = screen.colorDepth;
+};
+
+var getLocation = function() {
+    var suc = function(p) {
+        alert(p.coords.latitude + " " + p.coords.longitude);
+    };
+    var locFail = function() {
+    };
+    navigator.geolocation.getCurrentPosition(suc, locFail);
+};
+
+var beep = function() {
+    navigator.notification.beep(2);
+};
+
+var vibrate = function() {
+    navigator.notification.vibrate(0);
+};
+
+function roundNumber(num) {
+    var dec = 3;
+    var result = Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
+    return result;
+}
+
+var accelerationWatch = null;
+
+function updateAcceleration(a) {
+    document.getElementById('x').innerHTML = roundNumber(a.x);
+    document.getElementById('y').innerHTML = roundNumber(a.y);
+    document.getElementById('z').innerHTML = roundNumber(a.z);
+}
+
+var toggleAccel = function() {
+    if (accelerationWatch !== null) {
+        navigator.accelerometer.clearWatch(accelerationWatch);
+        updateAcceleration({
+            x : "",
+            y : "",
+            z : ""
+        });
+        accelerationWatch = null;
+    } else {
+        var options = {};
+        options.frequency = 1000;
+        accelerationWatch = navigator.accelerometer.watchAcceleration(
+                updateAcceleration, function(ex) {
+                    alert("accel fail (" + ex.name + ": " + ex.message + ")");
+                }, options);
+    }
+};
+
+var preventBehavior = function(e) {
+    e.preventDefault();
+};
+
+function dump_pic(data) {
+    var viewport = document.getElementById('viewport');
+    console.log(data);
+    viewport.style.display = "";
+    viewport.style.position = "absolute";
+    viewport.style.top = "10px";
+    viewport.style.left = "10px";
+    document.getElementById("test_img").src = data;
+}
+
+function fail(msg) {
+    alert(msg);
+}
+
+function show_pic() {
+    navigator.camera.getPicture(dump_pic, fail, {
+        quality : 50
+    });
+}
+
+function close() {
+    var viewport = document.getElementById('viewport');
+    viewport.style.position = "relative";
+    viewport.style.display = "none";
+}
+
+function contacts_success(contacts) {
+    alert(contacts.length
+            + ' contacts returned.'
+            + (contacts[2] && contacts[2].name ? (' Third contact is ' + contacts[2].name.formatted)
+                    : ''));
+}
+
+function get_contacts() {
+    var obj = new ContactFindOptions();
+    obj.filter = "";
+    obj.multiple = true;
+    navigator.contacts.find(
+            [ "displayName", "name" ], contacts_success,
+            fail, obj);
+}
+
+function check_network() {
+    var networkState = navigator.network.connection.type;
+
+    var states = {};
+    states[Connection.UNKNOWN]  = 'Unknown connection';
+    states[Connection.ETHERNET] = 'Ethernet connection';
+    states[Connection.WIFI]     = 'WiFi connection';
+    states[Connection.CELL_2G]  = 'Cell 2G connection';
+    states[Connection.CELL_3G]  = 'Cell 3G connection';
+    states[Connection.CELL_4G]  = 'Cell 4G connection';
+    states[Connection.NONE]     = 'No network connection';
+
+    confirm('Connection type:\n ' + states[networkState]);
+}
+
+var watchID = null;
+
+function updateHeading(h) {
+    document.getElementById('h').innerHTML = h.magneticHeading;
+}
+
+function toggleCompass() {
+    if (watchID !== null) {
+        navigator.compass.clearWatch(watchID);
+        watchID = null;
+        updateHeading({ magneticHeading : "Off"});
+    } else {        
+        var options = { frequency: 1000 };
+        watchID = navigator.compass.watchHeading(updateHeading, function(e) {
+            alert('Compass Error: ' + e.code);
+        }, options);
+    }
+}
+
+function init() {
+    // the next line makes it impossible to see Contacts on the HTC Evo since it
+    // doesn't have a scroll button
+    // document.addEventListener("touchmove", preventBehavior, false);
+    document.addEventListener("deviceready", deviceInfo, true);
+}
diff --git a/bin/templates/project/assets/www/master.css b/bin/templates/project/assets/www/master.css
new file mode 100644
index 0000000..3aad33d
--- /dev/null
+++ b/bin/templates/project/assets/www/master.css
@@ -0,0 +1,116 @@
+/*
+       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.
+*/
+
+
+ body {
+    background:#222 none repeat scroll 0 0;
+    color:#666;
+    font-family:Helvetica;
+    font-size:72%;
+    line-height:1.5em;
+    margin:0;
+    border-top:1px solid #393939;
+  }
+
+  #info{
+    background:#ffa;
+    border: 1px solid #ffd324;
+    -webkit-border-radius: 5px;
+    border-radius: 5px;
+    clear:both;
+    margin:15px 6px 0;
+    width:295px;
+    padding:4px 0px 2px 10px;
+  }
+  
+  #info > h4{
+    font-size:.95em;
+    margin:5px 0;
+  }
+ 	
+  #stage.theme{
+    padding-top:3px;
+  }
+
+  /* Definition List */
+  #stage.theme > dl{
+  	padding-top:10px;
+  	clear:both;
+  	margin:0;
+  	list-style-type:none;
+  	padding-left:10px;
+  	overflow:auto;
+  }
+
+  #stage.theme > dl > dt{
+  	font-weight:bold;
+  	float:left;
+  	margin-left:5px;
+  }
+
+  #stage.theme > dl > dd{
+  	width:45px;
+  	float:left;
+  	color:#a87;
+  	font-weight:bold;
+  }
+
+  /* Content Styling */
+  #stage.theme > h1, #stage.theme > h2, #stage.theme > p{
+    margin:1em 0 .5em 13px;
+  }
+
+  #stage.theme > h1{
+    color:#eee;
+    font-size:1.6em;
+    text-align:center;
+    margin:0;
+    margin-top:15px;
+    padding:0;
+  }
+
+  #stage.theme > h2{
+  	clear:both;
+    margin:0;
+    padding:3px;
+    font-size:1em;
+    text-align:center;
+  }
+
+  /* Stage Buttons */
+  #stage.theme a.btn{
+  	border: 1px solid #555;
+  	-webkit-border-radius: 5px;
+  	border-radius: 5px;
+  	text-align:center;
+  	display:block;
+  	float:left;
+  	background:#444;
+  	width:150px;
+  	color:#9ab;
+  	font-size:1.1em;
+  	text-decoration:none;
+  	padding:1.2em 0;
+  	margin:3px 0px 3px 5px;
+  }
+  #stage.theme a.btn.large{
+  	width:308px;
+  	padding:1.2em 0;
+  }
+
diff --git a/bin/templates/project/res/drawable-hdpi/icon.png b/bin/templates/project/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000..4d27634
--- /dev/null
+++ b/bin/templates/project/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/bin/templates/project/res/drawable-ldpi/icon.png b/bin/templates/project/res/drawable-ldpi/icon.png
new file mode 100644
index 0000000..cd5032a
--- /dev/null
+++ b/bin/templates/project/res/drawable-ldpi/icon.png
Binary files differ
diff --git a/bin/templates/project/res/drawable-mdpi/icon.png b/bin/templates/project/res/drawable-mdpi/icon.png
new file mode 100644
index 0000000..e79c606
--- /dev/null
+++ b/bin/templates/project/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/bin/templates/project/res/drawable-xhdpi/icon.png b/bin/templates/project/res/drawable-xhdpi/icon.png
new file mode 100644
index 0000000..ec7ffbf
--- /dev/null
+++ b/bin/templates/project/res/drawable-xhdpi/icon.png
Binary files differ
diff --git a/bin/templates/project/res/drawable/icon.png b/bin/templates/project/res/drawable/icon.png
new file mode 100644
index 0000000..ec7ffbf
--- /dev/null
+++ b/bin/templates/project/res/drawable/icon.png
Binary files differ
diff --git a/bin/templates/project/res/values/strings.xml b/bin/templates/project/res/values/strings.xml
new file mode 100644
index 0000000..e8ed749
--- /dev/null
+++ b/bin/templates/project/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">__NAME__</string>
+</resources>
diff --git a/bin/update b/bin/update
new file mode 100755
index 0000000..aabe7db
--- /dev/null
+++ b/bin/update
@@ -0,0 +1,33 @@
+#!/usr/bin/env node
+
+/*
+       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.
+*/
+var path   = require('path');
+var args  = process.argv;
+var create = require('./lib/create');
+
+// Support basic help commands
+if(args.length < 3 || (args[2] == '--help' || args[2] == '/?' || args[2] == '-h' ||
+                    args[2] == 'help' || args[2] == '-help' || args[2] == '/help')) {
+    console.log('Usage: ' + path.relative(process.cwd(), path.join(__dirname, 'update')) + ' <path_to_project>');
+    process.exit(1);
+} else {
+    create.updateProject(args[2]);
+}
+
diff --git a/bin/update.bat b/bin/update.bat
new file mode 100644
index 0000000..d0aa7a0
--- /dev/null
+++ b/bin/update.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0update"
+IF EXIST %script_path% (
+    node %script_path% %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'update' script in 'bin' folder, aborting...>&2
+    EXIT /B 1
+)
diff --git a/framework/.classpath b/framework/.classpath
new file mode 100644
index 0000000..767bb46
--- /dev/null
+++ b/framework/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="src" path="gen"/>
+	<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+	<classpathentry kind="output" path="bin/classes"/>
+</classpath>
diff --git a/framework/.project b/framework/.project
new file mode 100644
index 0000000..ed4a955
--- /dev/null
+++ b/framework/.project
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+    <name>Cordova</name>
+    <comment></comment>
+    <projects>
+    </projects>
+    <buildSpec>
+        <buildCommand>
+            <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+            <arguments>
+            </arguments>
+        </buildCommand>
+        <buildCommand>
+            <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+            <arguments>
+            </arguments>
+        </buildCommand>
+        <buildCommand>
+            <name>org.eclipse.jdt.core.javabuilder</name>
+            <arguments>
+            </arguments>
+        </buildCommand>
+        <buildCommand>
+            <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+            <arguments>
+            </arguments>
+        </buildCommand>
+    </buildSpec>
+    <natures>
+        <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+        <nature>org.eclipse.jdt.core.javanature</nature>
+    </natures>
+</projectDescription>
diff --git a/framework/AndroidManifest.xml b/framework/AndroidManifest.xml
new file mode 100755
index 0000000..9dc7b00
--- /dev/null
+++ b/framework/AndroidManifest.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+       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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:windowSoftInputMode="adjustPan"
+      package="org.apache.cordova" android:versionName="1.0" android:versionCode="1">
+    <supports-screens
+        android:largeScreens="true"
+        android:normalScreens="true"
+        android:smallScreens="true"
+        android:resizeable="true"
+        android:anyDensity="true"
+        />
+    <!-- android:xlargeScreens="true" screen supported only after Android-9 -->
+
+    <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.RECEIVE_SMS" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.RECORD_VIDEO"/>
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.BROADCAST_STICKY" />
+
+    <uses-feature android:name="android.hardware.camera" />
+    <uses-feature android:name="android.hardware.camera.autofocus" />
+
+    <application android:icon="@drawable/icon" android:label="@string/app_name"
+        android:debuggable="true">
+        <activity android:name="org.apache.cordova.DroidGap" android:label="@string/app_name"
+                  android:configChanges="orientation|keyboardHidden">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+    <uses-sdk android:minSdkVersion="8" />
+</manifest>
diff --git a/framework/ant.properties b/framework/ant.properties
new file mode 100644
index 0000000..243b691
--- /dev/null
+++ b/framework/ant.properties
@@ -0,0 +1,34 @@
+#       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.
+#
+# This file is used to override default values used by the Ant build system.
+#
+# This file must be checked in Version Control Systems, as it is
+# integral to the build system of your project.
+
+# This file is only used by the Ant script.
+
+# You can use this to override default values such as
+#  'source.dir' for the location of your java source folder and
+#  'out.dir' for the location of your output folder.
+
+# You can also use it define how the release builds are signed by declaring
+# the following properties:
+#  'key.store' for the location of your keystore and
+#  'key.alias' for the name of the key to use.
+# The password will be asked during the build when you use the 'release' target.
+
diff --git a/framework/assets/www/cordova.js b/framework/assets/www/cordova.js
new file mode 100644
index 0000000..0c1edec
--- /dev/null
+++ b/framework/assets/www/cordova.js
@@ -0,0 +1,1715 @@
+// Platform: android
+// 3.2.0-dev-5ad41a7
+/*
+ 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.
+*/
+;(function() {
+var CORDOVA_JS_BUILD_LABEL = '3.2.0-dev-5ad41a7';
+// file: lib/scripts/require.js
+
+/*jshint -W079 */
+/*jshint -W020 */
+
+var require,
+    define;
+
+(function () {
+    var modules = {},
+    // Stack of moduleIds currently being built.
+        requireStack = [],
+    // Map of module ID -> index into requireStack of modules currently being built.
+        inProgressModules = {},
+        SEPERATOR = ".";
+
+
+
+    function build(module) {
+        var factory = module.factory,
+            localRequire = function (id) {
+                var resultantId = id;
+                //Its a relative path, so lop off the last portion and add the id (minus "./")
+                if (id.charAt(0) === ".") {
+                    resultantId = module.id.slice(0, module.id.lastIndexOf(SEPERATOR)) + SEPERATOR + id.slice(2);
+                }
+                return require(resultantId);
+            };
+        module.exports = {};
+        delete module.factory;
+        factory(localRequire, module.exports, module);
+        return module.exports;
+    }
+
+    require = function (id) {
+        if (!modules[id]) {
+            throw "module " + id + " not found";
+        } else if (id in inProgressModules) {
+            var cycle = requireStack.slice(inProgressModules[id]).join('->') + '->' + id;
+            throw "Cycle in require graph: " + cycle;
+        }
+        if (modules[id].factory) {
+            try {
+                inProgressModules[id] = requireStack.length;
+                requireStack.push(id);
+                return build(modules[id]);
+            } finally {
+                delete inProgressModules[id];
+                requireStack.pop();
+            }
+        }
+        return modules[id].exports;
+    };
+
+    define = function (id, factory) {
+        if (modules[id]) {
+            throw "module " + id + " already defined";
+        }
+
+        modules[id] = {
+            id: id,
+            factory: factory
+        };
+    };
+
+    define.remove = function (id) {
+        delete modules[id];
+    };
+
+    define.moduleMap = modules;
+})();
+
+//Export for use in node
+if (typeof module === "object" && typeof require === "function") {
+    module.exports.require = require;
+    module.exports.define = define;
+}
+
+// file: lib/cordova.js
+define("cordova", function(require, exports, module) {
+
+
+var channel = require('cordova/channel');
+var platform = require('cordova/platform');
+
+/**
+ * Intercept calls to addEventListener + removeEventListener and handle deviceready,
+ * resume, and pause events.
+ */
+var m_document_addEventListener = document.addEventListener;
+var m_document_removeEventListener = document.removeEventListener;
+var m_window_addEventListener = window.addEventListener;
+var m_window_removeEventListener = window.removeEventListener;
+
+/**
+ * Houses custom event handlers to intercept on document + window event listeners.
+ */
+var documentEventHandlers = {},
+    windowEventHandlers = {};
+
+document.addEventListener = function(evt, handler, capture) {
+    var e = evt.toLowerCase();
+    if (typeof documentEventHandlers[e] != 'undefined') {
+        documentEventHandlers[e].subscribe(handler);
+    } else {
+        m_document_addEventListener.call(document, evt, handler, capture);
+    }
+};
+
+window.addEventListener = function(evt, handler, capture) {
+    var e = evt.toLowerCase();
+    if (typeof windowEventHandlers[e] != 'undefined') {
+        windowEventHandlers[e].subscribe(handler);
+    } else {
+        m_window_addEventListener.call(window, evt, handler, capture);
+    }
+};
+
+document.removeEventListener = function(evt, handler, capture) {
+    var e = evt.toLowerCase();
+    // If unsubscribing from an event that is handled by a plugin
+    if (typeof documentEventHandlers[e] != "undefined") {
+        documentEventHandlers[e].unsubscribe(handler);
+    } else {
+        m_document_removeEventListener.call(document, evt, handler, capture);
+    }
+};
+
+window.removeEventListener = function(evt, handler, capture) {
+    var e = evt.toLowerCase();
+    // If unsubscribing from an event that is handled by a plugin
+    if (typeof windowEventHandlers[e] != "undefined") {
+        windowEventHandlers[e].unsubscribe(handler);
+    } else {
+        m_window_removeEventListener.call(window, evt, handler, capture);
+    }
+};
+
+function createEvent(type, data) {
+    var event = document.createEvent('Events');
+    event.initEvent(type, false, false);
+    if (data) {
+        for (var i in data) {
+            if (data.hasOwnProperty(i)) {
+                event[i] = data[i];
+            }
+        }
+    }
+    return event;
+}
+
+
+var cordova = {
+    define:define,
+    require:require,
+    version:CORDOVA_JS_BUILD_LABEL,
+    platformId:platform.id,
+    /**
+     * Methods to add/remove your own addEventListener hijacking on document + window.
+     */
+    addWindowEventHandler:function(event) {
+        return (windowEventHandlers[event] = channel.create(event));
+    },
+    addStickyDocumentEventHandler:function(event) {
+        return (documentEventHandlers[event] = channel.createSticky(event));
+    },
+    addDocumentEventHandler:function(event) {
+        return (documentEventHandlers[event] = channel.create(event));
+    },
+    removeWindowEventHandler:function(event) {
+        delete windowEventHandlers[event];
+    },
+    removeDocumentEventHandler:function(event) {
+        delete documentEventHandlers[event];
+    },
+    /**
+     * Retrieve original event handlers that were replaced by Cordova
+     *
+     * @return object
+     */
+    getOriginalHandlers: function() {
+        return {'document': {'addEventListener': m_document_addEventListener, 'removeEventListener': m_document_removeEventListener},
+        'window': {'addEventListener': m_window_addEventListener, 'removeEventListener': m_window_removeEventListener}};
+    },
+    /**
+     * Method to fire event from native code
+     * bNoDetach is required for events which cause an exception which needs to be caught in native code
+     */
+    fireDocumentEvent: function(type, data, bNoDetach) {
+        var evt = createEvent(type, data);
+        if (typeof documentEventHandlers[type] != 'undefined') {
+            if( bNoDetach ) {
+                documentEventHandlers[type].fire(evt);
+            }
+            else {
+                setTimeout(function() {
+                    // Fire deviceready on listeners that were registered before cordova.js was loaded.
+                    if (type == 'deviceready') {
+                        document.dispatchEvent(evt);
+                    }
+                    documentEventHandlers[type].fire(evt);
+                }, 0);
+            }
+        } else {
+            document.dispatchEvent(evt);
+        }
+    },
+    fireWindowEvent: function(type, data) {
+        var evt = createEvent(type,data);
+        if (typeof windowEventHandlers[type] != 'undefined') {
+            setTimeout(function() {
+                windowEventHandlers[type].fire(evt);
+            }, 0);
+        } else {
+            window.dispatchEvent(evt);
+        }
+    },
+
+    /**
+     * Plugin callback mechanism.
+     */
+    // Randomize the starting callbackId to avoid collisions after refreshing or navigating.
+    // This way, it's very unlikely that any new callback would get the same callbackId as an old callback.
+    callbackId: Math.floor(Math.random() * 2000000000),
+    callbacks:  {},
+    callbackStatus: {
+        NO_RESULT: 0,
+        OK: 1,
+        CLASS_NOT_FOUND_EXCEPTION: 2,
+        ILLEGAL_ACCESS_EXCEPTION: 3,
+        INSTANTIATION_EXCEPTION: 4,
+        MALFORMED_URL_EXCEPTION: 5,
+        IO_EXCEPTION: 6,
+        INVALID_ACTION: 7,
+        JSON_EXCEPTION: 8,
+        ERROR: 9
+    },
+
+    /**
+     * Called by native code when returning successful result from an action.
+     */
+    callbackSuccess: function(callbackId, args) {
+        try {
+            cordova.callbackFromNative(callbackId, true, args.status, [args.message], args.keepCallback);
+        } catch (e) {
+            console.log("Error in error callback: " + callbackId + " = "+e);
+        }
+    },
+
+    /**
+     * Called by native code when returning error result from an action.
+     */
+    callbackError: function(callbackId, args) {
+        // TODO: Deprecate callbackSuccess and callbackError in favour of callbackFromNative.
+        // Derive success from status.
+        try {
+            cordova.callbackFromNative(callbackId, false, args.status, [args.message], args.keepCallback);
+        } catch (e) {
+            console.log("Error in error callback: " + callbackId + " = "+e);
+        }
+    },
+
+    /**
+     * Called by native code when returning the result from an action.
+     */
+    callbackFromNative: function(callbackId, success, status, args, keepCallback) {
+        var callback = cordova.callbacks[callbackId];
+        if (callback) {
+            if (success && status == cordova.callbackStatus.OK) {
+                callback.success && callback.success.apply(null, args);
+            } else if (!success) {
+                callback.fail && callback.fail.apply(null, args);
+            }
+
+            // Clear callback if not expecting any more results
+            if (!keepCallback) {
+                delete cordova.callbacks[callbackId];
+            }
+        }
+    },
+    addConstructor: function(func) {
+        channel.onCordovaReady.subscribe(function() {
+            try {
+                func();
+            } catch(e) {
+                console.log("Failed to run constructor: " + e);
+            }
+        });
+    }
+};
+
+
+module.exports = cordova;
+
+});
+
+// file: lib/android/android/nativeapiprovider.js
+define("cordova/android/nativeapiprovider", function(require, exports, module) {
+
+/**
+ * Exports the ExposedJsApi.java object if available, otherwise exports the PromptBasedNativeApi.
+ */
+
+var nativeApi = this._cordovaNative || require('cordova/android/promptbasednativeapi');
+var currentApi = nativeApi;
+
+module.exports = {
+    get: function() { return currentApi; },
+    setPreferPrompt: function(value) {
+        currentApi = value ? require('cordova/android/promptbasednativeapi') : nativeApi;
+    },
+    // Used only by tests.
+    set: function(value) {
+        currentApi = value;
+    }
+};
+
+});
+
+// file: lib/android/android/promptbasednativeapi.js
+define("cordova/android/promptbasednativeapi", function(require, exports, module) {
+
+/**
+ * Implements the API of ExposedJsApi.java, but uses prompt() to communicate.
+ * This is used only on the 2.3 simulator, where addJavascriptInterface() is broken.
+ */
+
+module.exports = {
+    exec: function(service, action, callbackId, argsJson) {
+        return prompt(argsJson, 'gap:'+JSON.stringify([service, action, callbackId]));
+    },
+    setNativeToJsBridgeMode: function(value) {
+        prompt(value, 'gap_bridge_mode:');
+    },
+    retrieveJsMessages: function(fromOnlineEvent) {
+        return prompt(+fromOnlineEvent, 'gap_poll:');
+    }
+};
+
+});
+
+// file: lib/common/argscheck.js
+define("cordova/argscheck", function(require, exports, module) {
+
+var exec = require('cordova/exec');
+var utils = require('cordova/utils');
+
+var moduleExports = module.exports;
+
+var typeMap = {
+    'A': 'Array',
+    'D': 'Date',
+    'N': 'Number',
+    'S': 'String',
+    'F': 'Function',
+    'O': 'Object'
+};
+
+function extractParamName(callee, argIndex) {
+    return (/.*?\((.*?)\)/).exec(callee)[1].split(', ')[argIndex];
+}
+
+function checkArgs(spec, functionName, args, opt_callee) {
+    if (!moduleExports.enableChecks) {
+        return;
+    }
+    var errMsg = null;
+    var typeName;
+    for (var i = 0; i < spec.length; ++i) {
+        var c = spec.charAt(i),
+            cUpper = c.toUpperCase(),
+            arg = args[i];
+        // Asterix means allow anything.
+        if (c == '*') {
+            continue;
+        }
+        typeName = utils.typeName(arg);
+        if ((arg === null || arg === undefined) && c == cUpper) {
+            continue;
+        }
+        if (typeName != typeMap[cUpper]) {
+            errMsg = 'Expected ' + typeMap[cUpper];
+            break;
+        }
+    }
+    if (errMsg) {
+        errMsg += ', but got ' + typeName + '.';
+        errMsg = 'Wrong type for parameter "' + extractParamName(opt_callee || args.callee, i) + '" of ' + functionName + ': ' + errMsg;
+        // Don't log when running unit tests.
+        if (typeof jasmine == 'undefined') {
+            console.error(errMsg);
+        }
+        throw TypeError(errMsg);
+    }
+}
+
+function getValue(value, defaultValue) {
+    return value === undefined ? defaultValue : value;
+}
+
+moduleExports.checkArgs = checkArgs;
+moduleExports.getValue = getValue;
+moduleExports.enableChecks = true;
+
+
+});
+
+// file: lib/common/base64.js
+define("cordova/base64", function(require, exports, module) {
+
+var base64 = exports;
+
+base64.fromArrayBuffer = function(arrayBuffer) {
+    var array = new Uint8Array(arrayBuffer);
+    return uint8ToBase64(array);
+};
+
+//------------------------------------------------------------------------------
+
+/* This code is based on the performance tests at http://jsperf.com/b64tests
+ * This 12-bit-at-a-time algorithm was the best performing version on all
+ * platforms tested.
+ */
+
+var b64_6bit = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+var b64_12bit;
+
+var b64_12bitTable = function() {
+    b64_12bit = [];
+    for (var i=0; i<64; i++) {
+        for (var j=0; j<64; j++) {
+            b64_12bit[i*64+j] = b64_6bit[i] + b64_6bit[j];
+        }
+    }
+    b64_12bitTable = function() { return b64_12bit; };
+    return b64_12bit;
+};
+
+function uint8ToBase64(rawData) {
+    var numBytes = rawData.byteLength;
+    var output="";
+    var segment;
+    var table = b64_12bitTable();
+    for (var i=0;i<numBytes-2;i+=3) {
+        segment = (rawData[i] << 16) + (rawData[i+1] << 8) + rawData[i+2];
+        output += table[segment >> 12];
+        output += table[segment & 0xfff];
+    }
+    if (numBytes - i == 2) {
+        segment = (rawData[i] << 16) + (rawData[i+1] << 8);
+        output += table[segment >> 12];
+        output += b64_6bit[(segment & 0xfff) >> 6];
+        output += '=';
+    } else if (numBytes - i == 1) {
+        segment = (rawData[i] << 16);
+        output += table[segment >> 12];
+        output += '==';
+    }
+    return output;
+}
+
+});
+
+// file: lib/common/builder.js
+define("cordova/builder", function(require, exports, module) {
+
+var utils = require('cordova/utils');
+
+function each(objects, func, context) {
+    for (var prop in objects) {
+        if (objects.hasOwnProperty(prop)) {
+            func.apply(context, [objects[prop], prop]);
+        }
+    }
+}
+
+function clobber(obj, key, value) {
+    exports.replaceHookForTesting(obj, key);
+    obj[key] = value;
+    // Getters can only be overridden by getters.
+    if (obj[key] !== value) {
+        utils.defineGetter(obj, key, function() {
+            return value;
+        });
+    }
+}
+
+function assignOrWrapInDeprecateGetter(obj, key, value, message) {
+    if (message) {
+        utils.defineGetter(obj, key, function() {
+            console.log(message);
+            delete obj[key];
+            clobber(obj, key, value);
+            return value;
+        });
+    } else {
+        clobber(obj, key, value);
+    }
+}
+
+function include(parent, objects, clobber, merge) {
+    each(objects, function (obj, key) {
+        try {
+            var result = obj.path ? require(obj.path) : {};
+
+            if (clobber) {
+                // Clobber if it doesn't exist.
+                if (typeof parent[key] === 'undefined') {
+                    assignOrWrapInDeprecateGetter(parent, key, result, obj.deprecated);
+                } else if (typeof obj.path !== 'undefined') {
+                    // If merging, merge properties onto parent, otherwise, clobber.
+                    if (merge) {
+                        recursiveMerge(parent[key], result);
+                    } else {
+                        assignOrWrapInDeprecateGetter(parent, key, result, obj.deprecated);
+                    }
+                }
+                result = parent[key];
+            } else {
+                // Overwrite if not currently defined.
+                if (typeof parent[key] == 'undefined') {
+                    assignOrWrapInDeprecateGetter(parent, key, result, obj.deprecated);
+                } else {
+                    // Set result to what already exists, so we can build children into it if they exist.
+                    result = parent[key];
+                }
+            }
+
+            if (obj.children) {
+                include(result, obj.children, clobber, merge);
+            }
+        } catch(e) {
+            utils.alert('Exception building cordova JS globals: ' + e + ' for key "' + key + '"');
+        }
+    });
+}
+
+/**
+ * Merge properties from one object onto another recursively.  Properties from
+ * the src object will overwrite existing target property.
+ *
+ * @param target Object to merge properties into.
+ * @param src Object to merge properties from.
+ */
+function recursiveMerge(target, src) {
+    for (var prop in src) {
+        if (src.hasOwnProperty(prop)) {
+            if (target.prototype && target.prototype.constructor === target) {
+                // If the target object is a constructor override off prototype.
+                clobber(target.prototype, prop, src[prop]);
+            } else {
+                if (typeof src[prop] === 'object' && typeof target[prop] === 'object') {
+                    recursiveMerge(target[prop], src[prop]);
+                } else {
+                    clobber(target, prop, src[prop]);
+                }
+            }
+        }
+    }
+}
+
+exports.buildIntoButDoNotClobber = function(objects, target) {
+    include(target, objects, false, false);
+};
+exports.buildIntoAndClobber = function(objects, target) {
+    include(target, objects, true, false);
+};
+exports.buildIntoAndMerge = function(objects, target) {
+    include(target, objects, true, true);
+};
+exports.recursiveMerge = recursiveMerge;
+exports.assignOrWrapInDeprecateGetter = assignOrWrapInDeprecateGetter;
+exports.replaceHookForTesting = function() {};
+
+});
+
+// file: lib/common/channel.js
+define("cordova/channel", function(require, exports, module) {
+
+var utils = require('cordova/utils'),
+    nextGuid = 1;
+
+/**
+ * Custom pub-sub "channel" that can have functions subscribed to it
+ * This object is used to define and control firing of events for
+ * cordova initialization, as well as for custom events thereafter.
+ *
+ * The order of events during page load and Cordova startup is as follows:
+ *
+ * onDOMContentLoaded*         Internal event that is received when the web page is loaded and parsed.
+ * onNativeReady*              Internal event that indicates the Cordova native side is ready.
+ * onCordovaReady*             Internal event fired when all Cordova JavaScript objects have been created.
+ * onDeviceReady*              User event fired to indicate that Cordova is ready
+ * onResume                    User event fired to indicate a start/resume lifecycle event
+ * onPause                     User event fired to indicate a pause lifecycle event
+ * onDestroy*                  Internal event fired when app is being destroyed (User should use window.onunload event, not this one).
+ *
+ * The events marked with an * are sticky. Once they have fired, they will stay in the fired state.
+ * All listeners that subscribe after the event is fired will be executed right away.
+ *
+ * The only Cordova events that user code should register for are:
+ *      deviceready           Cordova native code is initialized and Cordova APIs can be called from JavaScript
+ *      pause                 App has moved to background
+ *      resume                App has returned to foreground
+ *
+ * Listeners can be registered as:
+ *      document.addEventListener("deviceready", myDeviceReadyListener, false);
+ *      document.addEventListener("resume", myResumeListener, false);
+ *      document.addEventListener("pause", myPauseListener, false);
+ *
+ * The DOM lifecycle events should be used for saving and restoring state
+ *      window.onload
+ *      window.onunload
+ *
+ */
+
+/**
+ * Channel
+ * @constructor
+ * @param type  String the channel name
+ */
+var Channel = function(type, sticky) {
+    this.type = type;
+    // Map of guid -> function.
+    this.handlers = {};
+    // 0 = Non-sticky, 1 = Sticky non-fired, 2 = Sticky fired.
+    this.state = sticky ? 1 : 0;
+    // Used in sticky mode to remember args passed to fire().
+    this.fireArgs = null;
+    // Used by onHasSubscribersChange to know if there are any listeners.
+    this.numHandlers = 0;
+    // Function that is called when the first listener is subscribed, or when
+    // the last listener is unsubscribed.
+    this.onHasSubscribersChange = null;
+},
+    channel = {
+        /**
+         * Calls the provided function only after all of the channels specified
+         * have been fired. All channels must be sticky channels.
+         */
+        join: function(h, c) {
+            var len = c.length,
+                i = len,
+                f = function() {
+                    if (!(--i)) h();
+                };
+            for (var j=0; j<len; j++) {
+                if (c[j].state === 0) {
+                    throw Error('Can only use join with sticky channels.');
+                }
+                c[j].subscribe(f);
+            }
+            if (!len) h();
+        },
+        create: function(type) {
+            return channel[type] = new Channel(type, false);
+        },
+        createSticky: function(type) {
+            return channel[type] = new Channel(type, true);
+        },
+
+        /**
+         * cordova Channels that must fire before "deviceready" is fired.
+         */
+        deviceReadyChannelsArray: [],
+        deviceReadyChannelsMap: {},
+
+        /**
+         * Indicate that a feature needs to be initialized before it is ready to be used.
+         * This holds up Cordova's "deviceready" event until the feature has been initialized
+         * and Cordova.initComplete(feature) is called.
+         *
+         * @param feature {String}     The unique feature name
+         */
+        waitForInitialization: function(feature) {
+            if (feature) {
+                var c = channel[feature] || this.createSticky(feature);
+                this.deviceReadyChannelsMap[feature] = c;
+                this.deviceReadyChannelsArray.push(c);
+            }
+        },
+
+        /**
+         * Indicate that initialization code has completed and the feature is ready to be used.
+         *
+         * @param feature {String}     The unique feature name
+         */
+        initializationComplete: function(feature) {
+            var c = this.deviceReadyChannelsMap[feature];
+            if (c) {
+                c.fire();
+            }
+        }
+    };
+
+function forceFunction(f) {
+    if (typeof f != 'function') throw "Function required as first argument!";
+}
+
+/**
+ * Subscribes the given function to the channel. Any time that
+ * Channel.fire is called so too will the function.
+ * Optionally specify an execution context for the function
+ * and a guid that can be used to stop subscribing to the channel.
+ * Returns the guid.
+ */
+Channel.prototype.subscribe = function(f, c) {
+    // need a function to call
+    forceFunction(f);
+    if (this.state == 2) {
+        f.apply(c || this, this.fireArgs);
+        return;
+    }
+
+    var func = f,
+        guid = f.observer_guid;
+    if (typeof c == "object") { func = utils.close(c, f); }
+
+    if (!guid) {
+        // first time any channel has seen this subscriber
+        guid = '' + nextGuid++;
+    }
+    func.observer_guid = guid;
+    f.observer_guid = guid;
+
+    // Don't add the same handler more than once.
+    if (!this.handlers[guid]) {
+        this.handlers[guid] = func;
+        this.numHandlers++;
+        if (this.numHandlers == 1) {
+            this.onHasSubscribersChange && this.onHasSubscribersChange();
+        }
+    }
+};
+
+/**
+ * Unsubscribes the function with the given guid from the channel.
+ */
+Channel.prototype.unsubscribe = function(f) {
+    // need a function to unsubscribe
+    forceFunction(f);
+
+    var guid = f.observer_guid,
+        handler = this.handlers[guid];
+    if (handler) {
+        delete this.handlers[guid];
+        this.numHandlers--;
+        if (this.numHandlers === 0) {
+            this.onHasSubscribersChange && this.onHasSubscribersChange();
+        }
+    }
+};
+
+/**
+ * Calls all functions subscribed to this channel.
+ */
+Channel.prototype.fire = function(e) {
+    var fail = false,
+        fireArgs = Array.prototype.slice.call(arguments);
+    // Apply stickiness.
+    if (this.state == 1) {
+        this.state = 2;
+        this.fireArgs = fireArgs;
+    }
+    if (this.numHandlers) {
+        // Copy the values first so that it is safe to modify it from within
+        // callbacks.
+        var toCall = [];
+        for (var item in this.handlers) {
+            toCall.push(this.handlers[item]);
+        }
+        for (var i = 0; i < toCall.length; ++i) {
+            toCall[i].apply(this, fireArgs);
+        }
+        if (this.state == 2 && this.numHandlers) {
+            this.numHandlers = 0;
+            this.handlers = {};
+            this.onHasSubscribersChange && this.onHasSubscribersChange();
+        }
+    }
+};
+
+
+// defining them here so they are ready super fast!
+// DOM event that is received when the web page is loaded and parsed.
+channel.createSticky('onDOMContentLoaded');
+
+// Event to indicate the Cordova native side is ready.
+channel.createSticky('onNativeReady');
+
+// Event to indicate that all Cordova JavaScript objects have been created
+// and it's time to run plugin constructors.
+channel.createSticky('onCordovaReady');
+
+// Event to indicate that all automatically loaded JS plugins are loaded and ready.
+channel.createSticky('onPluginsReady');
+
+// Event to indicate that Cordova is ready
+channel.createSticky('onDeviceReady');
+
+// Event to indicate a resume lifecycle event
+channel.create('onResume');
+
+// Event to indicate a pause lifecycle event
+channel.create('onPause');
+
+// Event to indicate a destroy lifecycle event
+channel.createSticky('onDestroy');
+
+// Channels that must fire before "deviceready" is fired.
+channel.waitForInitialization('onCordovaReady');
+channel.waitForInitialization('onDOMContentLoaded');
+
+module.exports = channel;
+
+});
+
+// file: lib/android/exec.js
+define("cordova/exec", function(require, exports, module) {
+
+/**
+ * Execute a cordova command.  It is up to the native side whether this action
+ * is synchronous or asynchronous.  The native side can return:
+ *      Synchronous: PluginResult object as a JSON string
+ *      Asynchronous: Empty string ""
+ * If async, the native side will cordova.callbackSuccess or cordova.callbackError,
+ * depending upon the result of the action.
+ *
+ * @param {Function} success    The success callback
+ * @param {Function} fail       The fail callback
+ * @param {String} service      The name of the service to use
+ * @param {String} action       Action to be run in cordova
+ * @param {String[]} [args]     Zero or more arguments to pass to the method
+ */
+var cordova = require('cordova'),
+    nativeApiProvider = require('cordova/android/nativeapiprovider'),
+    utils = require('cordova/utils'),
+    base64 = require('cordova/base64'),
+    jsToNativeModes = {
+        PROMPT: 0,
+        JS_OBJECT: 1,
+        // This mode is currently for benchmarking purposes only. It must be enabled
+        // on the native side through the ENABLE_LOCATION_CHANGE_EXEC_MODE
+        // constant within CordovaWebViewClient.java before it will work.
+        LOCATION_CHANGE: 2
+    },
+    nativeToJsModes = {
+        // Polls for messages using the JS->Native bridge.
+        POLLING: 0,
+        // For LOAD_URL to be viable, it would need to have a work-around for
+        // the bug where the soft-keyboard gets dismissed when a message is sent.
+        LOAD_URL: 1,
+        // For the ONLINE_EVENT to be viable, it would need to intercept all event
+        // listeners (both through addEventListener and window.ononline) as well
+        // as set the navigator property itself.
+        ONLINE_EVENT: 2,
+        // Uses reflection to access private APIs of the WebView that can send JS
+        // to be executed.
+        // Requires Android 3.2.4 or above.
+        PRIVATE_API: 3
+    },
+    jsToNativeBridgeMode,  // Set lazily.
+    nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT,
+    pollEnabled = false,
+    messagesFromNative = [];
+
+function androidExec(success, fail, service, action, args) {
+    // Set default bridge modes if they have not already been set.
+    // By default, we use the failsafe, since addJavascriptInterface breaks too often
+    if (jsToNativeBridgeMode === undefined) {
+        androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
+    }
+
+    // Process any ArrayBuffers in the args into a string.
+    for (var i = 0; i < args.length; i++) {
+        if (utils.typeName(args[i]) == 'ArrayBuffer') {
+            args[i] = base64.fromArrayBuffer(args[i]);
+        }
+    }
+
+    var callbackId = service + cordova.callbackId++,
+        argsJson = JSON.stringify(args);
+
+    if (success || fail) {
+        cordova.callbacks[callbackId] = {success:success, fail:fail};
+    }
+
+    if (jsToNativeBridgeMode == jsToNativeModes.LOCATION_CHANGE) {
+        window.location = 'http://cdv_exec/' + service + '#' + action + '#' + callbackId + '#' + argsJson;
+    } else {
+        var messages = nativeApiProvider.get().exec(service, action, callbackId, argsJson);
+        // If argsJson was received by Java as null, try again with the PROMPT bridge mode.
+        // This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2.  See CB-2666.
+        if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && messages === "@Null arguments.") {
+            androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT);
+            androidExec(success, fail, service, action, args);
+            androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
+            return;
+        } else {
+            androidExec.processMessages(messages);
+        }
+    }
+}
+
+function pollOnceFromOnlineEvent() {
+    pollOnce(true);
+}
+
+function pollOnce(opt_fromOnlineEvent) {
+    var msg = nativeApiProvider.get().retrieveJsMessages(!!opt_fromOnlineEvent);
+    androidExec.processMessages(msg);
+}
+
+function pollingTimerFunc() {
+    if (pollEnabled) {
+        pollOnce();
+        setTimeout(pollingTimerFunc, 50);
+    }
+}
+
+function hookOnlineApis() {
+    function proxyEvent(e) {
+        cordova.fireWindowEvent(e.type);
+    }
+    // The network module takes care of firing online and offline events.
+    // It currently fires them only on document though, so we bridge them
+    // to window here (while first listening for exec()-releated online/offline
+    // events).
+    window.addEventListener('online', pollOnceFromOnlineEvent, false);
+    window.addEventListener('offline', pollOnceFromOnlineEvent, false);
+    cordova.addWindowEventHandler('online');
+    cordova.addWindowEventHandler('offline');
+    document.addEventListener('online', proxyEvent, false);
+    document.addEventListener('offline', proxyEvent, false);
+}
+
+hookOnlineApis();
+
+androidExec.jsToNativeModes = jsToNativeModes;
+androidExec.nativeToJsModes = nativeToJsModes;
+
+androidExec.setJsToNativeBridgeMode = function(mode) {
+    if (mode == jsToNativeModes.JS_OBJECT && !window._cordovaNative) {
+        console.log('Falling back on PROMPT mode since _cordovaNative is missing. Expected for Android 3.2 and lower only.');
+        mode = jsToNativeModes.PROMPT;
+    }
+    nativeApiProvider.setPreferPrompt(mode == jsToNativeModes.PROMPT);
+    jsToNativeBridgeMode = mode;
+};
+
+androidExec.setNativeToJsBridgeMode = function(mode) {
+    if (mode == nativeToJsBridgeMode) {
+        return;
+    }
+    if (nativeToJsBridgeMode == nativeToJsModes.POLLING) {
+        pollEnabled = false;
+    }
+
+    nativeToJsBridgeMode = mode;
+    // Tell the native side to switch modes.
+    nativeApiProvider.get().setNativeToJsBridgeMode(mode);
+
+    if (mode == nativeToJsModes.POLLING) {
+        pollEnabled = true;
+        setTimeout(pollingTimerFunc, 1);
+    }
+};
+
+// Processes a single message, as encoded by NativeToJsMessageQueue.java.
+function processMessage(message) {
+    try {
+        var firstChar = message.charAt(0);
+        if (firstChar == 'J') {
+            eval(message.slice(1));
+        } else if (firstChar == 'S' || firstChar == 'F') {
+            var success = firstChar == 'S';
+            var keepCallback = message.charAt(1) == '1';
+            var spaceIdx = message.indexOf(' ', 2);
+            var status = +message.slice(2, spaceIdx);
+            var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1);
+            var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx);
+            var payloadKind = message.charAt(nextSpaceIdx + 1);
+            var payload;
+            if (payloadKind == 's') {
+                payload = message.slice(nextSpaceIdx + 2);
+            } else if (payloadKind == 't') {
+                payload = true;
+            } else if (payloadKind == 'f') {
+                payload = false;
+            } else if (payloadKind == 'N') {
+                payload = null;
+            } else if (payloadKind == 'n') {
+                payload = +message.slice(nextSpaceIdx + 2);
+            } else if (payloadKind == 'A') {
+                var data = message.slice(nextSpaceIdx + 2);
+                var bytes = window.atob(data);
+                var arraybuffer = new Uint8Array(bytes.length);
+                for (var i = 0; i < bytes.length; i++) {
+                    arraybuffer[i] = bytes.charCodeAt(i);
+                }
+                payload = arraybuffer.buffer;
+            } else if (payloadKind == 'S') {
+                payload = window.atob(message.slice(nextSpaceIdx + 2));
+            } else {
+                payload = JSON.parse(message.slice(nextSpaceIdx + 1));
+            }
+            cordova.callbackFromNative(callbackId, success, status, [payload], keepCallback);
+        } else {
+            console.log("processMessage failed: invalid message:" + message);
+        }
+    } catch (e) {
+        console.log("processMessage failed: Message: " + message);
+        console.log("processMessage failed: Error: " + e);
+        console.log("processMessage failed: Stack: " + e.stack);
+    }
+}
+
+// This is called from the NativeToJsMessageQueue.java.
+androidExec.processMessages = function(messages) {
+    if (messages) {
+        messagesFromNative.push(messages);
+        // Check for the reentrant case, and enqueue the message if that's the case.
+        if (messagesFromNative.length > 1) {
+            return;
+        }
+        while (messagesFromNative.length) {
+            // Don't unshift until the end so that reentrancy can be detected.
+            messages = messagesFromNative[0];
+            // The Java side can send a * message to indicate that it
+            // still has messages waiting to be retrieved.
+            if (messages == '*') {
+                messagesFromNative.shift();
+                window.setTimeout(pollOnce, 0);
+                return;
+            }
+
+            var spaceIdx = messages.indexOf(' ');
+            var msgLen = +messages.slice(0, spaceIdx);
+            var message = messages.substr(spaceIdx + 1, msgLen);
+            messages = messages.slice(spaceIdx + msgLen + 1);
+            processMessage(message);
+            if (messages) {
+                messagesFromNative[0] = messages;
+            } else {
+                messagesFromNative.shift();
+            }
+        }
+    }
+};
+
+module.exports = androidExec;
+
+});
+
+// file: lib/common/init.js
+define("cordova/init", function(require, exports, module) {
+
+var channel = require('cordova/channel');
+var cordova = require('cordova');
+var modulemapper = require('cordova/modulemapper');
+var platform = require('cordova/platform');
+var pluginloader = require('cordova/pluginloader');
+
+var platformInitChannelsArray = [channel.onNativeReady, channel.onPluginsReady];
+
+function logUnfiredChannels(arr) {
+    for (var i = 0; i < arr.length; ++i) {
+        if (arr[i].state != 2) {
+            console.log('Channel not fired: ' + arr[i].type);
+        }
+    }
+}
+
+window.setTimeout(function() {
+    if (channel.onDeviceReady.state != 2) {
+        console.log('deviceready has not fired after 5 seconds.');
+        logUnfiredChannels(platformInitChannelsArray);
+        logUnfiredChannels(channel.deviceReadyChannelsArray);
+    }
+}, 5000);
+
+// Replace navigator before any modules are required(), to ensure it happens as soon as possible.
+// We replace it so that properties that can't be clobbered can instead be overridden.
+function replaceNavigator(origNavigator) {
+    var CordovaNavigator = function() {};
+    CordovaNavigator.prototype = origNavigator;
+    var newNavigator = new CordovaNavigator();
+    // This work-around really only applies to new APIs that are newer than Function.bind.
+    // Without it, APIs such as getGamepads() break.
+    if (CordovaNavigator.bind) {
+        for (var key in origNavigator) {
+            if (typeof origNavigator[key] == 'function') {
+                newNavigator[key] = origNavigator[key].bind(origNavigator);
+            }
+        }
+    }
+    return newNavigator;
+}
+if (window.navigator) {
+    window.navigator = replaceNavigator(window.navigator);
+}
+
+if (!window.console) {
+    window.console = {
+        log: function(){}
+    };
+}
+if (!window.console.warn) {
+    window.console.warn = function(msg) {
+        this.log("warn: " + msg);
+    };
+}
+
+// Register pause, resume and deviceready channels as events on document.
+channel.onPause = cordova.addDocumentEventHandler('pause');
+channel.onResume = cordova.addDocumentEventHandler('resume');
+channel.onDeviceReady = cordova.addStickyDocumentEventHandler('deviceready');
+
+// Listen for DOMContentLoaded and notify our channel subscribers.
+if (document.readyState == 'complete' || document.readyState == 'interactive') {
+    channel.onDOMContentLoaded.fire();
+} else {
+    document.addEventListener('DOMContentLoaded', function() {
+        channel.onDOMContentLoaded.fire();
+    }, false);
+}
+
+// _nativeReady is global variable that the native side can set
+// to signify that the native code is ready. It is a global since
+// it may be called before any cordova JS is ready.
+if (window._nativeReady) {
+    channel.onNativeReady.fire();
+}
+
+modulemapper.clobbers('cordova', 'cordova');
+modulemapper.clobbers('cordova/exec', 'cordova.exec');
+modulemapper.clobbers('cordova/exec', 'Cordova.exec');
+
+// Call the platform-specific initialization.
+platform.bootstrap && platform.bootstrap();
+
+pluginloader.load(function() {
+    channel.onPluginsReady.fire();
+});
+
+/**
+ * Create all cordova objects once native side is ready.
+ */
+channel.join(function() {
+    modulemapper.mapModules(window);
+
+    platform.initialize && platform.initialize();
+
+    // Fire event to notify that all objects are created
+    channel.onCordovaReady.fire();
+
+    // Fire onDeviceReady event once page has fully loaded, all
+    // constructors have run and cordova info has been received from native
+    // side.
+    channel.join(function() {
+        require('cordova').fireDocumentEvent('deviceready');
+    }, channel.deviceReadyChannelsArray);
+
+}, platformInitChannelsArray);
+
+
+});
+
+// file: lib/common/modulemapper.js
+define("cordova/modulemapper", function(require, exports, module) {
+
+var builder = require('cordova/builder'),
+    moduleMap = define.moduleMap,
+    symbolList,
+    deprecationMap;
+
+exports.reset = function() {
+    symbolList = [];
+    deprecationMap = {};
+};
+
+function addEntry(strategy, moduleName, symbolPath, opt_deprecationMessage) {
+    if (!(moduleName in moduleMap)) {
+        throw new Error('Module ' + moduleName + ' does not exist.');
+    }
+    symbolList.push(strategy, moduleName, symbolPath);
+    if (opt_deprecationMessage) {
+        deprecationMap[symbolPath] = opt_deprecationMessage;
+    }
+}
+
+// Note: Android 2.3 does have Function.bind().
+exports.clobbers = function(moduleName, symbolPath, opt_deprecationMessage) {
+    addEntry('c', moduleName, symbolPath, opt_deprecationMessage);
+};
+
+exports.merges = function(moduleName, symbolPath, opt_deprecationMessage) {
+    addEntry('m', moduleName, symbolPath, opt_deprecationMessage);
+};
+
+exports.defaults = function(moduleName, symbolPath, opt_deprecationMessage) {
+    addEntry('d', moduleName, symbolPath, opt_deprecationMessage);
+};
+
+exports.runs = function(moduleName) {
+    addEntry('r', moduleName, null);
+};
+
+function prepareNamespace(symbolPath, context) {
+    if (!symbolPath) {
+        return context;
+    }
+    var parts = symbolPath.split('.');
+    var cur = context;
+    for (var i = 0, part; part = parts[i]; ++i) {
+        cur = cur[part] = cur[part] || {};
+    }
+    return cur;
+}
+
+exports.mapModules = function(context) {
+    var origSymbols = {};
+    context.CDV_origSymbols = origSymbols;
+    for (var i = 0, len = symbolList.length; i < len; i += 3) {
+        var strategy = symbolList[i];
+        var moduleName = symbolList[i + 1];
+        var module = require(moduleName);
+        // <runs/>
+        if (strategy == 'r') {
+            continue;
+        }
+        var symbolPath = symbolList[i + 2];
+        var lastDot = symbolPath.lastIndexOf('.');
+        var namespace = symbolPath.substr(0, lastDot);
+        var lastName = symbolPath.substr(lastDot + 1);
+
+        var deprecationMsg = symbolPath in deprecationMap ? 'Access made to deprecated symbol: ' + symbolPath + '. ' + deprecationMsg : null;
+        var parentObj = prepareNamespace(namespace, context);
+        var target = parentObj[lastName];
+
+        if (strategy == 'm' && target) {
+            builder.recursiveMerge(target, module);
+        } else if ((strategy == 'd' && !target) || (strategy != 'd')) {
+            if (!(symbolPath in origSymbols)) {
+                origSymbols[symbolPath] = target;
+            }
+            builder.assignOrWrapInDeprecateGetter(parentObj, lastName, module, deprecationMsg);
+        }
+    }
+};
+
+exports.getOriginalSymbol = function(context, symbolPath) {
+    var origSymbols = context.CDV_origSymbols;
+    if (origSymbols && (symbolPath in origSymbols)) {
+        return origSymbols[symbolPath];
+    }
+    var parts = symbolPath.split('.');
+    var obj = context;
+    for (var i = 0; i < parts.length; ++i) {
+        obj = obj && obj[parts[i]];
+    }
+    return obj;
+};
+
+exports.reset();
+
+
+});
+
+// file: lib/android/platform.js
+define("cordova/platform", function(require, exports, module) {
+
+module.exports = {
+    id: 'android',
+    bootstrap: function() {
+        var channel = require('cordova/channel'),
+            cordova = require('cordova'),
+            exec = require('cordova/exec'),
+            modulemapper = require('cordova/modulemapper');
+
+        // Tell the native code that a page change has occurred.
+        exec(null, null, 'PluginManager', 'startup', []);
+        // Tell the JS that the native side is ready.
+        channel.onNativeReady.fire();
+
+        // TODO: Extract this as a proper plugin.
+        modulemapper.clobbers('cordova/plugin/android/app', 'navigator.app');
+
+        // Inject a listener for the backbutton on the document.
+        var backButtonChannel = cordova.addDocumentEventHandler('backbutton');
+        backButtonChannel.onHasSubscribersChange = function() {
+            // If we just attached the first handler or detached the last handler,
+            // let native know we need to override the back button.
+            exec(null, null, "App", "overrideBackbutton", [this.numHandlers == 1]);
+        };
+
+        // Add hardware MENU and SEARCH button handlers
+        cordova.addDocumentEventHandler('menubutton');
+        cordova.addDocumentEventHandler('searchbutton');
+
+        // Let native code know we are all done on the JS side.
+        // Native code will then un-hide the WebView.
+        channel.onCordovaReady.subscribe(function() {
+            exec(null, null, "App", "show", []);
+        });
+    }
+};
+
+});
+
+// file: lib/android/plugin/android/app.js
+define("cordova/plugin/android/app", function(require, exports, module) {
+
+var exec = require('cordova/exec');
+
+module.exports = {
+    /**
+    * Clear the resource cache.
+    */
+    clearCache:function() {
+        exec(null, null, "App", "clearCache", []);
+    },
+
+    /**
+    * Load the url into the webview or into new browser instance.
+    *
+    * @param url           The URL to load
+    * @param props         Properties that can be passed in to the activity:
+    *      wait: int                           => wait msec before loading URL
+    *      loadingDialog: "Title,Message"      => display a native loading dialog
+    *      loadUrlTimeoutValue: int            => time in msec to wait before triggering a timeout error
+    *      clearHistory: boolean              => clear webview history (default=false)
+    *      openExternal: boolean              => open in a new browser (default=false)
+    *
+    * Example:
+    *      navigator.app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000});
+    */
+    loadUrl:function(url, props) {
+        exec(null, null, "App", "loadUrl", [url, props]);
+    },
+
+    /**
+    * Cancel loadUrl that is waiting to be loaded.
+    */
+    cancelLoadUrl:function() {
+        exec(null, null, "App", "cancelLoadUrl", []);
+    },
+
+    /**
+    * Clear web history in this web view.
+    * Instead of BACK button loading the previous web page, it will exit the app.
+    */
+    clearHistory:function() {
+        exec(null, null, "App", "clearHistory", []);
+    },
+
+    /**
+    * Go to previous page displayed.
+    * This is the same as pressing the backbutton on Android device.
+    */
+    backHistory:function() {
+        exec(null, null, "App", "backHistory", []);
+    },
+
+    /**
+    * Override the default behavior of the Android back button.
+    * If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired.
+    *
+    * Note: The user should not have to call this method.  Instead, when the user
+    *       registers for the "backbutton" event, this is automatically done.
+    *
+    * @param override        T=override, F=cancel override
+    */
+    overrideBackbutton:function(override) {
+        exec(null, null, "App", "overrideBackbutton", [override]);
+    },
+
+    /**
+    * Exit and terminate the application.
+    */
+    exitApp:function() {
+        return exec(null, null, "App", "exitApp", []);
+    }
+};
+
+});
+
+// file: lib/common/pluginloader.js
+define("cordova/pluginloader", function(require, exports, module) {
+
+var modulemapper = require('cordova/modulemapper');
+
+// Helper function to inject a <script> tag.
+function injectScript(url, onload, onerror) {
+    var script = document.createElement("script");
+    // onload fires even when script fails loads with an error.
+    script.onload = onload;
+    script.onerror = onerror || onload;
+    script.src = url;
+    document.head.appendChild(script);
+}
+
+function onScriptLoadingComplete(moduleList, finishPluginLoading) {
+    // Loop through all the plugins and then through their clobbers and merges.
+    for (var i = 0, module; module = moduleList[i]; i++) {
+        if (module) {
+            try {
+                if (module.clobbers && module.clobbers.length) {
+                    for (var j = 0; j < module.clobbers.length; j++) {
+                        modulemapper.clobbers(module.id, module.clobbers[j]);
+                    }
+                }
+
+                if (module.merges && module.merges.length) {
+                    for (var k = 0; k < module.merges.length; k++) {
+                        modulemapper.merges(module.id, module.merges[k]);
+                    }
+                }
+
+                // Finally, if runs is truthy we want to simply require() the module.
+                // This can be skipped if it had any merges or clobbers, though,
+                // since the mapper will already have required the module.
+                if (module.runs && !(module.clobbers && module.clobbers.length) && !(module.merges && module.merges.length)) {
+                    modulemapper.runs(module.id);
+                }
+            }
+            catch(err) {
+                // error with module, most likely clobbers, should we continue?
+            }
+        }
+    }
+
+    finishPluginLoading();
+}
+
+// Handler for the cordova_plugins.js content.
+// See plugman's plugin_loader.js for the details of this object.
+// This function is only called if the really is a plugins array that isn't empty.
+// Otherwise the onerror response handler will just call finishPluginLoading().
+function handlePluginsObject(path, moduleList, finishPluginLoading) {
+    // Now inject the scripts.
+    var scriptCounter = moduleList.length;
+
+    if (!scriptCounter) {
+        finishPluginLoading();
+        return;
+    }
+    function scriptLoadedCallback() {
+        if (!--scriptCounter) {
+            onScriptLoadingComplete(moduleList, finishPluginLoading);
+        }
+    }
+
+    for (var i = 0; i < moduleList.length; i++) {
+        injectScript(path + moduleList[i].file, scriptLoadedCallback);
+    }
+}
+
+function injectPluginScript(pathPrefix, finishPluginLoading) {
+    injectScript(pathPrefix + 'cordova_plugins.js', function(){
+        try {
+            var moduleList = require("cordova/plugin_list");
+            handlePluginsObject(pathPrefix, moduleList, finishPluginLoading);
+        } catch (e) {
+            // Error loading cordova_plugins.js, file not found or something
+            // this is an acceptable error, pre-3.0.0, so we just move on.
+            finishPluginLoading();
+        }
+    }, finishPluginLoading); // also, add script load error handler for file not found
+}
+
+function findCordovaPath() {
+    var path = null;
+    var scripts = document.getElementsByTagName('script');
+    var term = 'cordova.js';
+    for (var n = scripts.length-1; n>-1; n--) {
+        var src = scripts[n].src;
+        if (src.indexOf(term) == (src.length - term.length)) {
+            path = src.substring(0, src.length - term.length);
+            break;
+        }
+    }
+    return path;
+}
+
+// Tries to load all plugins' js-modules.
+// This is an async process, but onDeviceReady is blocked on onPluginsReady.
+// onPluginsReady is fired when there are no plugins to load, or they are all done.
+exports.load = function(callback) {
+    var pathPrefix = findCordovaPath();
+    if (pathPrefix === null) {
+        console.log('Could not find cordova.js script tag. Plugin loading may fail.');
+        pathPrefix = '';
+    }
+    injectPluginScript(pathPrefix, callback);
+};
+
+
+});
+
+// file: lib/common/urlutil.js
+define("cordova/urlutil", function(require, exports, module) {
+
+var urlutil = exports;
+var anchorEl = document.createElement('a');
+
+/**
+ * For already absolute URLs, returns what is passed in.
+ * For relative URLs, converts them to absolute ones.
+ */
+urlutil.makeAbsolute = function(url) {
+    anchorEl.href = url;
+    return anchorEl.href;
+};
+
+});
+
+// file: lib/common/utils.js
+define("cordova/utils", function(require, exports, module) {
+
+var utils = exports;
+
+/**
+ * Defines a property getter / setter for obj[key].
+ */
+utils.defineGetterSetter = function(obj, key, getFunc, opt_setFunc) {
+    if (Object.defineProperty) {
+        var desc = {
+            get: getFunc,
+            configurable: true
+        };
+        if (opt_setFunc) {
+            desc.set = opt_setFunc;
+        }
+        Object.defineProperty(obj, key, desc);
+    } else {
+        obj.__defineGetter__(key, getFunc);
+        if (opt_setFunc) {
+            obj.__defineSetter__(key, opt_setFunc);
+        }
+    }
+};
+
+/**
+ * Defines a property getter for obj[key].
+ */
+utils.defineGetter = utils.defineGetterSetter;
+
+utils.arrayIndexOf = function(a, item) {
+    if (a.indexOf) {
+        return a.indexOf(item);
+    }
+    var len = a.length;
+    for (var i = 0; i < len; ++i) {
+        if (a[i] == item) {
+            return i;
+        }
+    }
+    return -1;
+};
+
+/**
+ * Returns whether the item was found in the array.
+ */
+utils.arrayRemove = function(a, item) {
+    var index = utils.arrayIndexOf(a, item);
+    if (index != -1) {
+        a.splice(index, 1);
+    }
+    return index != -1;
+};
+
+utils.typeName = function(val) {
+    return Object.prototype.toString.call(val).slice(8, -1);
+};
+
+/**
+ * Returns an indication of whether the argument is an array or not
+ */
+utils.isArray = function(a) {
+    return utils.typeName(a) == 'Array';
+};
+
+/**
+ * Returns an indication of whether the argument is a Date or not
+ */
+utils.isDate = function(d) {
+    return utils.typeName(d) == 'Date';
+};
+
+/**
+ * Does a deep clone of the object.
+ */
+utils.clone = function(obj) {
+    if(!obj || typeof obj == 'function' || utils.isDate(obj) || typeof obj != 'object') {
+        return obj;
+    }
+
+    var retVal, i;
+
+    if(utils.isArray(obj)){
+        retVal = [];
+        for(i = 0; i < obj.length; ++i){
+            retVal.push(utils.clone(obj[i]));
+        }
+        return retVal;
+    }
+
+    retVal = {};
+    for(i in obj){
+        if(!(i in retVal) || retVal[i] != obj[i]) {
+            retVal[i] = utils.clone(obj[i]);
+        }
+    }
+    return retVal;
+};
+
+/**
+ * Returns a wrapped version of the function
+ */
+utils.close = function(context, func, params) {
+    if (typeof params == 'undefined') {
+        return function() {
+            return func.apply(context, arguments);
+        };
+    } else {
+        return function() {
+            return func.apply(context, params);
+        };
+    }
+};
+
+/**
+ * Create a UUID
+ */
+utils.createUUID = function() {
+    return UUIDcreatePart(4) + '-' +
+        UUIDcreatePart(2) + '-' +
+        UUIDcreatePart(2) + '-' +
+        UUIDcreatePart(2) + '-' +
+        UUIDcreatePart(6);
+};
+
+/**
+ * Extends a child object from a parent object using classical inheritance
+ * pattern.
+ */
+utils.extend = (function() {
+    // proxy used to establish prototype chain
+    var F = function() {};
+    // extend Child from Parent
+    return function(Child, Parent) {
+        F.prototype = Parent.prototype;
+        Child.prototype = new F();
+        Child.__super__ = Parent.prototype;
+        Child.prototype.constructor = Child;
+    };
+}());
+
+/**
+ * Alerts a message in any available way: alert or console.log.
+ */
+utils.alert = function(msg) {
+    if (window.alert) {
+        window.alert(msg);
+    } else if (console && console.log) {
+        console.log(msg);
+    }
+};
+
+
+//------------------------------------------------------------------------------
+function UUIDcreatePart(length) {
+    var uuidpart = "";
+    for (var i=0; i<length; i++) {
+        var uuidchar = parseInt((Math.random() * 256), 10).toString(16);
+        if (uuidchar.length == 1) {
+            uuidchar = "0" + uuidchar;
+        }
+        uuidpart += uuidchar;
+    }
+    return uuidpart;
+}
+
+
+});
+
+window.cordova = require('cordova');
+// file: lib/scripts/bootstrap.js
+
+require('cordova/init');
+
+})();
\ No newline at end of file
diff --git a/framework/assets/www/index.html b/framework/assets/www/index.html
new file mode 100644
index 0000000..57ad752
--- /dev/null
+++ b/framework/assets/www/index.html
@@ -0,0 +1,27 @@
+<!--
+       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.
+-->
+<html>
+  <head>
+    <title></title>
+    <script src="cordova.js"></script>
+  </head>
+  <body>
+
+  </body>
+</html>
diff --git a/framework/build.xml b/framework/build.xml
new file mode 100644
index 0000000..46242aa
--- /dev/null
+++ b/framework/build.xml
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+       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.
+-->
+<project name="Cordova" default="jar">
+
+    <!-- LOAD VERSION -->
+    <loadfile property="version" srcFile="../VERSION">
+        <filterchain>
+            <striplinebreaks/>
+        </filterchain>
+    </loadfile>
+
+    <!-- check that the version of ant is at least 1.8.0 -->
+    <antversion property="thisantversion" atleast="1.8.0" />
+    <fail message="The required minimum version of ant is 1.8.0, you have ${ant.version}"
+          unless="thisantversion" />
+
+    <!-- The local.properties file is created and updated by the 'android' 
+         tool. (For example "sdkdir/tools/android update project -p ." inside
+         of this directory where the AndroidManifest.xml file exists. This
+         properties file that gets built contains the path to the SDK. It 
+         should *NOT* be checked into Version Control Systems since it holds
+         data about the local machine. -->
+    <available file="local.properties" property="exists.local.properties" />
+    <fail message="You need to create the file 'local.properties' by running 'android update project -p .' here."
+          unless="exists.local.properties" />
+    <loadproperties srcFile="local.properties" />
+
+    <!-- The ant.properties file can be created by you. It is only edited by the
+         'android' tool to add properties to it.
+         This is the place to change some Ant specific build properties.
+         Here are some properties you may want to change/update:
+
+         source.dir
+             The name of the source directory. Default is 'src'.
+         out.dir
+             The name of the output directory. Default is 'bin'.
+
+         For other overridable properties, look at the beginning of the rules
+         files in the SDK, at tools/ant/build.xml
+
+         Properties related to the SDK location or the project target should
+         be updated using the 'android' tool with the 'update' action.
+         This file is an integral part of the build system for your
+         application and should be checked into Version Control Systems.
+
+         -->
+    <property file="ant.properties" />
+
+    <!-- The project.properties file is created and updated by the 'android'
+         tool, as well as ADT.
+
+         This contains project specific properties such as project target, and library
+         dependencies. Lower level build properties are stored in ant.properties
+         (or in .classpath for Eclipse projects).
+
+         This file is an integral part of the build system for your
+         application and should be checked into Version Control Systems. -->
+    <loadproperties srcFile="project.properties" />
+
+    <!-- quick check on sdk.dir -->
+    <fail
+            message="sdk.dir is missing. Make sure to generate local.properties using 'android update project'"
+            unless="sdk.dir"
+    />
+
+    <!-- version-tag: custom -->
+<!-- extension targets. Uncomment the ones where you want to do custom work
+     in between standard targets -->
+<!--
+    <target name="-pre-build">
+    </target>
+    <target name="-pre-compile">
+    </target>
+
+    /* This is typically used for code obfuscation.
+       Compiled code location: ${out.classes.absolute.dir}
+       If this is not done in place, override ${out.dex.input.absolute.dir} */
+    <target name="-post-compile">
+    </target>
+-->
+
+    <!-- Import the actual build file.
+
+         To customize existing targets, there are two options:
+         - Customize only one target:
+             - copy/paste the target into this file, *before* the
+               <import> task.
+             - customize it to your needs.
+         - Customize the whole content of build.xml
+             - copy/paste the content of the rules files (minus the top node)
+               into this file, replacing the <import> task.
+             - customize to your needs.
+
+         ***********************
+         ****** IMPORTANT ******
+         ***********************
+         In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
+         in order to avoid having your file be overridden by tools such as "android update project"
+    -->
+    <import file="${sdk.dir}/tools/ant/build.xml" />
+
+    <!-- Build Cordova jar file that includes all native code, and Cordova JS file
+         that includes all JavaScript code.
+    -->
+    <target name="jar" depends="-compile">
+      <jar jarfile="cordova-${version}.jar" basedir="bin/classes" excludes="org/apache/cordova/R.class,org/apache/cordova/R$*.class"/>
+    </target>
+
+    <!-- tests for Java files -->
+    <property name="test.dir" location="test/org/apache/cordova" />
+
+    <path id="test.classpath">
+        <!-- requires both junit and cordova -->
+        <pathelement location="libs/junit-4.10.jar" />
+        <pathelement location="cordova-${version}.jar" />
+        <pathelement location="${test.dir}" />
+    </path>
+
+    <target name="compile-test">
+        <javac srcdir="${test.dir}" >
+            <classpath refid="test.classpath" />
+        </javac>
+    </target>
+
+    <target name="test" depends="jar, compile-test">
+        <junit showoutput="true">
+            <classpath refid="test.classpath" />
+            <formatter type="brief" usefile="false" />
+            <batchtest fork="yes">
+                <fileset dir="${test.dir}">
+                    <include name="*Test.java" />
+                    <include name="**/*Test.java" />
+                </fileset>
+            </batchtest>
+        </junit>
+    </target>
+
+    <target name="cordova_debug" depends="debug">
+    </target>
+
+    <target name="cordova_release" depends="release">
+    </target>
+
+</project>
diff --git a/framework/default.properties b/framework/default.properties
new file mode 100644
index 0000000..d4e24dc
--- /dev/null
+++ b/framework/default.properties
@@ -0,0 +1,14 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "build.properties", and override values to adapt the script to your
+# project structure.
+
+# Indicates whether an apk should be generated for each density.
+split.density=false
+# Project target.
+target=android-14
+apk-configurations=
diff --git a/framework/project.properties b/framework/project.properties
new file mode 100644
index 0000000..2f39d91
--- /dev/null
+++ b/framework/project.properties
@@ -0,0 +1,16 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+
+# Indicates whether an apk should be generated for each density.
+split.density=false
+# Project target.
+target=android-17
+apk-configurations=
+renderscript.opt.level=O0
+android.library=true
diff --git a/framework/res/drawable-hdpi/icon.png b/framework/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000..4d27634
--- /dev/null
+++ b/framework/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/framework/res/drawable-ldpi/icon.png b/framework/res/drawable-ldpi/icon.png
new file mode 100644
index 0000000..cd5032a
--- /dev/null
+++ b/framework/res/drawable-ldpi/icon.png
Binary files differ
diff --git a/framework/res/drawable-mdpi/icon.png b/framework/res/drawable-mdpi/icon.png
new file mode 100644
index 0000000..e79c606
--- /dev/null
+++ b/framework/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/framework/res/drawable/splash.png b/framework/res/drawable/splash.png
new file mode 100755
index 0000000..d498589
--- /dev/null
+++ b/framework/res/drawable/splash.png
Binary files differ
diff --git a/framework/res/layout/main.xml b/framework/res/layout/main.xml
new file mode 100644
index 0000000..bf8a0ff
--- /dev/null
+++ b/framework/res/layout/main.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+       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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    >
+            <WebView android:id="@+id/appView"
+            android:layout_height="fill_parent"
+            android:layout_width="fill_parent"
+            />
+</LinearLayout>
diff --git a/framework/res/values/strings.xml b/framework/res/values/strings.xml
new file mode 100644
index 0000000..5c55155
--- /dev/null
+++ b/framework/res/values/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+       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.
+-->
+<resources>
+  <string name="app_name">Cordova</string>
+  <string name="go">Snap</string>
+  
+  <!-- WebView Back-End Names -->
+  <string name="backend_name_stock_android">Android WebView</string>
+  <string name="backend_name_amazon_chromium">Amazon Chromium WebView</string>
+  <string name="backend_name_unknown">Unknown</string>
+</resources>
diff --git a/framework/res/xml/config.xml b/framework/res/xml/config.xml
new file mode 100644
index 0000000..24e5725
--- /dev/null
+++ b/framework/res/xml/config.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<widget xmlns     = "http://www.w3.org/ns/widgets"
+        id        = "io.cordova.helloCordova"
+        version   = "2.0.0">
+    <name>Hello Cordova</name>
+
+    <description>
+        A sample Apache Cordova application that responds to the deviceready event.
+    </description>
+
+    <author href="http://cordova.io" email="dev@cordova.apache.org">
+        Apache Cordova Team
+    </author>
+
+    <access origin="*"/>
+
+    <!-- <content src="http://mysite.com/myapp.html" /> for external pages -->
+    <content src="index.html" />
+
+    <preference name="loglevel" value="DEBUG" />
+    <!--
+      <preference name="splashscreen" value="resourceName" />
+      <preference name="backgroundColor" value="0xFFF" />
+      <preference name="loadUrlTimeoutValue" value="20000" />
+      <preference name="InAppBrowserStorageEnabled" value="true" />
+      <preference name="disallowOverscroll" value="true" />
+    -->
+    <!-- This is required for native Android hooks -->
+    <feature name="App">
+        <param name="android-package" value="org.apache.cordova.App" />
+    </feature>
+</widget>
diff --git a/framework/src/com/squareup/okhttp/Address.java b/framework/src/com/squareup/okhttp/Address.java
new file mode 100644
index 0000000..cd41ac9
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/Address.java
@@ -0,0 +1,111 @@
+/*
+ * 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 com.squareup.okhttp;
+
+import java.net.Proxy;
+import java.net.UnknownHostException;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSocketFactory;
+
+import static com.squareup.okhttp.internal.Util.equal;
+
+/**
+ * A specification for a connection to an origin server. For simple connections,
+ * this is the server's hostname and port. If an explicit proxy is requested (or
+ * {@link Proxy#NO_PROXY no proxy} is explicitly requested), this also includes
+ * that proxy information. For secure connections the address also includes the
+ * SSL socket factory and hostname verifier.
+ *
+ * <p>HTTP requests that share the same {@code Address} may also share the same
+ * {@link Connection}.
+ */
+public final class Address {
+  final Proxy proxy;
+  final String uriHost;
+  final int uriPort;
+  final SSLSocketFactory sslSocketFactory;
+  final HostnameVerifier hostnameVerifier;
+
+  public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory,
+      HostnameVerifier hostnameVerifier, Proxy proxy) throws UnknownHostException {
+    if (uriHost == null) throw new NullPointerException("uriHost == null");
+    if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort);
+    this.proxy = proxy;
+    this.uriHost = uriHost;
+    this.uriPort = uriPort;
+    this.sslSocketFactory = sslSocketFactory;
+    this.hostnameVerifier = hostnameVerifier;
+  }
+
+  /** Returns the hostname of the origin server. */
+  public String getUriHost() {
+    return uriHost;
+  }
+
+  /**
+   * Returns the port of the origin server; typically 80 or 443. Unlike
+   * may {@code getPort()} accessors, this method never returns -1.
+   */
+  public int getUriPort() {
+    return uriPort;
+  }
+
+  /**
+   * Returns the SSL socket factory, or null if this is not an HTTPS
+   * address.
+   */
+  public SSLSocketFactory getSslSocketFactory() {
+    return sslSocketFactory;
+  }
+
+  /**
+   * Returns the hostname verifier, or null if this is not an HTTPS
+   * address.
+   */
+  public HostnameVerifier getHostnameVerifier() {
+    return hostnameVerifier;
+  }
+
+  /**
+   * Returns this address's explicitly-specified HTTP proxy, or null to
+   * delegate to the HTTP client's proxy selector.
+   */
+  public Proxy getProxy() {
+    return proxy;
+  }
+
+  @Override public boolean equals(Object other) {
+    if (other instanceof Address) {
+      Address that = (Address) other;
+      return equal(this.proxy, that.proxy)
+          && this.uriHost.equals(that.uriHost)
+          && this.uriPort == that.uriPort
+          && equal(this.sslSocketFactory, that.sslSocketFactory)
+          && equal(this.hostnameVerifier, that.hostnameVerifier);
+    }
+    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 + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
+    result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
+    return result;
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/Connection.java b/framework/src/com/squareup/okhttp/Connection.java
new file mode 100644
index 0000000..6a6c84d
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/Connection.java
@@ -0,0 +1,291 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.http.HttpAuthenticator;
+import com.squareup.okhttp.internal.http.HttpEngine;
+import com.squareup.okhttp.internal.http.HttpTransport;
+import com.squareup.okhttp.internal.http.RawHeaders;
+import com.squareup.okhttp.internal.http.SpdyTransport;
+import com.squareup.okhttp.internal.spdy.SpdyConnection;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Proxy;
+import java.net.Socket;
+import java.net.URL;
+import java.util.Arrays;
+import javax.net.ssl.SSLSocket;
+
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
+
+/**
+ * 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.
+ *
+ * <p>Typically instances of this class are created, connected and exercised
+ * automatically by the HTTP client. Applications may use this class to monitor
+ * HTTP connections as members of a {@link ConnectionPool connection pool}.
+ *
+ * <p>Do not confuse this class with the misnamed {@code HttpURLConnection},
+ * which isn't so much a connection as a single request/response exchange.
+ *
+ * <h3>Modern TLS</h3>
+ * There are tradeoffs when selecting which options to include when negotiating
+ * a secure connection to a remote host. Newer TLS options are quite useful:
+ * <ul>
+ * <li>Server Name Indication (SNI) enables one IP address to negotiate secure
+ * connections for multiple domain names.
+ * <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used
+ * for both HTTP and SPDY transports.
+ * </ul>
+ * Unfortunately, older HTTPS servers refuse to connect when such options are
+ * presented. Rather than avoiding these options entirely, this class allows a
+ * connection to be attempted with modern options and then retried without them
+ * should the attempt fail.
+ */
+public final class Connection implements Closeable {
+  private static final byte[] NPN_PROTOCOLS = new byte[] {
+      6, 's', 'p', 'd', 'y', '/', '3',
+      8, 'h', 't', 't', 'p', '/', '1', '.', '1'
+  };
+  private static final byte[] SPDY3 = new byte[] {
+      's', 'p', 'd', 'y', '/', '3'
+  };
+  private static final byte[] HTTP_11 = new byte[] {
+      'h', 't', 't', 'p', '/', '1', '.', '1'
+  };
+
+  private final Route route;
+
+  private Socket socket;
+  private InputStream in;
+  private OutputStream out;
+  private boolean connected = false;
+  private SpdyConnection spdyConnection;
+  private int httpMinorVersion = 1; // Assume HTTP/1.1
+  private long idleStartTimeNs;
+
+  public Connection(Route route) {
+    this.route = route;
+  }
+
+  public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
+      throws IOException {
+    if (connected) {
+      throw new IllegalStateException("already connected");
+    }
+    connected = true;
+    socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket();
+    socket.connect(route.inetSocketAddress, connectTimeout);
+    socket.setSoTimeout(readTimeout);
+    in = socket.getInputStream();
+    out = socket.getOutputStream();
+
+    if (route.address.sslSocketFactory != null) {
+      upgradeToTls(tunnelRequest);
+    }
+
+    // Use MTU-sized buffers to send fewer packets.
+    int mtu = Platform.get().getMtu(socket);
+    in = new BufferedInputStream(in, mtu);
+    out = new BufferedOutputStream(out, mtu);
+  }
+
+  /**
+   * Create an {@code SSLSocket} and perform the TLS handshake and certificate
+   * validation.
+   */
+  private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException {
+    Platform platform = Platform.get();
+
+    // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
+    if (requiresTunnel()) {
+      makeTunnel(tunnelRequest);
+    }
+
+    // Create the wrapper over connected socket.
+    socket = route.address.sslSocketFactory
+        .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */);
+    SSLSocket sslSocket = (SSLSocket) socket;
+    if (route.modernTls) {
+      platform.enableTlsExtensions(sslSocket, route.address.uriHost);
+    } else {
+      platform.supportTlsIntolerantServer(sslSocket);
+    }
+
+    if (route.modernTls) {
+      platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
+    }
+
+    // Force handshake. This can throw!
+    sslSocket.startHandshake();
+
+    // Verify that the socket's certificates are acceptable for the target host.
+    if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) {
+      throw new IOException("Hostname '" + route.address.uriHost + "' was not verified");
+    }
+
+    out = sslSocket.getOutputStream();
+    in = sslSocket.getInputStream();
+
+    byte[] selectedProtocol;
+    if (route.modernTls
+        && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
+      if (Arrays.equals(selectedProtocol, SPDY3)) {
+        sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
+        spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out)
+            .build();
+      } else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
+        throw new IOException(
+            "Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1"));
+      }
+    }
+  }
+
+  /** Returns true if {@link #connect} has been attempted on this connection. */
+  public boolean isConnected() {
+    return connected;
+  }
+
+  @Override public void close() throws IOException {
+    socket.close();
+  }
+
+  /** Returns the route used by this connection. */
+  public Route getRoute() {
+    return route;
+  }
+
+  /**
+   * Returns the socket that this connection uses, or null if the connection
+   * is not currently connected.
+   */
+  public Socket getSocket() {
+    return socket;
+  }
+
+  /** Returns true if this connection is alive. */
+  public boolean isAlive() {
+    return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
+  }
+
+  public void resetIdleStartTime() {
+    if (spdyConnection != null) {
+      throw new IllegalStateException("spdyConnection != null");
+    }
+    this.idleStartTimeNs = System.nanoTime();
+  }
+
+  /** Returns true if this connection is idle. */
+  public boolean isIdle() {
+    return spdyConnection == null || spdyConnection.isIdle();
+  }
+
+  /**
+   * Returns true if this connection has been idle for longer than
+   * {@code keepAliveDurationNs}.
+   */
+  public boolean isExpired(long keepAliveDurationNs) {
+    return isIdle() && System.nanoTime() - getIdleStartTimeNs() > keepAliveDurationNs;
+  }
+
+  /**
+   * Returns the time in ns when this connection became idle. Undefined if
+   * this connection is not idle.
+   */
+  public long getIdleStartTimeNs() {
+    return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs();
+  }
+
+  /** Returns the transport appropriate for this connection. */
+  public Object newTransport(HttpEngine httpEngine) throws IOException {
+    return (spdyConnection != null) ? new SpdyTransport(httpEngine, spdyConnection)
+        : new HttpTransport(httpEngine, out, in);
+  }
+
+  /**
+   * Returns true if this is a SPDY connection. Such connections can be used
+   * in multiple HTTP requests simultaneously.
+   */
+  public boolean isSpdy() {
+    return spdyConnection != null;
+  }
+
+  public SpdyConnection getSpdyConnection() {
+    return spdyConnection;
+  }
+
+  /**
+   * Returns the minor HTTP version that should be used for future requests on
+   * this connection. Either 0 for HTTP/1.0, or 1 for HTTP/1.1. The default
+   * value is 1 for new connections.
+   */
+  public int getHttpMinorVersion() {
+    return httpMinorVersion;
+  }
+
+  public void setHttpMinorVersion(int httpMinorVersion) {
+    this.httpMinorVersion = httpMinorVersion;
+  }
+
+  /**
+   * Returns 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 boolean requiresTunnel() {
+    return route.address.sslSocketFactory != null && route.proxy.type() == Proxy.Type.HTTP;
+  }
+
+  /**
+   * 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(TunnelRequest tunnelRequest) throws IOException {
+    RawHeaders requestHeaders = tunnelRequest.getRequestHeaders();
+    while (true) {
+      out.write(requestHeaders.toBytes());
+      RawHeaders responseHeaders = RawHeaders.fromBytes(in);
+
+      switch (responseHeaders.getResponseCode()) {
+        case HTTP_OK:
+          return;
+        case HTTP_PROXY_AUTH:
+          requestHeaders = new RawHeaders(requestHeaders);
+          URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/");
+          boolean credentialsFound = HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH,
+              responseHeaders, requestHeaders, route.proxy, url);
+          if (credentialsFound) {
+            continue;
+          } else {
+            throw new IOException("Failed to authenticate with proxy");
+          }
+        default:
+          throw new IOException(
+              "Unexpected response code for CONNECT: " + responseHeaders.getResponseCode());
+      }
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/ConnectionPool.java b/framework/src/com/squareup/okhttp/ConnectionPool.java
new file mode 100644
index 0000000..933bd73
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/ConnectionPool.java
@@ -0,0 +1,273 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Util;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+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;
+
+/**
+ * Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP
+ * requests that share the same {@link com.squareup.okhttp.Address} may share a
+ * {@link com.squareup.okhttp.Connection}. This class implements the policy of
+ * which connections to keep open for future use.
+ *
+ * <p>The {@link #getDefault() system-wide default} uses system properties for
+ * tuning parameters:
+ * <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 idle connections to
+ *         each to keep in the pool. Default is 5.
+ *     <li>{@code http.keepAliveDuration} Time in milliseconds to keep the
+ *         connection alive in the pool before closing it. Default is 5 minutes.
+ *         This property isn't used by {@code HttpURLConnection}.
+ * </ul>
+ *
+ * <p>The default instance <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.
+ */
+public class ConnectionPool {
+  private static final int MAX_CONNECTIONS_TO_CLEANUP = 2;
+  private static final long DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min
+
+  private static final ConnectionPool systemDefault;
+
+  static {
+    String keepAlive = System.getProperty("http.keepAlive");
+    String keepAliveDuration = System.getProperty("http.keepAliveDuration");
+    String maxIdleConnections = System.getProperty("http.maxConnections");
+    long keepAliveDurationMs = keepAliveDuration != null ? Long.parseLong(keepAliveDuration)
+        : DEFAULT_KEEP_ALIVE_DURATION_MS;
+    if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
+      systemDefault = new ConnectionPool(0, keepAliveDurationMs);
+    } else if (maxIdleConnections != null) {
+      systemDefault = new ConnectionPool(Integer.parseInt(maxIdleConnections), keepAliveDurationMs);
+    } else {
+      systemDefault = new ConnectionPool(5, keepAliveDurationMs);
+    }
+  }
+
+  /** The maximum number of idle connections for each address. */
+  private final int maxIdleConnections;
+  private final long keepAliveDurationNs;
+
+  private final LinkedList<Connection> connections = new LinkedList<Connection>();
+
+  /** We use a single background thread to cleanup expired connections. */
+  private final ExecutorService executorService =
+      new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+  private final Callable<Void> connectionsCleanupCallable = new Callable<Void>() {
+    @Override public Void call() throws Exception {
+      List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
+      int idleConnectionCount = 0;
+      synchronized (ConnectionPool.this) {
+        for (ListIterator<Connection> i = connections.listIterator(connections.size());
+            i.hasPrevious(); ) {
+          Connection connection = i.previous();
+          if (!connection.isAlive() || connection.isExpired(keepAliveDurationNs)) {
+            i.remove();
+            expiredConnections.add(connection);
+            if (expiredConnections.size() == MAX_CONNECTIONS_TO_CLEANUP) break;
+          } else if (connection.isIdle()) {
+            idleConnectionCount++;
+          }
+        }
+
+        for (ListIterator<Connection> i = connections.listIterator(connections.size());
+            i.hasPrevious() && idleConnectionCount > maxIdleConnections; ) {
+          Connection connection = i.previous();
+          if (connection.isIdle()) {
+            expiredConnections.add(connection);
+            i.remove();
+            --idleConnectionCount;
+          }
+        }
+      }
+      for (Connection expiredConnection : expiredConnections) {
+        Util.closeQuietly(expiredConnection);
+      }
+      return null;
+    }
+  };
+
+  public ConnectionPool(int maxIdleConnections, long keepAliveDurationMs) {
+    this.maxIdleConnections = maxIdleConnections;
+    this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000;
+  }
+
+  /**
+   * Returns a snapshot of the connections in this pool, ordered from newest to
+   * oldest. Waits for the cleanup callable to run if it is currently scheduled.
+   */
+  List<Connection> getConnections() {
+    waitForCleanupCallableToRun();
+    synchronized (this) {
+      return new ArrayList<Connection>(connections);
+    }
+  }
+
+  /**
+   * Blocks until the executor service has processed all currently enqueued
+   * jobs.
+   */
+  private void waitForCleanupCallableToRun() {
+    try {
+      executorService.submit(new Runnable() {
+        @Override public void run() {
+        }
+      }).get();
+    } catch (Exception e) {
+      throw new AssertionError();
+    }
+  }
+
+  public static ConnectionPool getDefault() {
+    return systemDefault;
+  }
+
+  /** Returns total number of connections in the pool. */
+  public synchronized int getConnectionCount() {
+    return connections.size();
+  }
+
+  /** Returns total number of spdy connections in the pool. */
+  public synchronized int getSpdyConnectionCount() {
+    int total = 0;
+    for (Connection connection : connections) {
+      if (connection.isSpdy()) total++;
+    }
+    return total;
+  }
+
+  /** Returns total number of http connections in the pool. */
+  public synchronized int getHttpConnectionCount() {
+    int total = 0;
+    for (Connection connection : connections) {
+      if (!connection.isSpdy()) total++;
+    }
+    return total;
+  }
+
+  /** Returns a recycled connection to {@code address}, or null if no such connection exists. */
+  public synchronized Connection get(Address address) {
+    Connection foundConnection = null;
+    for (ListIterator<Connection> i = connections.listIterator(connections.size());
+        i.hasPrevious(); ) {
+      Connection connection = i.previous();
+      if (!connection.getRoute().getAddress().equals(address)
+          || !connection.isAlive()
+          || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
+        continue;
+      }
+      i.remove();
+      if (!connection.isSpdy()) {
+        try {
+          Platform.get().tagSocket(connection.getSocket());
+        } catch (SocketException e) {
+          Util.closeQuietly(connection);
+          // When unable to tag, skip recycling and close
+          Platform.get().logW("Unable to tagSocket(): " + e);
+          continue;
+        }
+      }
+      foundConnection = connection;
+      break;
+    }
+
+    if (foundConnection != null && foundConnection.isSpdy()) {
+      connections.addFirst(foundConnection); // Add it back after iteration.
+    }
+
+    executorService.submit(connectionsCleanupCallable);
+    return foundConnection;
+  }
+
+  /**
+   * Gives {@code connection} to the pool. The pool may store the connection,
+   * or close it, as its policy describes.
+   *
+   * <p>It is an error to use {@code connection} after calling this method.
+   */
+  public void recycle(Connection connection) {
+    executorService.submit(connectionsCleanupCallable);
+
+    if (connection.isSpdy()) {
+      return;
+    }
+
+    if (!connection.isAlive()) {
+      Util.closeQuietly(connection);
+      return;
+    }
+
+    try {
+      Platform.get().untagSocket(connection.getSocket());
+    } catch (SocketException e) {
+      // When unable to remove tagging, skip recycling and close.
+      Platform.get().logW("Unable to untagSocket(): " + e);
+      Util.closeQuietly(connection);
+      return;
+    }
+
+    synchronized (this) {
+      connections.addFirst(connection);
+      connection.resetIdleStartTime();
+    }
+  }
+
+  /**
+   * Shares the SPDY connection with the pool. Callers to this method may
+   * continue to use {@code connection}.
+   */
+  public void maybeShare(Connection connection) {
+    executorService.submit(connectionsCleanupCallable);
+    if (!connection.isSpdy()) {
+      // Only SPDY connections are sharable.
+      return;
+    }
+    if (connection.isAlive()) {
+      synchronized (this) {
+        connections.addFirst(connection);
+      }
+    }
+  }
+
+  /** Close and remove all connections in the pool. */
+  public void evictAll() {
+    List<Connection> connections;
+    synchronized (this) {
+      connections = new ArrayList<Connection>(this.connections);
+      this.connections.clear();
+    }
+
+    for (Connection connection : connections) {
+      Util.closeQuietly(connection);
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/HttpResponseCache.java b/framework/src/com/squareup/okhttp/HttpResponseCache.java
new file mode 100644
index 0000000..a6d380a
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/HttpResponseCache.java
@@ -0,0 +1,693 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.Base64;
+import com.squareup.okhttp.internal.DiskLruCache;
+import com.squareup.okhttp.internal.StrictLineReader;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.HttpEngine;
+import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
+import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
+import com.squareup.okhttp.internal.http.OkResponseCache;
+import com.squareup.okhttp.internal.http.RawHeaders;
+import com.squareup.okhttp.internal.http.ResponseHeaders;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FilterInputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.ResponseCache;
+import java.net.SecureCacheResponse;
+import java.net.URI;
+import java.net.URLConnection;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+import static com.squareup.okhttp.internal.Util.US_ASCII;
+import static com.squareup.okhttp.internal.Util.UTF_8;
+
+/**
+ * Caches HTTP and HTTPS responses to the filesystem so they may be reused,
+ * saving time and bandwidth.
+ *
+ * <h3>Cache Optimization</h3>
+ * To measure cache effectiveness, this class tracks three statistics:
+ * <ul>
+ *     <li><strong>{@link #getRequestCount() Request Count:}</strong> the number
+ *         of HTTP requests issued since this cache was created.
+ *     <li><strong>{@link #getNetworkCount() Network Count:}</strong> the
+ *         number of those requests that required network use.
+ *     <li><strong>{@link #getHitCount() Hit Count:}</strong> the number of
+ *         those requests whose responses were served by the cache.
+ * </ul>
+ * Sometimes a request will result in a conditional cache hit. If the cache
+ * contains a stale copy of the response, the client will issue a conditional
+ * {@code GET}. The server will then send either the updated response if it has
+ * changed, or a short 'not modified' response if the client's copy is still
+ * valid. Such responses increment both the network count and hit count.
+ *
+ * <p>The best way to improve the cache hit rate is by configuring the web
+ * server to return cacheable responses. Although this client honors all <a
+ * href="http://www.ietf.org/rfc/rfc2616.txt">HTTP/1.1 (RFC 2068)</a> cache
+ * headers, it doesn't cache partial responses.
+ *
+ * <h3>Force a Network Response</h3>
+ * In some situations, such as after a user clicks a 'refresh' button, it may be
+ * necessary to skip the cache, and fetch data directly from the server. To force
+ * a full refresh, add the {@code no-cache} directive: <pre>   {@code
+ *         connection.addRequestProperty("Cache-Control", "no-cache");
+ * }</pre>
+ * If it is only necessary to force a cached response to be validated by the
+ * server, use the more efficient {@code max-age=0} instead: <pre>   {@code
+ *         connection.addRequestProperty("Cache-Control", "max-age=0");
+ * }</pre>
+ *
+ * <h3>Force a Cache Response</h3>
+ * Sometimes you'll want to show resources if they are available immediately,
+ * but not otherwise. This can be used so your application can show
+ * <i>something</i> while waiting for the latest data to be downloaded. To
+ * restrict a request to locally-cached resources, add the {@code
+ * only-if-cached} directive: <pre>   {@code
+ *     try {
+ *         connection.addRequestProperty("Cache-Control", "only-if-cached");
+ *         InputStream cached = connection.getInputStream();
+ *         // the resource was cached! show it
+ *     } catch (FileNotFoundException e) {
+ *         // the resource was not cached
+ *     }
+ * }</pre>
+ * This technique works even better in situations where a stale response is
+ * better than no response. To permit stale cached responses, use the {@code
+ * max-stale} directive with the maximum staleness in seconds: <pre>   {@code
+ *         int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
+ *         connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);
+ * }</pre>
+ */
+public final class HttpResponseCache extends ResponseCache {
+  private static final char[] DIGITS =
+      { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+  // TODO: add APIs to iterate the cache?
+  private static final int VERSION = 201105;
+  private static final int ENTRY_METADATA = 0;
+  private static final int ENTRY_BODY = 1;
+  private static final int ENTRY_COUNT = 2;
+
+  private final DiskLruCache cache;
+
+  /* read and write statistics, all guarded by 'this' */
+  private int writeSuccessCount;
+  private int writeAbortCount;
+  private int networkCount;
+  private int hitCount;
+  private int requestCount;
+
+  /**
+   * Although this class only exposes the limited ResponseCache API, it
+   * implements the full OkResponseCache interface. This field is used as a
+   * package private handle to the complete implementation. It delegates to
+   * public and private members of this type.
+   */
+  final OkResponseCache okResponseCache = new OkResponseCache() {
+    @Override public CacheResponse get(URI uri, String requestMethod,
+        Map<String, List<String>> requestHeaders) throws IOException {
+      return HttpResponseCache.this.get(uri, requestMethod, requestHeaders);
+    }
+
+    @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+      return HttpResponseCache.this.put(uri, connection);
+    }
+
+    @Override public void update(
+        CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException {
+      HttpResponseCache.this.update(conditionalCacheHit, connection);
+    }
+
+    @Override public void trackConditionalCacheHit() {
+      HttpResponseCache.this.trackConditionalCacheHit();
+    }
+
+    @Override public void trackResponse(ResponseSource source) {
+      HttpResponseCache.this.trackResponse(source);
+    }
+  };
+
+  public HttpResponseCache(File directory, long maxSize) throws IOException {
+    cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize);
+  }
+
+  private String uriToKey(URI uri) {
+    try {
+      MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+      byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8"));
+      return bytesToHexString(md5bytes);
+    } catch (NoSuchAlgorithmException e) {
+      throw new AssertionError(e);
+    } catch (UnsupportedEncodingException e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  private static String bytesToHexString(byte[] bytes) {
+    char[] digits = DIGITS;
+    char[] buf = new char[bytes.length * 2];
+    int c = 0;
+    for (byte b : bytes) {
+      buf[c++] = digits[(b >> 4) & 0xf];
+      buf[c++] = digits[b & 0xf];
+    }
+    return new String(buf);
+  }
+
+  @Override public CacheResponse get(URI uri, String requestMethod,
+      Map<String, List<String>> requestHeaders) {
+    String key = uriToKey(uri);
+    DiskLruCache.Snapshot snapshot;
+    Entry entry;
+    try {
+      snapshot = cache.get(key);
+      if (snapshot == null) {
+        return null;
+      }
+      entry = new Entry(snapshot.getInputStream(ENTRY_METADATA));
+    } catch (IOException e) {
+      // Give up because the cache cannot be read.
+      return null;
+    }
+
+    if (!entry.matches(uri, requestMethod, requestHeaders)) {
+      snapshot.close();
+      return null;
+    }
+
+    return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot)
+        : new EntryCacheResponse(entry, snapshot);
+  }
+
+  @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+    if (!(urlConnection instanceof HttpURLConnection)) {
+      return null;
+    }
+
+    HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
+    String requestMethod = httpConnection.getRequestMethod();
+    String key = uriToKey(uri);
+
+    if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
+        "DELETE")) {
+      try {
+        cache.remove(key);
+      } catch (IOException ignored) {
+        // The cache cannot be written.
+      }
+      return null;
+    } else if (!requestMethod.equals("GET")) {
+      // Don't cache non-GET responses. We're technically allowed to cache
+      // HEAD requests and some POST requests, but the complexity of doing
+      // so is high and the benefit is low.
+      return null;
+    }
+
+    HttpEngine httpEngine = getHttpEngine(httpConnection);
+    if (httpEngine == null) {
+      // Don't cache unless the HTTP implementation is ours.
+      return null;
+    }
+
+    ResponseHeaders response = httpEngine.getResponseHeaders();
+    if (response.hasVaryAll()) {
+      return null;
+    }
+
+    RawHeaders varyHeaders =
+        httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
+    Entry entry = new Entry(uri, varyHeaders, httpConnection);
+    DiskLruCache.Editor editor = null;
+    try {
+      editor = cache.edit(key);
+      if (editor == null) {
+        return null;
+      }
+      entry.writeTo(editor);
+      return new CacheRequestImpl(editor);
+    } catch (IOException e) {
+      abortQuietly(editor);
+      return null;
+    }
+  }
+
+  private void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
+      throws IOException {
+    HttpEngine httpEngine = getHttpEngine(httpConnection);
+    URI uri = httpEngine.getUri();
+    ResponseHeaders response = httpEngine.getResponseHeaders();
+    RawHeaders varyHeaders =
+        httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
+    Entry entry = new Entry(uri, varyHeaders, httpConnection);
+    DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse)
+        ? ((EntryCacheResponse) conditionalCacheHit).snapshot
+        : ((EntrySecureCacheResponse) conditionalCacheHit).snapshot;
+    DiskLruCache.Editor editor = null;
+    try {
+      editor = snapshot.edit(); // returns null if snapshot is not current
+      if (editor != null) {
+        entry.writeTo(editor);
+        editor.commit();
+      }
+    } catch (IOException e) {
+      abortQuietly(editor);
+    }
+  }
+
+  private void abortQuietly(DiskLruCache.Editor editor) {
+    // Give up because the cache cannot be written.
+    try {
+      if (editor != null) {
+        editor.abort();
+      }
+    } catch (IOException ignored) {
+    }
+  }
+
+  private HttpEngine getHttpEngine(URLConnection httpConnection) {
+    if (httpConnection instanceof HttpURLConnectionImpl) {
+      return ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
+    } else if (httpConnection instanceof HttpsURLConnectionImpl) {
+      return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine();
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   * Closes the cache and deletes all of its stored values. This will delete
+   * all files in the cache directory including files that weren't created by
+   * the cache.
+   */
+  public void delete() throws IOException {
+    cache.delete();
+  }
+
+  public synchronized int getWriteAbortCount() {
+    return writeAbortCount;
+  }
+
+  public synchronized int getWriteSuccessCount() {
+    return writeSuccessCount;
+  }
+
+  private synchronized void trackResponse(ResponseSource source) {
+    requestCount++;
+
+    switch (source) {
+      case CACHE:
+        hitCount++;
+        break;
+      case CONDITIONAL_CACHE:
+      case NETWORK:
+        networkCount++;
+        break;
+    }
+  }
+
+  private synchronized void trackConditionalCacheHit() {
+    hitCount++;
+  }
+
+  public synchronized int getNetworkCount() {
+    return networkCount;
+  }
+
+  public synchronized int getHitCount() {
+    return hitCount;
+  }
+
+  public synchronized int getRequestCount() {
+    return requestCount;
+  }
+
+  private final class CacheRequestImpl extends CacheRequest {
+    private final DiskLruCache.Editor editor;
+    private OutputStream cacheOut;
+    private boolean done;
+    private OutputStream body;
+
+    public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
+      this.editor = editor;
+      this.cacheOut = editor.newOutputStream(ENTRY_BODY);
+      this.body = new FilterOutputStream(cacheOut) {
+        @Override public void close() throws IOException {
+          synchronized (HttpResponseCache.this) {
+            if (done) {
+              return;
+            }
+            done = true;
+            writeSuccessCount++;
+          }
+          super.close();
+          editor.commit();
+        }
+
+        @Override
+        public void write(byte[] buffer, int offset, int length) throws IOException {
+          // Since we don't override "write(int oneByte)", we can write directly to "out"
+          // and avoid the inefficient implementation from the FilterOutputStream.
+          out.write(buffer, offset, length);
+        }
+      };
+    }
+
+    @Override public void abort() {
+      synchronized (HttpResponseCache.this) {
+        if (done) {
+          return;
+        }
+        done = true;
+        writeAbortCount++;
+      }
+      Util.closeQuietly(cacheOut);
+      try {
+        editor.abort();
+      } catch (IOException ignored) {
+      }
+    }
+
+    @Override public OutputStream getBody() throws IOException {
+      return body;
+    }
+  }
+
+  private static final class Entry {
+    private final String uri;
+    private final RawHeaders varyHeaders;
+    private final String requestMethod;
+    private final RawHeaders responseHeaders;
+    private final String cipherSuite;
+    private final Certificate[] peerCertificates;
+    private final Certificate[] localCertificates;
+
+    /**
+     * Reads an entry from an input stream. A typical entry looks like this:
+     * <pre>{@code
+     *   http://google.com/foo
+     *   GET
+     *   2
+     *   Accept-Language: fr-CA
+     *   Accept-Charset: UTF-8
+     *   HTTP/1.1 200 OK
+     *   3
+     *   Content-Type: image/png
+     *   Content-Length: 100
+     *   Cache-Control: max-age=600
+     * }</pre>
+     *
+     * <p>A typical HTTPS file looks like this:
+     * <pre>{@code
+     *   https://google.com/foo
+     *   GET
+     *   2
+     *   Accept-Language: fr-CA
+     *   Accept-Charset: UTF-8
+     *   HTTP/1.1 200 OK
+     *   3
+     *   Content-Type: image/png
+     *   Content-Length: 100
+     *   Cache-Control: max-age=600
+     *
+     *   AES_256_WITH_MD5
+     *   2
+     *   base64-encoded peerCertificate[0]
+     *   base64-encoded peerCertificate[1]
+     *   -1
+     * }</pre>
+     * The file is newline separated. The first two lines are the URL and
+     * the request method. Next is the number of HTTP Vary request header
+     * lines, followed by those lines.
+     *
+     * <p>Next is the response status line, followed by the number of HTTP
+     * response header lines, followed by those lines.
+     *
+     * <p>HTTPS responses also contain SSL session information. This begins
+     * with a blank line, and then a line containing the cipher suite. Next
+     * is the length of the peer certificate chain. These certificates are
+     * base64-encoded and appear each on their own line. The next line
+     * contains the length of the local certificate chain. These
+     * certificates are also base64-encoded and appear each on their own
+     * line. A length of -1 is used to encode a null array.
+     */
+    public Entry(InputStream in) throws IOException {
+      try {
+        StrictLineReader reader = new StrictLineReader(in, US_ASCII);
+        uri = reader.readLine();
+        requestMethod = reader.readLine();
+        varyHeaders = new RawHeaders();
+        int varyRequestHeaderLineCount = reader.readInt();
+        for (int i = 0; i < varyRequestHeaderLineCount; i++) {
+          varyHeaders.addLine(reader.readLine());
+        }
+
+        responseHeaders = new RawHeaders();
+        responseHeaders.setStatusLine(reader.readLine());
+        int responseHeaderLineCount = reader.readInt();
+        for (int i = 0; i < responseHeaderLineCount; i++) {
+          responseHeaders.addLine(reader.readLine());
+        }
+
+        if (isHttps()) {
+          String blank = reader.readLine();
+          if (blank.length() > 0) {
+            throw new IOException("expected \"\" but was \"" + blank + "\"");
+          }
+          cipherSuite = reader.readLine();
+          peerCertificates = readCertArray(reader);
+          localCertificates = readCertArray(reader);
+        } else {
+          cipherSuite = null;
+          peerCertificates = null;
+          localCertificates = null;
+        }
+      } finally {
+        in.close();
+      }
+    }
+
+    public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection)
+        throws IOException {
+      this.uri = uri.toString();
+      this.varyHeaders = varyHeaders;
+      this.requestMethod = httpConnection.getRequestMethod();
+      this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true);
+
+      if (isHttps()) {
+        HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
+        cipherSuite = httpsConnection.getCipherSuite();
+        Certificate[] peerCertificatesNonFinal = null;
+        try {
+          peerCertificatesNonFinal = httpsConnection.getServerCertificates();
+        } catch (SSLPeerUnverifiedException ignored) {
+        }
+        peerCertificates = peerCertificatesNonFinal;
+        localCertificates = httpsConnection.getLocalCertificates();
+      } else {
+        cipherSuite = null;
+        peerCertificates = null;
+        localCertificates = null;
+      }
+    }
+
+    public void writeTo(DiskLruCache.Editor editor) throws IOException {
+      OutputStream out = editor.newOutputStream(ENTRY_METADATA);
+      Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+
+      writer.write(uri + '\n');
+      writer.write(requestMethod + '\n');
+      writer.write(Integer.toString(varyHeaders.length()) + '\n');
+      for (int i = 0; i < varyHeaders.length(); i++) {
+        writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n');
+      }
+
+      writer.write(responseHeaders.getStatusLine() + '\n');
+      writer.write(Integer.toString(responseHeaders.length()) + '\n');
+      for (int i = 0; i < responseHeaders.length(); i++) {
+        writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n');
+      }
+
+      if (isHttps()) {
+        writer.write('\n');
+        writer.write(cipherSuite + '\n');
+        writeCertArray(writer, peerCertificates);
+        writeCertArray(writer, localCertificates);
+      }
+      writer.close();
+    }
+
+    private boolean isHttps() {
+      return uri.startsWith("https://");
+    }
+
+    private Certificate[] readCertArray(StrictLineReader reader) throws IOException {
+      int length = reader.readInt();
+      if (length == -1) {
+        return null;
+      }
+      try {
+        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+        Certificate[] result = new Certificate[length];
+        for (int i = 0; i < result.length; i++) {
+          String line = reader.readLine();
+          byte[] bytes = Base64.decode(line.getBytes("US-ASCII"));
+          result[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(bytes));
+        }
+        return result;
+      } catch (CertificateException e) {
+        throw new IOException(e.getMessage());
+      }
+    }
+
+    private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {
+      if (certificates == null) {
+        writer.write("-1\n");
+        return;
+      }
+      try {
+        writer.write(Integer.toString(certificates.length) + '\n');
+        for (Certificate certificate : certificates) {
+          byte[] bytes = certificate.getEncoded();
+          String line = Base64.encode(bytes);
+          writer.write(line + '\n');
+        }
+      } catch (CertificateEncodingException e) {
+        throw new IOException(e.getMessage());
+      }
+    }
+
+    public boolean matches(URI uri, String requestMethod,
+        Map<String, List<String>> requestHeaders) {
+      return this.uri.equals(uri.toString())
+          && this.requestMethod.equals(requestMethod)
+          && new ResponseHeaders(uri, responseHeaders).varyMatches(varyHeaders.toMultimap(false),
+          requestHeaders);
+    }
+  }
+
+  /**
+   * Returns an input stream that reads the body of a snapshot, closing the
+   * snapshot when the stream is closed.
+   */
+  private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) {
+    return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) {
+      @Override public void close() throws IOException {
+        snapshot.close();
+        super.close();
+      }
+    };
+  }
+
+  static class EntryCacheResponse extends CacheResponse {
+    private final Entry entry;
+    private final DiskLruCache.Snapshot snapshot;
+    private final InputStream in;
+
+    public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
+      this.entry = entry;
+      this.snapshot = snapshot;
+      this.in = newBodyInputStream(snapshot);
+    }
+
+    @Override public Map<String, List<String>> getHeaders() {
+      return entry.responseHeaders.toMultimap(true);
+    }
+
+    @Override public InputStream getBody() {
+      return in;
+    }
+  }
+
+  static class EntrySecureCacheResponse extends SecureCacheResponse {
+    private final Entry entry;
+    private final DiskLruCache.Snapshot snapshot;
+    private final InputStream in;
+
+    public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
+      this.entry = entry;
+      this.snapshot = snapshot;
+      this.in = newBodyInputStream(snapshot);
+    }
+
+    @Override public Map<String, List<String>> getHeaders() {
+      return entry.responseHeaders.toMultimap(true);
+    }
+
+    @Override public InputStream getBody() {
+      return in;
+    }
+
+    @Override public String getCipherSuite() {
+      return entry.cipherSuite;
+    }
+
+    @Override public List<Certificate> getServerCertificateChain()
+        throws SSLPeerUnverifiedException {
+      if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
+        throw new SSLPeerUnverifiedException(null);
+      }
+      return Arrays.asList(entry.peerCertificates.clone());
+    }
+
+    @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+      if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
+        throw new SSLPeerUnverifiedException(null);
+      }
+      return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal();
+    }
+
+    @Override public List<Certificate> getLocalCertificateChain() {
+      if (entry.localCertificates == null || entry.localCertificates.length == 0) {
+        return null;
+      }
+      return Arrays.asList(entry.localCertificates.clone());
+    }
+
+    @Override public Principal getLocalPrincipal() {
+      if (entry.localCertificates == null || entry.localCertificates.length == 0) {
+        return null;
+      }
+      return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal();
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/OkHttpClient.java b/framework/src/com/squareup/okhttp/OkHttpClient.java
new file mode 100644
index 0000000..7834bd6
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/OkHttpClient.java
@@ -0,0 +1,216 @@
+/*
+ * 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 com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
+import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
+import com.squareup.okhttp.internal.http.OkResponseCache;
+import com.squareup.okhttp.internal.http.OkResponseCacheAdapter;
+import java.net.CookieHandler;
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.ResponseCache;
+import java.net.URL;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
+
+/** Configures and creates HTTP connections. */
+public final class OkHttpClient {
+  private Proxy proxy;
+  private Set<Route> failedRoutes = Collections.synchronizedSet(new LinkedHashSet<Route>());
+  private ProxySelector proxySelector;
+  private CookieHandler cookieHandler;
+  private ResponseCache responseCache;
+  private SSLSocketFactory sslSocketFactory;
+  private HostnameVerifier hostnameVerifier;
+  private ConnectionPool connectionPool;
+  private boolean followProtocolRedirects = true;
+
+  /**
+   * Sets the HTTP proxy that will be used by connections created by this
+   * client. This takes precedence over {@link #setProxySelector}, which is
+   * only honored when this proxy is null (which it is by default). To disable
+   * proxy use completely, call {@code setProxy(Proxy.NO_PROXY)}.
+   */
+  public OkHttpClient setProxy(Proxy proxy) {
+    this.proxy = proxy;
+    return this;
+  }
+
+  public Proxy getProxy() {
+    return proxy;
+  }
+
+  /**
+   * Sets the proxy selection policy to be used if no {@link #setProxy proxy}
+   * is specified explicitly. The proxy selector may return multiple proxies;
+   * in that case they will be tried in sequence until a successful connection
+   * is established.
+   *
+   * <p>If unset, the {@link ProxySelector#getDefault() system-wide default}
+   * proxy selector will be used.
+   */
+  public OkHttpClient setProxySelector(ProxySelector proxySelector) {
+    this.proxySelector = proxySelector;
+    return this;
+  }
+
+  public ProxySelector getProxySelector() {
+    return proxySelector;
+  }
+
+  /**
+   * Sets the cookie handler to be used to read outgoing cookies and write
+   * incoming cookies.
+   *
+   * <p>If unset, the {@link CookieHandler#getDefault() system-wide default}
+   * cookie handler will be used.
+   */
+  public OkHttpClient setCookieHandler(CookieHandler cookieHandler) {
+    this.cookieHandler = cookieHandler;
+    return this;
+  }
+
+  public CookieHandler getCookieHandler() {
+    return cookieHandler;
+  }
+
+  /**
+   * Sets the response cache to be used to read and write cached responses.
+   *
+   * <p>If unset, the {@link ResponseCache#getDefault() system-wide default}
+   * response cache will be used.
+   */
+  public OkHttpClient setResponseCache(ResponseCache responseCache) {
+    this.responseCache = responseCache;
+    return this;
+  }
+
+  public ResponseCache getResponseCache() {
+    return responseCache;
+  }
+
+  private OkResponseCache okResponseCache() {
+    if (responseCache instanceof HttpResponseCache) {
+      return ((HttpResponseCache) responseCache).okResponseCache;
+    } else if (responseCache != null) {
+      return new OkResponseCacheAdapter(responseCache);
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   * Sets the socket factory used to secure HTTPS connections.
+   *
+   * <p>If unset, the {@link HttpsURLConnection#getDefaultSSLSocketFactory()
+   * system-wide default} SSL socket factory will be used.
+   */
+  public OkHttpClient setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
+    this.sslSocketFactory = sslSocketFactory;
+    return this;
+  }
+
+  public SSLSocketFactory getSslSocketFactory() {
+    return sslSocketFactory;
+  }
+
+  /**
+   * Sets the verifier used to confirm that response certificates apply to
+   * requested hostnames for HTTPS connections.
+   *
+   * <p>If unset, the {@link HttpsURLConnection#getDefaultHostnameVerifier()
+   * system-wide default} hostname verifier will be used.
+   */
+  public OkHttpClient setHostnameVerifier(HostnameVerifier hostnameVerifier) {
+    this.hostnameVerifier = hostnameVerifier;
+    return this;
+  }
+
+  public HostnameVerifier getHostnameVerifier() {
+    return hostnameVerifier;
+  }
+
+  /**
+   * Sets the connection pool used to recycle HTTP and HTTPS connections.
+   *
+   * <p>If unset, the {@link ConnectionPool#getDefault() system-wide
+   * default} connection pool will be used.
+   */
+  public OkHttpClient setConnectionPool(ConnectionPool connectionPool) {
+    this.connectionPool = connectionPool;
+    return this;
+  }
+
+  public ConnectionPool getConnectionPool() {
+    return connectionPool;
+  }
+
+  /**
+   * Configure this client to follow redirects from HTTPS to HTTP and from HTTP
+   * to HTTPS.
+   *
+   * <p>If unset, protocol redirects will be followed. This is different than
+   * the built-in {@code HttpURLConnection}'s default.
+   */
+  public OkHttpClient setFollowProtocolRedirects(boolean followProtocolRedirects) {
+    this.followProtocolRedirects = followProtocolRedirects;
+    return this;
+  }
+
+  public boolean getFollowProtocolRedirects() {
+    return followProtocolRedirects;
+  }
+
+  public HttpURLConnection open(URL url) {
+    String protocol = url.getProtocol();
+    OkHttpClient copy = copyWithDefaults();
+    if (protocol.equals("http")) {
+      return new HttpURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
+    } else if (protocol.equals("https")) {
+      return new HttpsURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
+    } else {
+      throw new IllegalArgumentException("Unexpected protocol: " + protocol);
+    }
+  }
+
+  /**
+   * Returns a shallow copy of this OkHttpClient that uses the system-wide default for
+   * each field that hasn't been explicitly configured.
+   */
+  private OkHttpClient copyWithDefaults() {
+    OkHttpClient result = new OkHttpClient();
+    result.proxy = proxy;
+    result.failedRoutes = failedRoutes;
+    result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault();
+    result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault();
+    result.responseCache = responseCache != null ? responseCache : ResponseCache.getDefault();
+    result.sslSocketFactory = sslSocketFactory != null
+        ? sslSocketFactory
+        : HttpsURLConnection.getDefaultSSLSocketFactory();
+    result.hostnameVerifier = hostnameVerifier != null
+        ? hostnameVerifier
+        : HttpsURLConnection.getDefaultHostnameVerifier();
+    result.connectionPool = connectionPool != null ? connectionPool : ConnectionPool.getDefault();
+    result.followProtocolRedirects = followProtocolRedirects;
+    return result;
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/OkResponseCache.java b/framework/src/com/squareup/okhttp/OkResponseCache.java
new file mode 100644
index 0000000..b7e3801
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/OkResponseCache.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.squareup.okhttp;
+
+import java.io.IOException;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+
+/**
+ * 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.
+ */
+public interface OkResponseCache {
+
+  /** Track an HTTP response being satisfied by {@code source}. */
+  void trackResponse(ResponseSource source);
+
+  /** Track an conditional GET that was satisfied by this cache. */
+  void trackConditionalCacheHit();
+
+  /** Updates stored HTTP headers using a hit on a conditional GET. */
+  void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
+      throws IOException;
+}
diff --git a/framework/src/com/squareup/okhttp/ResponseSource.java b/framework/src/com/squareup/okhttp/ResponseSource.java
new file mode 100644
index 0000000..4eca172
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/ResponseSource.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.squareup.okhttp;
+
+/** The source of an HTTP response. */
+public enum ResponseSource {
+
+  /** The response was returned from the local cache. */
+  CACHE,
+
+  /**
+   * The response is available in the cache but must be validated with the
+   * network. The cache result will be used if it is still valid; otherwise
+   * the network's response will be used.
+   */
+  CONDITIONAL_CACHE,
+
+  /** The response was returned from the network. */
+  NETWORK;
+
+  public boolean requiresConnection() {
+    return this == CONDITIONAL_CACHE || this == NETWORK;
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/Route.java b/framework/src/com/squareup/okhttp/Route.java
new file mode 100644
index 0000000..6968c60
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/Route.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+/** Represents the route used by a connection to reach an endpoint. */
+public class Route {
+  final Address address;
+  final Proxy proxy;
+  final InetSocketAddress inetSocketAddress;
+  final boolean modernTls;
+
+  public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
+      boolean modernTls) {
+    if (address == null) throw new NullPointerException("address == null");
+    if (proxy == null) throw new NullPointerException("proxy == null");
+    if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null");
+    this.address = address;
+    this.proxy = proxy;
+    this.inetSocketAddress = inetSocketAddress;
+    this.modernTls = modernTls;
+  }
+
+  /** Returns the {@link Address} of this route. */
+  public Address getAddress() {
+    return address;
+  }
+
+  /**
+   * Returns the {@link Proxy} of this route.
+   *
+   * <strong>Warning:</strong> This may be different than the proxy returned
+   * by {@link #getAddress}! That is the proxy that the user asked to be
+   * connected to; this returns the proxy that they were actually connected
+   * to. The two may disagree when a proxy selector selects a different proxy
+   * for a connection.
+   */
+  public Proxy getProxy() {
+    return proxy;
+  }
+
+  /** Returns the {@link InetSocketAddress} of this route. */
+  public InetSocketAddress getSocketAddress() {
+    return inetSocketAddress;
+  }
+
+  /** Returns true if this route uses modern tls. */
+  public boolean isModernTls() {
+    return modernTls;
+  }
+
+  /** Returns a copy of this route with flipped tls mode. */
+  public Route flipTlsMode() {
+    return new Route(address, proxy, inetSocketAddress, !modernTls);
+  }
+
+  @Override public boolean equals(Object obj) {
+    if (obj instanceof Route) {
+      Route other = (Route) obj;
+      return (address.equals(other.address)
+          && proxy.equals(other.proxy)
+          && inetSocketAddress.equals(other.inetSocketAddress)
+          && modernTls == other.modernTls);
+    }
+    return false;
+  }
+
+  @Override public int hashCode() {
+    int result = 17;
+    result = 31 * result + address.hashCode();
+    result = 31 * result + proxy.hashCode();
+    result = 31 * result + inetSocketAddress.hashCode();
+    result = result + (modernTls ? (31 * result) : 0);
+    return result;
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/TunnelRequest.java b/framework/src/com/squareup/okhttp/TunnelRequest.java
new file mode 100644
index 0000000..5260b87
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/TunnelRequest.java
@@ -0,0 +1,75 @@
+/*
+ * 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 com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.http.RawHeaders;
+
+import static com.squareup.okhttp.internal.Util.getDefaultPort;
+
+/**
+ * Routing and authentication information sent to an HTTP proxy to create a
+ * HTTPS to an origin server. Everything in the tunnel request is sent
+ * unencrypted to the proxy server.
+ *
+ * <p>See <a href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817, Section
+ * 5.2</a>.
+ */
+public final class TunnelRequest {
+  final String host;
+  final int port;
+  final String userAgent;
+  final String proxyAuthorization;
+
+  /**
+   * @param host the origin server's hostname. Not null.
+   * @param port the origin server's port, like 80 or 443.
+   * @param userAgent the client's user-agent. Not null.
+   * @param proxyAuthorization proxy authorization, or null if the proxy is
+   * used without an authorization header.
+   */
+  public TunnelRequest(String host, int port, String userAgent, String proxyAuthorization) {
+    if (host == null) throw new NullPointerException("host == null");
+    if (userAgent == null) throw new NullPointerException("userAgent == null");
+    this.host = host;
+    this.port = port;
+    this.userAgent = userAgent;
+    this.proxyAuthorization = proxyAuthorization;
+  }
+
+  /**
+   * If we're creating a TLS tunnel, send only the minimum set of headers.
+   * This avoids sending potentially sensitive data like HTTP cookies to
+   * the proxy unencrypted.
+   */
+  RawHeaders getRequestHeaders() {
+    RawHeaders result = new RawHeaders();
+    result.setRequestLine("CONNECT " + host + ":" + port + " HTTP/1.1");
+
+    // Always set Host and User-Agent.
+    result.set("Host", port == getDefaultPort("https") ? host : (host + ":" + port));
+    result.set("User-Agent", userAgent);
+
+    // Copy over the Proxy-Authorization header if it exists.
+    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;
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java b/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java
new file mode 100644
index 0000000..78c9691
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okhttp.internal;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An output stream for an HTTP request body.
+ *
+ * <p>Since a single socket's output stream may be used to write multiple HTTP
+ * requests to the same server, subclasses should not close the socket stream.
+ */
+public abstract class AbstractOutputStream extends OutputStream {
+  protected boolean closed;
+
+  @Override public final void write(int data) throws IOException {
+    write(new byte[] { (byte) data });
+  }
+
+  protected final void checkNotClosed() throws IOException {
+    if (closed) {
+      throw new IOException("stream closed");
+    }
+  }
+
+  /** Returns true if this stream was closed locally. */
+  public boolean isClosed() {
+    return closed;
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/Base64.java b/framework/src/com/squareup/okhttp/internal/Base64.java
new file mode 100644
index 0000000..79cd020
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/Base64.java
@@ -0,0 +1,164 @@
+/*
+ *  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 com.squareup.okhttp.internal;
+
+import java.io.UnsupportedEncodingException;
+
+import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
+
+/**
+ * <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 EMPTY_BYTE_ARRAY;
+    }
+    // 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/framework/src/com/squareup/okhttp/internal/DiskLruCache.java b/framework/src/com/squareup/okhttp/internal/DiskLruCache.java
new file mode 100644
index 0000000..f7fcb1e
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/DiskLruCache.java
@@ -0,0 +1,926 @@
+/*
+ * 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 com.squareup.okhttp.internal;
+
+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.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.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A cache that uses a bounded amount of space on a filesystem. Each cache
+ * entry has a string key and a fixed number of values. Each key must match
+ * the regex <strong>[a-z0-9_-]{1,64}</strong>. Values are byte sequences,
+ * accessible as streams or files. Each value must be between {@code 0} and
+ * {@code Integer.MAX_VALUE} bytes in length.
+ *
+ * <p>The cache stores its data in a directory on the filesystem. This
+ * directory must be exclusive to the cache; the cache may delete or overwrite
+ * 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_TEMP = "journal.tmp";
+  static final String JOURNAL_FILE_BACKUP = "journal.bkp";
+  static final String MAGIC = "libcore.io.DiskLruCache";
+  static final String VERSION_1 = "1";
+  static final long ANY_SEQUENCE_NUMBER = -1;
+  static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}");
+  private static final String CLEAN = "CLEAN";
+  private static final String DIRTY = "DIRTY";
+  private static final String REMOVE = "REMOVE";
+  private static final String READ = "READ";
+
+    /*
+     * This cache uses a journal file named "journal". A typical journal file
+     * looks like this:
+     *     libcore.io.DiskLruCache
+     *     1
+     *     100
+     *     2
+     *
+     *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
+     *     DIRTY 335c4c6028171cfddfbaae1a9c313c52
+     *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
+     *     REMOVE 335c4c6028171cfddfbaae1a9c313c52
+     *     DIRTY 1ab96a171faeeee38496d8b330771a7a
+     *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
+     *     READ 335c4c6028171cfddfbaae1a9c313c52
+     *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
+     *
+     * The first five lines of the journal form its header. They are the
+     * constant string "libcore.io.DiskLruCache", the disk cache's version,
+     * the application's version, the value count, and a blank line.
+     *
+     * Each of the subsequent lines in the file is a record of the state of a
+     * cache entry. Each line contains space-separated values: a state, a key,
+     * and optional state-specific values.
+     *   o DIRTY lines track that an entry is actively being created or updated.
+     *     Every successful DIRTY action should be followed by a CLEAN or REMOVE
+     *     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
+     *     temporary files may need to be deleted.
+     *   o CLEAN lines track a cache entry that has been successfully published
+     *     and may be read. A publish line is followed by the lengths of each of
+     *     its values.
+     *   o READ lines track accesses for LRU.
+     *   o REMOVE lines track entries that have been deleted.
+     *
+     * The journal file is appended to as cache operations occur. The journal may
+     * occasionally be compacted by dropping redundant lines. A temporary file named
+     * "journal.tmp" will be used during compaction; that file should be deleted if
+     * it exists when the cache is opened.
+     */
+
+  private final File directory;
+  private final File journalFile;
+  private final File journalFileTmp;
+  private final File journalFileBackup;
+  private final int appVersion;
+  private 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. */
+  final ThreadPoolExecutor executorService =
+      new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+  private final Callable<Void> cleanupCallable = new Callable<Void>() {
+    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_TEMP);
+    this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
+    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 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");
+    }
+
+    // If a bkp file exists, use it instead.
+    File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
+    if (backupFile.exists()) {
+      File journalFile = new File(directory, JOURNAL_FILE);
+      // If journal file also exists just delete backup file.
+      if (journalFile.exists()) {
+        backupFile.delete();
+      } else {
+        renameTo(backupFile, journalFile, false);
+      }
+    }
+
+    // Prefer to pick up where we left off.
+    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+    if (cache.journalFile.exists()) {
+      try {
+        cache.readJournal();
+        cache.processJournal();
+        cache.journalWriter = new BufferedWriter(
+            new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
+        return cache;
+      } catch (IOException journalIsCorrupt) {
+        Platform.get().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 {
+    StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
+    try {
+      String magic = reader.readLine();
+      String version = reader.readLine();
+      String appVersionString = reader.readLine();
+      String valueCountString = reader.readLine();
+      String blank = reader.readLine();
+      if (!MAGIC.equals(magic)
+          || !VERSION_1.equals(version)
+          || !Integer.toString(appVersion).equals(appVersionString)
+          || !Integer.toString(valueCount).equals(valueCountString)
+          || !"".equals(blank)) {
+        throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+            + valueCountString + ", " + blank + "]");
+      }
+
+      int lineCount = 0;
+      while (true) {
+        try {
+          readJournalLine(reader.readLine());
+          lineCount++;
+        } catch (EOFException endOfJournal) {
+          break;
+        }
+      }
+      redundantOpCount = lineCount - lruEntries.size();
+    } finally {
+      Util.closeQuietly(reader);
+    }
+  }
+
+  private void readJournalLine(String line) throws IOException {
+    int firstSpace = line.indexOf(' ');
+    if (firstSpace == -1) {
+      throw new IOException("unexpected journal line: " + line);
+    }
+
+    int keyBegin = firstSpace + 1;
+    int secondSpace = line.indexOf(' ', keyBegin);
+    final String key;
+    if (secondSpace == -1) {
+      key = line.substring(keyBegin);
+      if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
+        lruEntries.remove(key);
+        return;
+      }
+    } else {
+      key = line.substring(keyBegin, secondSpace);
+    }
+
+    Entry entry = lruEntries.get(key);
+    if (entry == null) {
+      entry = new Entry(key);
+      lruEntries.put(key, entry);
+    }
+
+    if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
+      String[] parts = line.substring(secondSpace + 1).split(" ");
+      entry.readable = true;
+      entry.currentEditor = null;
+      entry.setLengths(parts);
+    } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
+      entry.currentEditor = new Editor(entry);
+    } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
+      // This work was already done by calling lruEntries.get().
+    } else {
+      throw new IOException("unexpected journal line: " + line);
+    }
+  }
+
+  /**
+   * 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 OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
+    try {
+      writer.write(MAGIC);
+      writer.write("\n");
+      writer.write(VERSION_1);
+      writer.write("\n");
+      writer.write(Integer.toString(appVersion));
+      writer.write("\n");
+      writer.write(Integer.toString(valueCount));
+      writer.write("\n");
+      writer.write("\n");
+
+      for (Entry entry : lruEntries.values()) {
+        if (entry.currentEditor != null) {
+          writer.write(DIRTY + ' ' + entry.key + '\n');
+        } else {
+          writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+        }
+      }
+    } finally {
+      writer.close();
+    }
+
+    if (journalFile.exists()) {
+      renameTo(journalFile, journalFileBackup, true);
+    }
+    renameTo(journalFileTmp, journalFile, false);
+    journalFileBackup.delete();
+
+    journalWriter = new BufferedWriter(
+        new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
+  }
+
+  private static void deleteIfExists(File file) throws IOException {
+    if (file.exists() && !file.delete()) {
+      throw new IOException();
+    }
+  }
+
+  private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
+    if (deleteDestination) {
+      deleteIfExists(to);
+    }
+    if (!from.renameTo(to)) {
+      throw new IOException();
+    }
+  }
+
+  /**
+   * 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!
+      for (int i = 0; i < valueCount; i++) {
+        if (ins[i] != null) {
+          Util.closeQuietly(ins[i]);
+        } else {
+          break;
+        }
+      }
+      return null;
+    }
+
+    redundantOpCount++;
+    journalWriter.append(READ + ' ' + key + '\n');
+    if (journalRebuildRequired()) {
+      executorService.submit(cleanupCallable);
+    }
+
+    return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
+  }
+
+  /**
+   * 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 getMaxSize() {
+    return maxSize;
+  }
+
+  /**
+   * Changes the maximum number of bytes the cache can store and queues a job
+   * to trim the existing store, if necessary.
+   */
+  public synchronized void setMaxSize(long maxSize) {
+    this.maxSize = maxSize;
+    executorService.submit(cleanupCallable);
+  }
+
+  /**
+   * Returns the number of bytes currently being used to store the values in
+   * this cache. This may be greater than the max size if a background
+   * deletion is pending.
+   */
+  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 (!editor.written[i]) {
+          editor.abort();
+          throw new IllegalStateException("Newly created entry didn't create value for index " + i);
+        }
+        if (!entry.getDirtyFile(i).exists()) {
+          editor.abort();
+          return;
+        }
+      }
+    }
+
+    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');
+    }
+    journalWriter.flush();
+
+    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();
+    Util.deleteContents(directory);
+  }
+
+  private void validateKey(String key) {
+    Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
+    if (!matcher.matches()) {
+      throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\"");
+    }
+  }
+
+  private static String inputStreamToString(InputStream in) throws IOException {
+    return Util.readFully(new InputStreamReader(in, Util.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 final long[] lengths;
+
+    private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
+      this.key = key;
+      this.sequenceNumber = sequenceNumber;
+      this.ins = ins;
+      this.lengths = lengths;
+    }
+
+    /**
+     * 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));
+    }
+
+    /** Returns the byte length of the value for {@code index}. */
+    public long getLength(int index) {
+      return lengths[index];
+    }
+
+    public void close() {
+      for (InputStream in : ins) {
+        Util.closeQuietly(in);
+      }
+    }
+  }
+
+  private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
+    @Override
+    public void write(int b) throws IOException {
+      // Eat all writes silently. Nom nom.
+    }
+  };
+
+  /** Edits the values for an entry. */
+  public final class Editor {
+    private final Entry entry;
+    private final boolean[] written;
+    private boolean hasErrors;
+    private boolean committed;
+
+    private Editor(Entry entry) {
+      this.entry = entry;
+      this.written = (entry.readable) ? null : new boolean[valueCount];
+    }
+
+    /**
+     * 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;
+        }
+        try {
+          return new FileInputStream(entry.getCleanFile(index));
+        } catch (FileNotFoundException e) {
+          return null;
+        }
+      }
+    }
+
+    /**
+     * 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();
+        }
+        if (!entry.readable) {
+          written[index] = true;
+        }
+        File dirtyFile = entry.getDirtyFile(index);
+        FileOutputStream outputStream;
+        try {
+          outputStream = new FileOutputStream(dirtyFile);
+        } catch (FileNotFoundException e) {
+          // Attempt to recreate the cache directory.
+          directory.mkdirs();
+          try {
+            outputStream = new FileOutputStream(dirtyFile);
+          } catch (FileNotFoundException e2) {
+            // We are unable to recover. Silently eat the writes.
+            return NULL_OUTPUT_STREAM;
+          }
+        }
+        return new FaultHidingOutputStream(outputStream);
+      }
+    }
+
+    /** 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), Util.UTF_8);
+        writer.write(value);
+      } finally {
+        Util.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);
+      }
+      committed = 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);
+    }
+
+    public void abortUnlessCommitted() {
+      if (!committed) {
+        try {
+          abort();
+        } catch (IOException ignored) {
+        }
+      }
+    }
+
+    private 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: " + java.util.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/framework/src/com/squareup/okhttp/internal/Dns.java b/framework/src/com/squareup/okhttp/internal/Dns.java
new file mode 100644
index 0000000..69b2d37
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/Dns.java
@@ -0,0 +1,33 @@
+/*
+ * 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 com.squareup.okhttp.internal;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Domain name service. Prefer this over {@link InetAddress#getAllByName} to
+ * make code more testable.
+ */
+public interface Dns {
+  Dns DEFAULT = new Dns() {
+    @Override public InetAddress[] getAllByName(String host) throws UnknownHostException {
+      return InetAddress.getAllByName(host);
+    }
+  };
+
+  InetAddress[] getAllByName(String host) throws UnknownHostException;
+}
diff --git a/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java b/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java
new file mode 100644
index 0000000..c32b27a
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+
+/**
+ * An output stream wrapper that recovers from failures in the underlying stream
+ * by replacing it with another stream. This class buffers a fixed amount of
+ * data under the assumption that failures occur early in a stream's life.
+ * If a failure occurs after the buffer has been exhausted, no recovery is
+ * attempted.
+ *
+ * <p>Subclasses must override {@link #replacementStream} which will request a
+ * replacement stream each time an {@link IOException} is encountered on the
+ * current stream.
+ */
+public abstract class FaultRecoveringOutputStream extends AbstractOutputStream {
+  private final int maxReplayBufferLength;
+
+  /** Bytes to transmit on the replacement stream, or null if no recovery is possible. */
+  private ByteArrayOutputStream replayBuffer;
+  private OutputStream out;
+
+  /**
+   * @param maxReplayBufferLength the maximum number of successfully written
+   *     bytes to buffer so they can be replayed in the event of an error.
+   *     Failure recoveries are not possible once this limit has been exceeded.
+   */
+  public FaultRecoveringOutputStream(int maxReplayBufferLength, OutputStream out) {
+    if (maxReplayBufferLength < 0) throw new IllegalArgumentException();
+    this.maxReplayBufferLength = maxReplayBufferLength;
+    this.replayBuffer = new ByteArrayOutputStream(maxReplayBufferLength);
+    this.out = out;
+  }
+
+  @Override public final void write(byte[] buffer, int offset, int count) throws IOException {
+    if (closed) throw new IOException("stream closed");
+    checkOffsetAndCount(buffer.length, offset, count);
+
+    while (true) {
+      try {
+        out.write(buffer, offset, count);
+
+        if (replayBuffer != null) {
+          if (count + replayBuffer.size() > maxReplayBufferLength) {
+            // Failure recovery is no longer possible once we overflow the replay buffer.
+            replayBuffer = null;
+          } else {
+            // Remember the written bytes to the replay buffer.
+            replayBuffer.write(buffer, offset, count);
+          }
+        }
+        return;
+      } catch (IOException e) {
+        if (!recover(e)) throw e;
+      }
+    }
+  }
+
+  @Override public final void flush() throws IOException {
+    if (closed) {
+      return; // don't throw; this stream might have been closed on the caller's behalf
+    }
+    while (true) {
+      try {
+        out.flush();
+        return;
+      } catch (IOException e) {
+        if (!recover(e)) throw e;
+      }
+    }
+  }
+
+  @Override public final void close() throws IOException {
+    if (closed) {
+      return;
+    }
+    while (true) {
+      try {
+        out.close();
+        closed = true;
+        return;
+      } catch (IOException e) {
+        if (!recover(e)) throw e;
+      }
+    }
+  }
+
+  /**
+   * Attempt to replace {@code out} with another equivalent stream. Returns true
+   * if a suitable replacement stream was found.
+   */
+  private boolean recover(IOException e) {
+    if (replayBuffer == null) {
+      return false; // Can't recover because we've dropped data that we would need to replay.
+    }
+
+    while (true) {
+      OutputStream replacementStream = null;
+      try {
+        replacementStream = replacementStream(e);
+        if (replacementStream == null) {
+          return false;
+        }
+        replaceStream(replacementStream);
+        return true;
+      } catch (IOException replacementStreamFailure) {
+        // The replacement was also broken. Loop to ask for another replacement.
+        Util.closeQuietly(replacementStream);
+        e = replacementStreamFailure;
+      }
+    }
+  }
+
+  /**
+   * Returns true if errors in the underlying stream can currently be recovered.
+   */
+  public boolean isRecoverable() {
+    return replayBuffer != null;
+  }
+
+  /**
+   * Replaces the current output stream with {@code replacementStream}, writing
+   * any replay bytes to it if they exist. The current output stream is closed.
+   */
+  public final void replaceStream(OutputStream replacementStream) throws IOException {
+    if (!isRecoverable()) {
+      throw new IllegalStateException();
+    }
+    if (this.out == replacementStream) {
+      return; // Don't replace a stream with itself.
+    }
+    replayBuffer.writeTo(replacementStream);
+    Util.closeQuietly(out);
+    out = replacementStream;
+  }
+
+  /**
+   * Returns a replacement output stream to recover from {@code e} thrown by the
+   * previous stream. Returns a new OutputStream if recovery was successful, in
+   * which case all previously-written data will be replayed. Returns null if
+   * the failure cannot be recovered.
+   */
+  protected abstract OutputStream replacementStream(IOException e) throws IOException;
+}
diff --git a/framework/src/com/squareup/okhttp/internal/NamedRunnable.java b/framework/src/com/squareup/okhttp/internal/NamedRunnable.java
new file mode 100644
index 0000000..ce430b2
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/NamedRunnable.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okhttp.internal;
+
+/**
+ * Runnable implementation which always sets its thread name.
+ */
+public abstract class NamedRunnable implements Runnable {
+  private String name;
+
+  public NamedRunnable(String name) {
+    this.name = name;
+  }
+
+  @Override public final void run() {
+    String oldName = Thread.currentThread().getName();
+    Thread.currentThread().setName(name);
+    try {
+      execute();
+    } finally {
+      Thread.currentThread().setName(oldName);
+    }
+  }
+
+  protected abstract void execute();
+}
diff --git a/framework/src/com/squareup/okhttp/internal/Platform.java b/framework/src/com/squareup/okhttp/internal/Platform.java
new file mode 100644
index 0000000..6b4ac34
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/Platform.java
@@ -0,0 +1,389 @@
+/*
+ * 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.
+ */
+package com.squareup.okhttp.internal;
+
+import com.squareup.okhttp.OkHttpClient;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.net.NetworkInterface;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+import javax.net.ssl.SSLSocket;
+
+/**
+ * Access to Platform-specific features necessary for SPDY and advanced TLS.
+ *
+ * <h3>SPDY</h3>
+ * SPDY requires a TLS extension called NPN (Next Protocol Negotiation) that's
+ * available in Android 4.1+ and OpenJDK 7+ (with the npn-boot extension). It
+ * also requires a recent version of {@code DeflaterOutputStream} that is
+ * public API in Java 7 and callable via reflection in Android 4.1+.
+ */
+public class Platform {
+  private static final Platform PLATFORM = findPlatform();
+
+  private Constructor<DeflaterOutputStream> deflaterConstructor;
+
+  public static Platform get() {
+    return PLATFORM;
+  }
+
+  public void logW(String warning) {
+    System.out.println(warning);
+  }
+
+  public void tagSocket(Socket socket) throws SocketException {
+  }
+
+  public void untagSocket(Socket socket) throws SocketException {
+  }
+
+  public URI toUriLenient(URL url) throws URISyntaxException {
+    return url.toURI(); // this isn't as good as the built-in toUriLenient
+  }
+
+  /**
+   * Attempt a TLS connection with useful extensions enabled. This mode
+   * supports more features, but is less likely to be compatible with older
+   * HTTPS servers.
+   */
+  public void enableTlsExtensions(SSLSocket socket, String uriHost) {
+  }
+
+  /**
+   * Attempt a secure connection with basic functionality to maximize
+   * compatibility. Currently this uses SSL 3.0.
+   */
+  public void supportTlsIntolerantServer(SSLSocket socket) {
+    socket.setEnabledProtocols(new String[] {"SSLv3"});
+  }
+
+  /** Returns the negotiated protocol, or null if no protocol was negotiated. */
+  public byte[] getNpnSelectedProtocol(SSLSocket socket) {
+    return null;
+  }
+
+  /**
+   * Sets client-supported protocols on a socket to send to a server. The
+   * protocols are only sent if the socket implementation supports NPN.
+   */
+  public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
+  }
+
+  /**
+   * Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
+   * value blocks. This throws an {@link UnsupportedOperationException} on
+   * Java 6 and earlier where there is no built-in API to do SYNC_FLUSH.
+   */
+  public OutputStream newDeflaterOutputStream(OutputStream out, Deflater deflater,
+      boolean syncFlush) {
+    try {
+      Constructor<DeflaterOutputStream> constructor = deflaterConstructor;
+      if (constructor == null) {
+        constructor = deflaterConstructor = DeflaterOutputStream.class.getConstructor(
+            OutputStream.class, Deflater.class, boolean.class);
+      }
+      return constructor.newInstance(out, deflater, syncFlush);
+    } catch (NoSuchMethodException e) {
+      throw new UnsupportedOperationException("Cannot SPDY; no SYNC_FLUSH available");
+    } catch (InvocationTargetException e) {
+      throw e.getCause() instanceof RuntimeException ? (RuntimeException) e.getCause()
+          : new RuntimeException(e.getCause());
+    } catch (InstantiationException e) {
+      throw new RuntimeException(e);
+    } catch (IllegalAccessException e) {
+      throw new AssertionError();
+    }
+  }
+
+  /**
+   * Returns the maximum transmission unit of the network interface used by
+   * {@code socket}, or a reasonable default if this platform doesn't expose the
+   * MTU to the application layer.
+   *
+   * <p>The returned value should only be used as an optimization; such as to
+   * size buffers efficiently.
+   */
+  public int getMtu(Socket socket) throws IOException {
+    return 1400; // Smaller than 1500 to leave room for headers on interfaces like PPPoE.
+  }
+
+  /** Attempt to match the host runtime to a capable Platform implementation. */
+  private static Platform findPlatform() {
+    Method getMtu;
+    try {
+      getMtu = NetworkInterface.class.getMethod("getMTU");
+    } catch (NoSuchMethodException e) {
+      return new Platform(); // No Java 1.6 APIs. It's either Java 1.5, Android 2.2 or earlier.
+    }
+
+    // Attempt to find Android 2.3+ APIs.
+    Class<?> openSslSocketClass;
+    Method setUseSessionTickets;
+    Method setHostname;
+    try {
+      openSslSocketClass = Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
+      setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class);
+      setHostname = openSslSocketClass.getMethod("setHostname", String.class);
+
+      // Attempt to find Android 4.1+ APIs.
+      try {
+        Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
+        Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
+        return new Android41(getMtu, openSslSocketClass, setUseSessionTickets, setHostname,
+            setNpnProtocols, getNpnSelectedProtocol);
+      } catch (NoSuchMethodException ignored) {
+        return new Android23(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
+      }
+    } catch (ClassNotFoundException ignored) {
+      // This isn't an Android runtime.
+    } catch (NoSuchMethodException ignored) {
+      // This isn't Android 2.3 or better.
+    }
+
+    // Attempt to find the Jetty's NPN extension for OpenJDK.
+    try {
+      String npnClassName = "org.eclipse.jetty.npn.NextProtoNego";
+      Class<?> nextProtoNegoClass = Class.forName(npnClassName);
+      Class<?> providerClass = Class.forName(npnClassName + "$Provider");
+      Class<?> clientProviderClass = Class.forName(npnClassName + "$ClientProvider");
+      Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
+      Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
+      Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
+      return new JdkWithJettyNpnPlatform(getMtu, putMethod, getMethod, clientProviderClass,
+          serverProviderClass);
+    } catch (ClassNotFoundException ignored) {
+      // NPN isn't on the classpath.
+    } catch (NoSuchMethodException ignored) {
+      // The NPN version isn't what we expect.
+    }
+
+    return getMtu != null ? new Java5(getMtu) : new Platform();
+  }
+
+  private static class Java5 extends Platform {
+    private final Method getMtu;
+
+    private Java5(Method getMtu) {
+      this.getMtu = getMtu;
+    }
+
+    @Override public int getMtu(Socket socket) throws IOException {
+      try {
+        NetworkInterface networkInterface = NetworkInterface.getByInetAddress(
+            socket.getLocalAddress());
+        return (Integer) getMtu.invoke(networkInterface);
+      } catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      } catch (InvocationTargetException e) {
+        if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
+        throw new RuntimeException(e.getCause());
+      }
+    }
+  }
+
+  /**
+   * Android version 2.3 and newer support TLS session tickets and server name
+   * indication (SNI).
+   */
+  private static class Android23 extends Java5 {
+    protected final Class<?> openSslSocketClass;
+    private final Method setUseSessionTickets;
+    private final Method setHostname;
+
+    private Android23(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
+        Method setHostname) {
+      super(getMtu);
+      this.openSslSocketClass = openSslSocketClass;
+      this.setUseSessionTickets = setUseSessionTickets;
+      this.setHostname = setHostname;
+    }
+
+    @Override public void enableTlsExtensions(SSLSocket socket, String uriHost) {
+      super.enableTlsExtensions(socket, uriHost);
+      if (openSslSocketClass.isInstance(socket)) {
+        // This is Android: use reflection on OpenSslSocketImpl.
+        try {
+          setUseSessionTickets.invoke(socket, true);
+          setHostname.invoke(socket, uriHost);
+        } catch (InvocationTargetException e) {
+          throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+          throw new AssertionError(e);
+        }
+      }
+    }
+  }
+
+  /** Android version 4.1 and newer support NPN. */
+  private static class Android41 extends Android23 {
+    private final Method setNpnProtocols;
+    private final Method getNpnSelectedProtocol;
+
+    private Android41(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
+        Method setHostname, Method setNpnProtocols, Method getNpnSelectedProtocol) {
+      super(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
+      this.setNpnProtocols = setNpnProtocols;
+      this.getNpnSelectedProtocol = getNpnSelectedProtocol;
+    }
+
+    @Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
+      if (!openSslSocketClass.isInstance(socket)) {
+        return;
+      }
+      try {
+        setNpnProtocols.invoke(socket, new Object[] {npnProtocols});
+      } catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      } catch (InvocationTargetException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
+      if (!openSslSocketClass.isInstance(socket)) {
+        return null;
+      }
+      try {
+        return (byte[]) getNpnSelectedProtocol.invoke(socket);
+      } catch (InvocationTargetException e) {
+        throw new RuntimeException(e);
+      } catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      }
+    }
+  }
+
+  /**
+   * OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class
+   * path.
+   */
+  private static class JdkWithJettyNpnPlatform extends Java5 {
+    private final Method getMethod;
+    private final Method putMethod;
+    private final Class<?> clientProviderClass;
+    private final Class<?> serverProviderClass;
+
+    public JdkWithJettyNpnPlatform(Method getMtu, Method putMethod, Method getMethod,
+        Class<?> clientProviderClass, Class<?> serverProviderClass) {
+      super(getMtu);
+      this.putMethod = putMethod;
+      this.getMethod = getMethod;
+      this.clientProviderClass = clientProviderClass;
+      this.serverProviderClass = serverProviderClass;
+    }
+
+    @Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
+      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;
+        }
+        Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(),
+            new Class[] {clientProviderClass, serverProviderClass},
+            new JettyNpnProvider(strings));
+        putMethod.invoke(null, socket, provider);
+      } catch (UnsupportedEncodingException e) {
+        throw new AssertionError(e);
+      } catch (InvocationTargetException e) {
+        throw new AssertionError(e);
+      } catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      }
+    }
+
+    @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
+      try {
+        JettyNpnProvider provider =
+            (JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
+        if (!provider.unsupported && provider.selected == null) {
+          Logger logger = Logger.getLogger(OkHttpClient.class.getName());
+          logger.log(Level.INFO,
+              "NPN callback dropped so SPDY is disabled. " + "Is npn-boot on the boot class path?");
+          return null;
+        }
+        return provider.unsupported ? null : provider.selected.getBytes("US-ASCII");
+      } catch (UnsupportedEncodingException e) {
+        throw new AssertionError();
+      } catch (InvocationTargetException e) {
+        throw new AssertionError();
+      } catch (IllegalAccessException e) {
+        throw new AssertionError();
+      }
+    }
+  }
+
+  /**
+   * Handle the methods of NextProtoNego's ClientProvider and ServerProvider
+   * without a compile-time dependency on those interfaces.
+   */
+  private static class JettyNpnProvider implements InvocationHandler {
+    private final List<String> protocols;
+    private boolean unsupported;
+    private String selected;
+
+    public JettyNpnProvider(List<String> protocols) {
+      this.protocols = protocols;
+    }
+
+    @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+      String methodName = method.getName();
+      Class<?> returnType = method.getReturnType();
+      if (args == null) {
+        args = Util.EMPTY_STRING_ARRAY;
+      }
+      if (methodName.equals("supports") && boolean.class == returnType) {
+        return true;
+      } else if (methodName.equals("unsupported") && void.class == returnType) {
+        this.unsupported = true;
+        return null;
+      } else if (methodName.equals("protocols") && args.length == 0) {
+        return protocols;
+      } else if (methodName.equals("selectProtocol")
+          && String.class == returnType
+          && args.length == 1
+          && (args[0] == null || args[0] instanceof List)) {
+        // TODO: use OpenSSL's algorithm which uses both lists
+        List<?> serverProtocols = (List) args[0];
+        this.selected = protocols.get(0);
+        return selected;
+      } else if (methodName.equals("protocolSelected") && args.length == 1) {
+        this.selected = (String) args[0];
+        return null;
+      } else {
+        return method.invoke(this, args);
+      }
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/StrictLineReader.java b/framework/src/com/squareup/okhttp/internal/StrictLineReader.java
new file mode 100644
index 0000000..3ddc693
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/StrictLineReader.java
@@ -0,0 +1,208 @@
+/*
+ * 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 com.squareup.okhttp.internal;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+/**
+ * Buffers input from an {@link InputStream} for reading lines.
+ *
+ * <p>This class is used for buffered reading of lines. For purposes of this class, a line ends with
+ * "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated line at
+ * end of input is invalid and will be ignored, the caller may use {@code hasUnterminatedLine()}
+ * to detect it after catching the {@code EOFException}.
+ *
+ * <p>This class is intended for reading input that strictly consists of lines, such as line-based
+ * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
+ * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
+ * end-of-input reporting and a more restrictive definition of a line.
+ *
+ * <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
+ * and 10, respectively, and the representation of no other character contains these values.
+ * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
+ * The default charset is US_ASCII.
+ */
+public class StrictLineReader implements Closeable {
+  private static final byte CR = (byte) '\r';
+  private static final byte LF = (byte) '\n';
+
+  private final InputStream in;
+  private final Charset charset;
+
+  /*
+   * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
+   * and the data in the range [pos, end) is buffered for reading. At end of input, if there is
+   * an unterminated line, we set end == -1, otherwise end == pos. If the underlying
+   * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
+   */
+  private byte[] buf;
+  private int pos;
+  private int end;
+
+  /**
+   * Constructs a new {@code LineReader} with the specified charset and the default capacity.
+   *
+   * @param in the {@code InputStream} to read data from.
+   * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
+   *     supported.
+   * @throws NullPointerException if {@code in} or {@code charset} is null.
+   * @throws IllegalArgumentException if the specified charset is not supported.
+   */
+  public StrictLineReader(InputStream in, Charset charset) {
+    this(in, 8192, charset);
+  }
+
+  /**
+   * Constructs a new {@code LineReader} with the specified capacity and charset.
+   *
+   * @param in the {@code InputStream} to read data from.
+   * @param capacity the capacity of the buffer.
+   * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
+   *     supported.
+   * @throws NullPointerException if {@code in} or {@code charset} is null.
+   * @throws IllegalArgumentException if {@code capacity} is negative or zero
+   *     or the specified charset is not supported.
+   */
+  public StrictLineReader(InputStream in, int capacity, Charset charset) {
+    if (in == null || charset == null) {
+      throw new NullPointerException();
+    }
+    if (capacity < 0) {
+      throw new IllegalArgumentException("capacity <= 0");
+    }
+    if (!(charset.equals(Util.US_ASCII))) {
+      throw new IllegalArgumentException("Unsupported encoding");
+    }
+
+    this.in = in;
+    this.charset = charset;
+    buf = new byte[capacity];
+  }
+
+  /**
+   * Closes the reader by closing the underlying {@code InputStream} and
+   * marking this reader as closed.
+   *
+   * @throws IOException for errors when closing the underlying {@code InputStream}.
+   */
+  public void close() throws IOException {
+    synchronized (in) {
+      if (buf != null) {
+        buf = null;
+        in.close();
+      }
+    }
+  }
+
+  /**
+   * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
+   * this end of line marker is not included in the result.
+   *
+   * @return the next line from the input.
+   * @throws IOException for underlying {@code InputStream} errors.
+   * @throws EOFException for the end of source stream.
+   */
+  public String readLine() throws IOException {
+    synchronized (in) {
+      if (buf == null) {
+        throw new IOException("LineReader is closed");
+      }
+
+      // Read more data if we are at the end of the buffered data.
+      // Though it's an error to read after an exception, we will let {@code fillBuf()}
+      // throw again if that happens; thus we need to handle end == -1 as well as end == pos.
+      if (pos >= end) {
+        fillBuf();
+      }
+      // Try to find LF in the buffered data and return the line if successful.
+      for (int i = pos; i != end; ++i) {
+        if (buf[i] == LF) {
+          int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
+          String res = new String(buf, pos, lineEnd - pos, charset.name());
+          pos = i + 1;
+          return res;
+        }
+      }
+
+      // Let's anticipate up to 80 characters on top of those already read.
+      ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
+        @Override
+        public String toString() {
+          int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
+          try {
+            return new String(buf, 0, length, charset.name());
+          } catch (UnsupportedEncodingException e) {
+            throw new AssertionError(e); // Since we control the charset this will never happen.
+          }
+        }
+      };
+
+      while (true) {
+        out.write(buf, pos, end - pos);
+        // Mark unterminated line in case fillBuf throws EOFException or IOException.
+        end = -1;
+        fillBuf();
+        // Try to find LF in the buffered data and return the line if successful.
+        for (int i = pos; i != end; ++i) {
+          if (buf[i] == LF) {
+            if (i != pos) {
+              out.write(buf, pos, i - pos);
+            }
+            pos = i + 1;
+            return out.toString();
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Read an {@code int} from a line containing its decimal representation.
+   *
+   * @return the value of the {@code int} from the next line.
+   * @throws IOException for underlying {@code InputStream} errors or conversion error.
+   * @throws EOFException for the end of source stream.
+   */
+  public int readInt() throws IOException {
+    String intString = readLine();
+    try {
+      return Integer.parseInt(intString);
+    } catch (NumberFormatException e) {
+      throw new IOException("expected an int but was \"" + intString + "\"");
+    }
+  }
+
+  /**
+   * Reads new input data into the buffer. Call only with pos == end or end == -1,
+   * depending on the desired outcome if the function throws.
+   */
+  private void fillBuf() throws IOException {
+    int result = in.read(buf, 0, buf.length);
+    if (result == -1) {
+      throw new EOFException();
+    }
+    pos = 0;
+    end = result;
+  }
+}
+
diff --git a/framework/src/com/squareup/okhttp/internal/Util.java b/framework/src/com/squareup/okhttp/internal/Util.java
new file mode 100644
index 0000000..290e5ea
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/Util.java
@@ -0,0 +1,327 @@
+/*
+ * 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 com.squareup.okhttp.internal;
+
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.net.Socket;
+import java.net.URI;
+import java.net.URL;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Junk drawer of utility methods. */
+public final class Util {
+  public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+  public static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+  /** 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 static AtomicReference<byte[]> skipBuffer = new AtomicReference<byte[]>();
+
+  private Util() {
+  }
+
+  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) {
+    return specifiedPort != -1 ? specifiedPort : getDefaultPort(scheme);
+  }
+
+  public static int getDefaultPort(String scheme) {
+    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 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);
+    }
+  }
+
+  /** 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));
+  }
+
+  /**
+   * Closes {@code closeable}, ignoring any checked exceptions. Does nothing
+   * if {@code closeable} is null.
+   */
+  public static void closeQuietly(Closeable closeable) {
+    if (closeable != null) {
+      try {
+        closeable.close();
+      } catch (RuntimeException rethrown) {
+        throw rethrown;
+      } catch (Exception ignored) {
+      }
+    }
+  }
+
+  /**
+   * Closes {@code socket}, ignoring any checked exceptions. Does nothing if
+   * {@code socket} is null.
+   */
+  public static void closeQuietly(Socket socket) {
+    if (socket != null) {
+      try {
+        socket.close();
+      } catch (RuntimeException rethrown) {
+        throw rethrown;
+      } catch (Exception ignored) {
+      }
+    }
+  }
+
+  /**
+   * Closes {@code a} and {@code b}. If either close fails, this completes
+   * the other close and rethrows the first encountered exception.
+   */
+  public static void closeAll(Closeable a, Closeable b) throws IOException {
+    Throwable thrown = null;
+    try {
+      a.close();
+    } catch (Throwable e) {
+      thrown = e;
+    }
+    try {
+      b.close();
+    } catch (Throwable e) {
+      if (thrown == null) thrown = e;
+    }
+    if (thrown == null) return;
+    if (thrown instanceof IOException) throw (IOException) thrown;
+    if (thrown instanceof RuntimeException) throw (RuntimeException) thrown;
+    if (thrown instanceof Error) throw (Error) thrown;
+    throw new AssertionError(thrown);
+  }
+
+  /**
+   * Deletes the contents of {@code dir}. Throws an IOException if any file
+   * could not be deleted, or if {@code dir} is not a readable directory.
+   */
+  public static void deleteContents(File dir) throws IOException {
+    File[] files = dir.listFiles();
+    if (files == null) {
+      throw new IOException("not a readable directory: " + dir);
+    }
+    for (File file : files) {
+      if (file.isDirectory()) {
+        deleteContents(file);
+      }
+      if (!file.delete()) {
+        throw new IOException("failed to delete file: " + file);
+      }
+    }
+  }
+
+  /**
+   * 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");
+    }
+    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 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/framework/src/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java b/framework/src/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java
new file mode 100644
index 0000000..187f3b6
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/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 com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheRequest;
+
+/**
+ * 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 Util.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 streamCancelled) throws IOException {
+    if (cacheRequest != null) {
+      cacheBody.close();
+    }
+    httpEngine.release(streamCancelled);
+  }
+
+  /**
+   * 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(true);
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java b/framework/src/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java
new file mode 100644
index 0000000..90675b0
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/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 com.squareup.okhttp.internal.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/framework/src/com/squareup/okhttp/internal/http/HeaderParser.java b/framework/src/com/squareup/okhttp/internal/http/HeaderParser.java
new file mode 100644
index 0000000..12e6409
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/HeaderParser.java
@@ -0,0 +1,112 @@
+/*
+ * 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 com.squareup.okhttp.internal.http;
+
+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);
+    }
+  }
+
+  /**
+   * 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.
+   */
+  public 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.
+   */
+  public 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/framework/src/com/squareup/okhttp/internal/http/HttpAuthenticator.java b/framework/src/com/squareup/okhttp/internal/http/HttpAuthenticator.java
new file mode 100644
index 0000000..4ccd12a
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpAuthenticator.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2012 Square, Inc.
+ * 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 com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.internal.Base64;
+import java.io.IOException;
+import java.net.Authenticator;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.PasswordAuthentication;
+import java.net.Proxy;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+
+/** Handles HTTP authentication headers from origin and proxy servers. */
+public final class HttpAuthenticator {
+  private HttpAuthenticator() {
+  }
+
+  /**
+   * 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.
+   */
+  public static boolean processAuthHeader(int responseCode, RawHeaders responseHeaders,
+      RawHeaders successorRequestHeaders, Proxy proxy, URL url) 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 = getCredentials(responseHeaders, challengeHeader, proxy, url);
+    if (credentials == null) {
+      return false; // Could not find credentials so end the 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 that may satisfy the challenge.
+   * Returns null if a challenge header was not provided or if credentials
+   * were not available.
+   */
+  private static String getCredentials(RawHeaders responseHeaders, String challengeHeader,
+      Proxy proxy, URL url) throws IOException {
+    List<Challenge> challenges = parseChallenges(responseHeaders, challengeHeader);
+    if (challenges.isEmpty()) {
+      return null;
+    }
+
+    for (Challenge challenge : challenges) {
+      // Use the global authenticator to get the password.
+      PasswordAuthentication auth;
+      if (responseHeaders.getResponseCode() == HTTP_PROXY_AUTH) {
+        InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
+        auth = Authenticator.requestPasswordAuthentication(proxyAddress.getHostName(),
+            getConnectToInetAddress(proxy, url), proxyAddress.getPort(), url.getProtocol(),
+            challenge.realm, challenge.scheme, url, Authenticator.RequestorType.PROXY);
+      } else {
+        auth = Authenticator.requestPasswordAuthentication(url.getHost(),
+            getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(), challenge.realm,
+            challenge.scheme, url, Authenticator.RequestorType.SERVER);
+      }
+      if (auth == null) {
+        continue;
+      }
+
+      // Use base64 to 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 static InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
+    return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
+        ? ((InetSocketAddress) proxy.address()).getAddress() : InetAddress.getByName(url.getHost());
+  }
+
+  /**
+   * Parse RFC 2617 challenges. This API is only interested in the scheme
+   * name and realm.
+   */
+  private 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 = HeaderParser.skipUntil(value, pos, " ");
+
+        String scheme = value.substring(tokenStart, pos).trim();
+        pos = HeaderParser.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 = HeaderParser.skipUntil(value, pos, "\"");
+        String realm = value.substring(realmStart, pos);
+        pos++; // Consume '"' close quote.
+        pos = HeaderParser.skipUntil(value, pos, ",");
+        pos++; // Consume ',' comma.
+        pos = HeaderParser.skipWhitespace(value, pos);
+        result.add(new Challenge(scheme, realm));
+      }
+    }
+    return result;
+  }
+
+  /** An RFC 2617 challenge. */
+  private static 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/framework/src/com/squareup/okhttp/internal/http/HttpDate.java b/framework/src/com/squareup/okhttp/internal/http/HttpDate.java
new file mode 100644
index 0000000..acb5fda
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpDate.java
@@ -0,0 +1,82 @@
+/*
+ * 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 com.squareup.okhttp.internal.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.
+ */
+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/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java b/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java
new file mode 100644
index 0000000..7a06dca
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -0,0 +1,664 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Address;
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.ResponseSource;
+import com.squareup.okhttp.TunnelRequest;
+import com.squareup.okhttp.internal.Dns;
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Util;
+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.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.UnknownHostException;
+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.HostnameVerifier;
+import javax.net.ssl.SSLSocketFactory;
+
+import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
+import static com.squareup.okhttp.internal.Util.getDefaultPort;
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
+
+/**
+ * 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 GATEWAY_TIMEOUT_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 504 Gateway Timeout"));
+      return result;
+    }
+    @Override public InputStream getBody() throws IOException {
+      return new ByteArrayInputStream(EMPTY_BYTE_ARRAY);
+    }
+  };
+  public static final int HTTP_CONTINUE = 100;
+
+  protected final HttpURLConnectionImpl policy;
+
+  protected final String method;
+
+  private ResponseSource responseSource;
+
+  protected Connection connection;
+  protected RouteSelector routeSelector;
+  private OutputStream requestBodyOut;
+
+  private Transport transport;
+
+  private InputStream responseTransferIn;
+  private InputStream responseBodyIn;
+
+  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,
+      Connection connection, RetryableOutputStream requestBodyOut) throws IOException {
+    this.policy = policy;
+    this.method = method;
+    this.connection = connection;
+    this.requestBodyOut = requestBodyOut;
+
+    try {
+      uri = Platform.get().toUriLenient(policy.getURL());
+    } catch (URISyntaxException e) {
+      throw new IOException(e.getMessage());
+    }
+
+    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 (policy.responseCache != null) {
+      policy.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 GATEWAY_TIMEOUT response instead, as specified
+    // by http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4.
+    if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) {
+      if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
+        Util.closeQuietly(cachedResponseBody);
+      }
+      this.responseSource = ResponseSource.CACHE;
+      this.cacheResponse = GATEWAY_TIMEOUT_RESPONSE;
+      RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(cacheResponse.getHeaders(), true);
+      setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody());
+    }
+
+    if (responseSource.requiresConnection()) {
+      sendSocketRequest();
+    } else if (connection != null) {
+      policy.connectionPool.recycle(connection);
+      policy.getFailedRoutes().remove(connection.getRoute());
+      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() || policy.responseCache == null) {
+      return;
+    }
+
+    CacheResponse candidate =
+        policy.responseCache.get(uri, method, requestHeaders.getHeaders().toMultimap(false));
+    if (candidate == null) {
+      return;
+    }
+
+    Map<String, List<String>> responseHeadersMap = candidate.getHeaders();
+    cachedResponseBody = candidate.getBody();
+    if (!acceptCacheResponseType(candidate)
+        || responseHeadersMap == null
+        || cachedResponseBody == null) {
+      Util.closeQuietly(cachedResponseBody);
+      return;
+    }
+
+    RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap, true);
+    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) {
+      Util.closeQuietly(cachedResponseBody);
+    } else {
+      throw new AssertionError();
+    }
+  }
+
+  private void sendSocketRequest() throws IOException {
+    if (connection == null) {
+      connect();
+    }
+
+    if (transport != null) {
+      throw new IllegalStateException();
+    }
+
+    transport = (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 final void connect() throws IOException {
+    if (connection != null) {
+      return;
+    }
+    if (routeSelector == null) {
+      String uriHost = uri.getHost();
+      if (uriHost == null) {
+        throw new UnknownHostException(uri.toString());
+      }
+      SSLSocketFactory sslSocketFactory = null;
+      HostnameVerifier hostnameVerifier = null;
+      if (uri.getScheme().equalsIgnoreCase("https")) {
+        sslSocketFactory = policy.sslSocketFactory;
+        hostnameVerifier = policy.hostnameVerifier;
+      }
+      Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory,
+          hostnameVerifier, policy.requestedProxy);
+      routeSelector = new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool,
+          Dns.DEFAULT, policy.getFailedRoutes());
+    }
+    connection = routeSelector.next();
+    if (!connection.isConnected()) {
+      connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig());
+      policy.connectionPool.maybeShare(connection);
+      policy.getFailedRoutes().remove(connection.getRoute());
+    }
+    connected(connection);
+    if (connection.getRoute().getProxy() != policy.requestedProxy) {
+      // Update the request line if the proxy changed; it may need a host name.
+      requestHeaders.getHeaders().setRequestLine(getRequestLine());
+    }
+  }
+
+  /**
+   * Called after a socket connection has been created or retrieved from the
+   * pool. Subclasses use this hook to get a reference to the TLS data.
+   */
+  protected void connected(Connection connection) {
+  }
+
+  /**
+   * Called immediately before the transport transmits HTTP request headers.
+   * This is used to observe the sent time should the request be cached.
+   */
+  public void writingRequestHeaders() {
+    if (sentRequestMillis != -1) {
+      throw new IllegalStateException();
+    }
+    sentRequestMillis = System.currentTimeMillis();
+  }
+
+  /**
+   * @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.equals("POST") || method.equals("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 Connection getConnection() {
+    return connection;
+  }
+
+  /**
+   * 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() || policy.responseCache == null) {
+      return;
+    }
+
+    // Should we cache this response for this request?
+    if (!responseHeaders.isCacheable(requestHeaders)) {
+      return;
+    }
+
+    // Offer this request to the cache.
+    cacheRequest = policy.responseCache.put(uri, policy.getHttpConnectionToCache());
+  }
+
+  /**
+   * 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) {
+      policy.connectionPool.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 streamCancelled) {
+    // If the response body comes from the cache, close it.
+    if (responseBodyIn == cachedResponseBody) {
+      Util.closeQuietly(responseBodyIn);
+    }
+
+    if (!connectionReleased && connection != null) {
+      connectionReleased = true;
+
+      if (transport == null || !transport.makeReusable(streamCancelled, requestBodyOut,
+          responseTransferIn)) {
+        Util.closeQuietly(connection);
+        connection = null;
+      } else if (automaticallyReleaseConnectionToPool) {
+        policy.connectionPool.recycle(connection);
+        connection = null;
+      }
+    }
+  }
+
+  private void initContentStream(InputStream transferStream) throws IOException {
+    responseTransferIn = transferStream;
+    if (transparentGzip && responseHeaders.isContentEncodingGzip()) {
+      // If the response was transparently gzipped, remove the gzip header field
+      // so clients don't double decompress. http://b/3009828
+      //
+      // Also remove the Content-Length in this case because it contains the
+      // length 528 of the gzipped response. This isn't terribly useful and is
+      // dangerous because 529 clients can query the content length, but not
+      // the content encoding.
+      responseHeaders.stripContentEncoding();
+      responseHeaders.stripContentLength();
+      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.equals("HEAD")) {
+      return false;
+    }
+
+    if ((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().setRequestLine(getRequestLine());
+
+    if (requestHeaders.getUserAgent() == null) {
+      requestHeaders.setUserAgent(getDefaultUserAgent());
+    }
+
+    if (requestHeaders.getHost() == null) {
+      requestHeaders.setHost(getOriginAddress(policy.getURL()));
+    }
+
+    if ((connection == null || connection.getHttpMinorVersion() != 0)
+        && requestHeaders.getConnection() == null) {
+      requestHeaders.setConnection("Keep-Alive");
+    }
+
+    if (requestHeaders.getAcceptEncoding() == null) {
+      transparentGzip = true;
+      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 = policy.cookieHandler;
+    if (cookieHandler != null) {
+      requestHeaders.addCookies(
+          cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false)));
+    }
+  }
+
+  /**
+   * 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.getHttpMinorVersion() != 0) ? "HTTP/1.1" : "HTTP/1.0";
+    return method + " " + requestString() + " " + protocol;
+  }
+
+  private String requestString() {
+    URL url = policy.getURL();
+    if (includeAuthorityInRequestLine()) {
+      return url.toString();
+    } else {
+      return requestPath(url);
+    }
+  }
+
+  /**
+   * Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never
+   * empty, even if the request URL is. Includes the query component if it
+   * exists.
+   */
+  public static String requestPath(URL url) {
+    String fileOnly = url.getFile();
+    if (fileOnly == null) {
+      return "/";
+    } else if (!fileOnly.startsWith("/")) {
+      return "/" + fileOnly;
+    } else {
+      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 connection == null
+        ? policy.usingProxy() // A proxy was requested.
+        : connection.getRoute().getProxy().type() == Proxy.Type.HTTP; // A proxy was selected.
+  }
+
+  public static String getDefaultUserAgent() {
+    String agent = System.getProperty("http.agent");
+    return agent != null ? agent : ("Java" + System.getProperty("java.version"));
+  }
+
+  public static String getOriginAddress(URL url) {
+    int port = url.getPort();
+    String result = url.getHost();
+    if (port > 0 && port != getDefaultPort(url.getProtocol())) {
+      result = result + ":" + port;
+    }
+    return result;
+  }
+
+  /**
+   * 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()) {
+      responseHeaders.setResponseSource(responseSource);
+      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());
+    responseHeaders.setResponseSource(responseSource);
+
+    if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
+      if (cachedResponseHeaders.validate(responseHeaders)) {
+        release(false);
+        ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
+        setResponse(combinedHeaders, cachedResponseBody);
+        policy.responseCache.trackConditionalCacheHit();
+        policy.responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
+        return;
+      } else {
+        Util.closeQuietly(cachedResponseBody);
+      }
+    }
+
+    if (hasResponseBody()) {
+      maybeCache(); // reentrant. this calls into user code which may call back into this!
+    }
+
+    initContentStream(transport.getTransferStream(cacheRequest));
+  }
+
+  protected TunnelRequest getTunnelConfig() {
+    return null;
+  }
+
+  public void receiveHeaders(RawHeaders headers) throws IOException {
+    CookieHandler cookieHandler = policy.cookieHandler;
+    if (cookieHandler != null) {
+      cookieHandler.put(uri, headers.toMultimap(true));
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpResponseCache.java b/framework/src/com/squareup/okhttp/internal/http/HttpResponseCache.java
new file mode 100644
index 0000000..8735166
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpResponseCache.java
@@ -0,0 +1,608 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.OkResponseCache;
+import com.squareup.okhttp.ResponseSource;
+import com.squareup.okhttp.internal.Base64;
+import com.squareup.okhttp.internal.DiskLruCache;
+import com.squareup.okhttp.internal.StrictLineReader;
+import com.squareup.okhttp.internal.Util;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FilterInputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.ResponseCache;
+import java.net.SecureCacheResponse;
+import java.net.URI;
+import java.net.URLConnection;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+import static com.squareup.okhttp.internal.Util.US_ASCII;
+import static com.squareup.okhttp.internal.Util.UTF_8;
+
+/**
+ * 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 OkResponseCache {
+  private static final char[] DIGITS =
+      { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+  // TODO: add APIs to iterate the cache?
+  private static final int VERSION = 201105;
+  private static final int ENTRY_METADATA = 0;
+  private static final int ENTRY_BODY = 1;
+  private static final int ENTRY_COUNT = 2;
+
+  private final DiskLruCache cache;
+
+  /* read and write statistics, all guarded by 'this' */
+  private int writeSuccessCount;
+  private int writeAbortCount;
+  private int networkCount;
+  private int hitCount;
+  private int requestCount;
+
+  public HttpResponseCache(File directory, long maxSize) throws IOException {
+    cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize);
+  }
+
+  private String uriToKey(URI uri) {
+    try {
+      MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+      byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8"));
+      return bytesToHexString(md5bytes);
+    } catch (NoSuchAlgorithmException e) {
+      throw new AssertionError(e);
+    } catch (UnsupportedEncodingException e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  private static String bytesToHexString(byte[] bytes) {
+    char[] digits = DIGITS;
+    char[] buf = new char[bytes.length * 2];
+    int c = 0;
+    for (byte b : bytes) {
+      buf[c++] = digits[(b >> 4) & 0xf];
+      buf[c++] = digits[b & 0xf];
+    }
+    return new String(buf);
+  }
+
+  @Override public CacheResponse get(URI uri, String requestMethod,
+      Map<String, List<String>> requestHeaders) {
+    String key = uriToKey(uri);
+    DiskLruCache.Snapshot snapshot;
+    Entry entry;
+    try {
+      snapshot = cache.get(key);
+      if (snapshot == null) {
+        return null;
+      }
+      entry = new Entry(snapshot.getInputStream(ENTRY_METADATA));
+    } catch (IOException e) {
+      // Give up because the cache cannot be read.
+      return null;
+    }
+
+    if (!entry.matches(uri, requestMethod, requestHeaders)) {
+      snapshot.close();
+      return null;
+    }
+
+    return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot)
+        : new EntryCacheResponse(entry, snapshot);
+  }
+
+  @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+    if (!(urlConnection instanceof HttpURLConnection)) {
+      return null;
+    }
+
+    HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
+    String requestMethod = httpConnection.getRequestMethod();
+    String key = uriToKey(uri);
+
+    if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
+        "DELETE")) {
+      try {
+        cache.remove(key);
+      } catch (IOException ignored) {
+        // The cache cannot be written.
+      }
+      return null;
+    } else if (!requestMethod.equals("GET")) {
+      // Don't cache non-GET responses. We're technically allowed to cache
+      // HEAD requests and some POST requests, but the complexity of doing
+      // so is high and the benefit is low.
+      return null;
+    }
+
+    HttpEngine httpEngine = getHttpEngine(httpConnection);
+    if (httpEngine == null) {
+      // Don't cache unless the HTTP implementation is ours.
+      return null;
+    }
+
+    ResponseHeaders response = httpEngine.getResponseHeaders();
+    if (response.hasVaryAll()) {
+      return null;
+    }
+
+    RawHeaders varyHeaders =
+        httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
+    Entry entry = new Entry(uri, varyHeaders, httpConnection);
+    DiskLruCache.Editor editor = null;
+    try {
+      editor = cache.edit(key);
+      if (editor == null) {
+        return null;
+      }
+      entry.writeTo(editor);
+      return new CacheRequestImpl(editor);
+    } catch (IOException e) {
+      abortQuietly(editor);
+      return null;
+    }
+  }
+
+  /**
+   * 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, HttpURLConnection httpConnection)
+      throws IOException {
+    HttpEngine httpEngine = getHttpEngine(httpConnection);
+    URI uri = httpEngine.getUri();
+    ResponseHeaders response = httpEngine.getResponseHeaders();
+    RawHeaders varyHeaders =
+        httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
+    Entry entry = new Entry(uri, varyHeaders, httpConnection);
+    DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse)
+        ? ((EntryCacheResponse) conditionalCacheHit).snapshot
+        : ((EntrySecureCacheResponse) conditionalCacheHit).snapshot;
+    DiskLruCache.Editor editor = null;
+    try {
+      editor = snapshot.edit(); // returns null if snapshot is not current
+      if (editor != null) {
+        entry.writeTo(editor);
+        editor.commit();
+      }
+    } catch (IOException e) {
+      abortQuietly(editor);
+    }
+  }
+
+  private void abortQuietly(DiskLruCache.Editor editor) {
+    // Give up because the cache cannot be written.
+    try {
+      if (editor != null) {
+        editor.abort();
+      }
+    } catch (IOException ignored) {
+    }
+  }
+
+  private HttpEngine getHttpEngine(URLConnection httpConnection) {
+    if (httpConnection instanceof HttpURLConnectionImpl) {
+      return ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
+    } else if (httpConnection instanceof HttpsURLConnectionImpl) {
+      return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine();
+    } else {
+      return null;
+    }
+  }
+
+  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 write(byte[] buffer, int offset, int length) throws IOException {
+          // Since we don't override "write(int oneByte)", we can write directly to "out"
+          // and avoid the inefficient implementation from the FilterOutputStream.
+          out.write(buffer, offset, length);
+        }
+      };
+    }
+
+    @Override public void abort() {
+      synchronized (HttpResponseCache.this) {
+        if (done) {
+          return;
+        }
+        done = true;
+        writeAbortCount++;
+      }
+      Util.closeQuietly(cacheOut);
+      try {
+        editor.abort();
+      } catch (IOException ignored) {
+      }
+    }
+
+    @Override public OutputStream getBody() throws IOException {
+      return body;
+    }
+  }
+
+  private static final class Entry {
+    private final String uri;
+    private final RawHeaders varyHeaders;
+    private final String requestMethod;
+    private final RawHeaders responseHeaders;
+    private final String cipherSuite;
+    private final Certificate[] peerCertificates;
+    private final Certificate[] localCertificates;
+
+    /**
+     * Reads an entry from an input stream. A typical entry looks like this:
+     * <pre>{@code
+     *   http://google.com/foo
+     *   GET
+     *   2
+     *   Accept-Language: fr-CA
+     *   Accept-Charset: UTF-8
+     *   HTTP/1.1 200 OK
+     *   3
+     *   Content-Type: image/png
+     *   Content-Length: 100
+     *   Cache-Control: max-age=600
+     * }</pre>
+     *
+     * <p>A typical HTTPS file looks like this:
+     * <pre>{@code
+     *   https://google.com/foo
+     *   GET
+     *   2
+     *   Accept-Language: fr-CA
+     *   Accept-Charset: UTF-8
+     *   HTTP/1.1 200 OK
+     *   3
+     *   Content-Type: image/png
+     *   Content-Length: 100
+     *   Cache-Control: max-age=600
+     *
+     *   AES_256_WITH_MD5
+     *   2
+     *   base64-encoded peerCertificate[0]
+     *   base64-encoded peerCertificate[1]
+     *   -1
+     * }</pre>
+     * The file is newline separated. The first two lines are the URL and
+     * the request method. Next is the number of HTTP Vary request header
+     * lines, followed by those lines.
+     *
+     * <p>Next is the response status line, followed by the number of HTTP
+     * response header lines, followed by those lines.
+     *
+     * <p>HTTPS responses also contain SSL session information. This begins
+     * with a blank line, and then a line containing the cipher suite. Next
+     * is the length of the peer certificate chain. These certificates are
+     * base64-encoded and appear each on their own line. The next line
+     * contains the length of the local certificate chain. These
+     * certificates are also base64-encoded and appear each on their own
+     * line. A length of -1 is used to encode a null array.
+     */
+    public Entry(InputStream in) throws IOException {
+      try {
+        StrictLineReader reader = new StrictLineReader(in, US_ASCII);
+        uri = reader.readLine();
+        requestMethod = reader.readLine();
+        varyHeaders = new RawHeaders();
+        int varyRequestHeaderLineCount = reader.readInt();
+        for (int i = 0; i < varyRequestHeaderLineCount; i++) {
+          varyHeaders.addLine(reader.readLine());
+        }
+
+        responseHeaders = new RawHeaders();
+        responseHeaders.setStatusLine(reader.readLine());
+        int responseHeaderLineCount = reader.readInt();
+        for (int i = 0; i < responseHeaderLineCount; i++) {
+          responseHeaders.addLine(reader.readLine());
+        }
+
+        if (isHttps()) {
+          String blank = reader.readLine();
+          if (!blank.isEmpty()) {
+            throw new IOException("expected \"\" but was \"" + blank + "\"");
+          }
+          cipherSuite = reader.readLine();
+          peerCertificates = readCertArray(reader);
+          localCertificates = readCertArray(reader);
+        } else {
+          cipherSuite = null;
+          peerCertificates = null;
+          localCertificates = null;
+        }
+      } finally {
+        in.close();
+      }
+    }
+
+    public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection)
+        throws IOException {
+      this.uri = uri.toString();
+      this.varyHeaders = varyHeaders;
+      this.requestMethod = httpConnection.getRequestMethod();
+      this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true);
+
+      if (isHttps()) {
+        HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
+        cipherSuite = httpsConnection.getCipherSuite();
+        Certificate[] peerCertificatesNonFinal = null;
+        try {
+          peerCertificatesNonFinal = httpsConnection.getServerCertificates();
+        } catch (SSLPeerUnverifiedException ignored) {
+        }
+        peerCertificates = peerCertificatesNonFinal;
+        localCertificates = httpsConnection.getLocalCertificates();
+      } else {
+        cipherSuite = null;
+        peerCertificates = null;
+        localCertificates = null;
+      }
+    }
+
+    public void writeTo(DiskLruCache.Editor editor) throws IOException {
+      OutputStream out = editor.newOutputStream(ENTRY_METADATA);
+      Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+
+      writer.write(uri + '\n');
+      writer.write(requestMethod + '\n');
+      writer.write(Integer.toString(varyHeaders.length()) + '\n');
+      for (int i = 0; i < varyHeaders.length(); i++) {
+        writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n');
+      }
+
+      writer.write(responseHeaders.getStatusLine() + '\n');
+      writer.write(Integer.toString(responseHeaders.length()) + '\n');
+      for (int i = 0; i < responseHeaders.length(); i++) {
+        writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n');
+      }
+
+      if (isHttps()) {
+        writer.write('\n');
+        writer.write(cipherSuite + '\n');
+        writeCertArray(writer, peerCertificates);
+        writeCertArray(writer, localCertificates);
+      }
+      writer.close();
+    }
+
+    private boolean isHttps() {
+      return uri.startsWith("https://");
+    }
+
+    private Certificate[] readCertArray(StrictLineReader reader) throws IOException {
+      int length = reader.readInt();
+      if (length == -1) {
+        return null;
+      }
+      try {
+        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+        Certificate[] result = new Certificate[length];
+        for (int i = 0; i < result.length; i++) {
+          String line = reader.readLine();
+          byte[] bytes = Base64.decode(line.getBytes("US-ASCII"));
+          result[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(bytes));
+        }
+        return result;
+      } catch (CertificateException e) {
+        throw new IOException(e);
+      }
+    }
+
+    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(false),
+          requestHeaders);
+    }
+  }
+
+  /**
+   * Returns an input stream that reads the body of a snapshot, closing the
+   * snapshot when the stream is closed.
+   */
+  private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) {
+    return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) {
+      @Override public void close() throws IOException {
+        snapshot.close();
+        super.close();
+      }
+    };
+  }
+
+  static class EntryCacheResponse extends CacheResponse {
+    private final Entry entry;
+    private final DiskLruCache.Snapshot snapshot;
+    private final InputStream in;
+
+    public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
+      this.entry = entry;
+      this.snapshot = snapshot;
+      this.in = newBodyInputStream(snapshot);
+    }
+
+    @Override public Map<String, List<String>> getHeaders() {
+      return entry.responseHeaders.toMultimap(true);
+    }
+
+    @Override public InputStream getBody() {
+      return in;
+    }
+  }
+
+  static class EntrySecureCacheResponse extends SecureCacheResponse {
+    private final Entry entry;
+    private final DiskLruCache.Snapshot snapshot;
+    private final InputStream in;
+
+    public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
+      this.entry = entry;
+      this.snapshot = snapshot;
+      this.in = newBodyInputStream(snapshot);
+    }
+
+    @Override public Map<String, List<String>> getHeaders() {
+      return entry.responseHeaders.toMultimap(true);
+    }
+
+    @Override public InputStream getBody() {
+      return in;
+    }
+
+    @Override public String getCipherSuite() {
+      return entry.cipherSuite;
+    }
+
+    @Override public List<Certificate> getServerCertificateChain()
+        throws SSLPeerUnverifiedException {
+      if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
+        throw new SSLPeerUnverifiedException(null);
+      }
+      return Arrays.asList(entry.peerCertificates.clone());
+    }
+
+    @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+      if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
+        throw new SSLPeerUnverifiedException(null);
+      }
+      return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal();
+    }
+
+    @Override public List<Certificate> getLocalCertificateChain() {
+      if (entry.localCertificates == null || entry.localCertificates.length == 0) {
+        return null;
+      }
+      return Arrays.asList(entry.localCertificates.clone());
+    }
+
+    @Override public Principal getLocalPrincipal() {
+      if (entry.localCertificates == null || entry.localCertificates.length == 0) {
+        return null;
+      }
+      return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal();
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java b/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java
new file mode 100644
index 0000000..f6d77b2
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java
@@ -0,0 +1,485 @@
+/*
+ * 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 com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.internal.AbstractOutputStream;
+import com.squareup.okhttp.internal.Util;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheRequest;
+import java.net.ProtocolException;
+import java.net.Socket;
+
+import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+
+public final class HttpTransport implements Transport {
+  /**
+   * The timeout to use while discarding a stream of input data. Since this is
+   * used for connection reuse, this timeout should be significantly less than
+   * the time it takes to establish a new connection.
+   */
+  private static final int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
+
+  public static final int DEFAULT_CHUNK_LENGTH = 1024;
+
+  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.getHttpMinorVersion() != 0) {
+      httpEngine.requestHeaders.setChunked();
+      chunked = true;
+    }
+
+    // Stream a request body of unknown length.
+    if (chunked) {
+      int chunkLength = httpEngine.policy.getChunkLength();
+      if (chunkLength == -1) {
+        chunkLength = 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 {
+    httpEngine.writingRequestHeaders();
+    RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders();
+    byte[] bytes = headersToSend.toBytes();
+    requestOut.write(bytes);
+  }
+
+  @Override public ResponseHeaders readResponseHeaders() throws IOException {
+    RawHeaders headers = RawHeaders.fromBytes(socketIn);
+    httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion());
+    httpEngine.receiveHeaders(headers);
+    return new ResponseHeaders(httpEngine.uri, headers);
+  }
+
+  public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
+      InputStream responseBodyIn) {
+    if (streamCancelled) {
+      return false;
+    }
+
+    // We cannot reuse sockets that have incomplete output.
+    if (requestBodyOut != null && !((AbstractOutputStream) requestBodyOut).isClosed()) {
+      return false;
+    }
+
+    // If the request specified that the connection shouldn't be reused, don't reuse it.
+    if (httpEngine.requestHeaders.hasConnectionClose()) {
+      return false;
+    }
+
+    // If the response specified that the connection shouldn't be reused, don't reuse it.
+    if (httpEngine.responseHeaders != null && httpEngine.responseHeaders.hasConnectionClose()) {
+      return false;
+    }
+
+    if (responseBodyIn instanceof UnknownLengthHttpInputStream) {
+      return false;
+    }
+
+    if (responseBodyIn != null) {
+      return discardStream(httpEngine, responseBodyIn);
+    }
+
+    return true;
+  }
+
+  /**
+   * Discards the response body so that the connection can be reused. This
+   * needs to be done judiciously, since it delays the current request in
+   * order to speed up a potential future request that may never occur.
+   */
+  private static boolean discardStream(HttpEngine httpEngine, InputStream responseBodyIn) {
+    Connection connection = httpEngine.connection;
+    if (connection == null) return false;
+    Socket socket = connection.getSocket();
+    if (socket == null) return false;
+    try {
+      int socketTimeout = socket.getSoTimeout();
+      socket.setSoTimeout(DISCARD_STREAM_TIMEOUT_MILLIS);
+      try {
+        Util.skipAll(responseBodyIn);
+        return true;
+      } finally {
+        socket.setSoTimeout(socketTimeout);
+      }
+    } catch (IOException e) {
+      return false;
+    }
+  }
+
+  @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 connection (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 AbstractOutputStream {
+    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();
+      checkOffsetAndCount(buffer.length, offset, count);
+      if (count > bytesRemaining) {
+        throw new ProtocolException("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 ProtocolException("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 AbstractOutputStream {
+    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();
+      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(false);
+      }
+    }
+
+    @Override public int read(byte[] buffer, int offset, int count) throws IOException {
+      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 ProtocolException("unexpected end of stream");
+      }
+      bytesRemaining -= read;
+      cacheWrite(buffer, offset, read);
+      if (bytesRemaining == 0) {
+        endOfInput(false);
+      }
+      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;
+      }
+      if (bytesRemaining != 0 && !discardStream(httpEngine, this)) {
+        unexpectedEndOfInput();
+      }
+      closed = true;
+    }
+  }
+
+  /** An HTTP body with alternating chunk sizes and chunk bodies. */
+  private static class ChunkedInputStream extends AbstractHttpInputStream {
+    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 {
+      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);
+      return read;
+    }
+
+    private void readChunkSize() throws IOException {
+      // read the suffix of the previous chunk
+      if (bytesRemainingInChunk != NO_CHUNK_YET) {
+        Util.readAsciiLine(in);
+      }
+      String chunkSizeString = Util.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 ProtocolException("Expected a hex chunk size but was " + chunkSizeString);
+      }
+      if (bytesRemainingInChunk == 0) {
+        hasMoreChunks = false;
+        RawHeaders rawResponseHeaders = httpEngine.responseHeaders.getHeaders();
+        RawHeaders.readHeaders(transport.socketIn, rawResponseHeaders);
+        httpEngine.receiveHeaders(rawResponseHeaders);
+        endOfInput(false);
+      }
+    }
+
+    @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;
+      }
+      if (hasMoreChunks && !discardStream(httpEngine, this)) {
+        unexpectedEndOfInput();
+      }
+      closed = true;
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
new file mode 100644
index 0000000..eabe649
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
@@ -0,0 +1,556 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Connection;
+import com.squareup.o