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.okhttp.ConnectionPool;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Route;
+import com.squareup.okhttp.internal.AbstractOutputStream;
+import com.squareup.okhttp.internal.FaultRecoveringOutputStream;
+import com.squareup.okhttp.internal.Util;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CookieHandler;
+import java.net.HttpRetryException;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketPermission;
+import java.net.URL;
+import java.security.Permission;
+import java.security.cert.CertificateException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSocketFactory;
+
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
+
+/**
+ * This implementation uses HttpEngine to send requests and receive responses.
+ * This class may use multiple HttpEngines to follow redirects, authentication
+ * retries, etc. to retrieve the final response body.
+ *
+ * <h3>What does 'connected' mean?</h3>
+ * This class inherits a {@code connected} field from the superclass. That field
+ * is <strong>not</strong> used to indicate not whether this URLConnection is
+ * currently connected. Instead, it indicates whether a connection has ever been
+ * attempted. Once a connection has been attempted, certain properties (request
+ * header fields, request method, etc.) are immutable. Test the {@code
+ * connection} field on this class for null/non-null to determine of an instance
+ * is currently connected to a server.
+ */
+public class HttpURLConnectionImpl extends HttpURLConnection {
+
+  /** Numeric status code, 307: Temporary Redirect. */
+  static final int HTTP_TEMP_REDIRECT = 307;
+
+  /**
+   * How many redirects should we follow? Chrome follows 21; Firefox, curl,
+   * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
+   */
+  private static final int MAX_REDIRECTS = 20;
+
+  /**
+   * The minimum number of request body bytes to transmit before we're willing
+   * to let a routine {@link IOException} bubble up to the user. This is used to
+   * size a buffer for data that will be replayed upon error.
+   */
+  private static final int MAX_REPLAY_BUFFER_LENGTH = 8192;
+
+  private final boolean followProtocolRedirects;
+
+  /** The proxy requested by the client, or null for a proxy to be selected automatically. */
+  final Proxy requestedProxy;
+
+  final ProxySelector proxySelector;
+  final CookieHandler cookieHandler;
+  final OkResponseCache responseCache;
+  final ConnectionPool connectionPool;
+  /* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */
+  SSLSocketFactory sslSocketFactory;
+  HostnameVerifier hostnameVerifier;
+  final Set<Route> failedRoutes;
+
+  private final RawHeaders rawRequestHeaders = new RawHeaders();
+
+  private int redirectionCount;
+  private FaultRecoveringOutputStream faultRecoveringRequestBody;
+
+  protected IOException httpEngineFailure;
+  protected HttpEngine httpEngine;
+
+  public HttpURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
+      Set<Route> failedRoutes) {
+    super(url);
+    this.followProtocolRedirects = client.getFollowProtocolRedirects();
+    this.failedRoutes = failedRoutes;
+    this.requestedProxy = client.getProxy();
+    this.proxySelector = client.getProxySelector();
+    this.cookieHandler = client.getCookieHandler();
+    this.connectionPool = client.getConnectionPool();
+    this.sslSocketFactory = client.getSslSocketFactory();
+    this.hostnameVerifier = client.getHostnameVerifier();
+    this.responseCache = responseCache;
+  }
+
+  Set<Route> getFailedRoutes() {
+    return failedRoutes;
+  }
+
+  @Override public final void connect() throws IOException {
+    initHttpEngine();
+    boolean success;
+    do {
+      success = execute(false);
+    } while (!success);
+  }
+
+  @Override public final void disconnect() {
+    // Calling disconnect() before a connection exists should have no effect.
+    if (httpEngine != null) {
+      // We close the response body here instead of in
+      // HttpEngine.release because that is called when input
+      // has been completely read from the underlying socket.
+      // However the response body can be a GZIPInputStream that
+      // still has unread data.
+      if (httpEngine.hasResponse()) {
+        Util.closeQuietly(httpEngine.getResponseBody());
+      }
+      httpEngine.release(true);
+    }
+  }
+
+  /**
+   * Returns an input stream from the server in the case of error such as the
+   * requested file (txt, htm, html) is not found on the remote server.
+   */
+  @Override public final InputStream getErrorStream() {
+    try {
+      HttpEngine response = getResponse();
+      if (response.hasResponseBody() && response.getResponseCode() >= HTTP_BAD_REQUEST) {
+        return response.getResponseBody();
+      }
+      return null;
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Returns the value of the field at {@code position}. Returns null if there
+   * are fewer than {@code position} headers.
+   */
+  @Override public final String getHeaderField(int position) {
+    try {
+      return getResponse().getResponseHeaders().getHeaders().getValue(position);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Returns the value of the field corresponding to the {@code fieldName}, or
+   * null if there is no such field. If the field has multiple values, the
+   * last value is returned.
+   */
+  @Override public final String getHeaderField(String fieldName) {
+    try {
+      RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders();
+      return fieldName == null ? rawHeaders.getStatusLine() : rawHeaders.get(fieldName);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  @Override public final String getHeaderFieldKey(int position) {
+    try {
+      return getResponse().getResponseHeaders().getHeaders().getFieldName(position);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  @Override public final Map<String, List<String>> getHeaderFields() {
+    try {
+      return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  @Override public final Map<String, List<String>> getRequestProperties() {
+    if (connected) {
+      throw new IllegalStateException(
+          "Cannot access request header fields after connection is set");
+    }
+    return rawRequestHeaders.toMultimap(false);
+  }
+
+  @Override public final InputStream getInputStream() throws IOException {
+    if (!doInput) {
+      throw new ProtocolException("This protocol does not support input");
+    }
+
+    HttpEngine response = getResponse();
+
+    // if the requested file does not exist, throw an exception formerly the
+    // Error page from the server was returned if the requested file was
+    // text/html this has changed to return FileNotFoundException for all
+    // file types
+    if (getResponseCode() >= HTTP_BAD_REQUEST) {
+      throw new FileNotFoundException(url.toString());
+    }
+
+    InputStream result = response.getResponseBody();
+    if (result == null) {
+      throw new ProtocolException("No response body exists; responseCode=" + getResponseCode());
+    }
+    return result;
+  }
+
+  @Override public final OutputStream getOutputStream() throws IOException {
+    connect();
+
+    OutputStream out = httpEngine.getRequestBody();
+    if (out == null) {
+      throw new ProtocolException("method does not support a request body: " + method);
+    } else if (httpEngine.hasResponse()) {
+      throw new ProtocolException("cannot write request body after response has been read");
+    }
+
+    if (faultRecoveringRequestBody == null) {
+      faultRecoveringRequestBody = new FaultRecoveringOutputStream(MAX_REPLAY_BUFFER_LENGTH, out) {
+        @Override protected OutputStream replacementStream(IOException e) throws IOException {
+          if (httpEngine.getRequestBody() instanceof AbstractOutputStream
+              && ((AbstractOutputStream) httpEngine.getRequestBody()).isClosed()) {
+            return null; // Don't recover once the underlying stream has been closed.
+          }
+          if (handleFailure(e)) {
+            return httpEngine.getRequestBody();
+          }
+          return null; // This is a permanent failure.
+        }
+      };
+    }
+
+    return faultRecoveringRequestBody;
+  }
+
+  @Override public final Permission getPermission() throws IOException {
+    String hostName = getURL().getHost();
+    int hostPort = Util.getEffectivePort(getURL());
+    if (usingProxy()) {
+      InetSocketAddress proxyAddress = (InetSocketAddress) requestedProxy.address();
+      hostName = proxyAddress.getHostName();
+      hostPort = proxyAddress.getPort();
+    }
+    return new SocketPermission(hostName + ":" + hostPort, "connect, resolve");
+  }
+
+  @Override public final String getRequestProperty(String field) {
+    if (field == null) {
+      return null;
+    }
+    return rawRequestHeaders.get(field);
+  }
+
+  private void initHttpEngine() throws IOException {
+    if (httpEngineFailure != null) {
+      throw httpEngineFailure;
+    } else if (httpEngine != null) {
+      return;
+    }
+
+    connected = true;
+    try {
+      if (doOutput) {
+        if (method.equals("GET")) {
+          // they are requesting a stream to write to. This implies a POST method
+          method = "POST";
+        } else if (!method.equals("POST") && !method.equals("PUT")) {
+          // If the request method is neither POST nor PUT, then you're not writing
+          throw new ProtocolException(method + " does not support writing");
+        }
+      }
+      httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
+    } catch (IOException e) {
+      httpEngineFailure = e;
+      throw e;
+    }
+  }
+
+  protected HttpURLConnection getHttpConnectionToCache() {
+    return this;
+  }
+
+  private HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
+      Connection connection, RetryableOutputStream requestBody) throws IOException {
+    if (url.getProtocol().equals("http")) {
+      return new HttpEngine(this, method, requestHeaders, connection, requestBody);
+    } else if (url.getProtocol().equals("https")) {
+      return new HttpsURLConnectionImpl.HttpsEngine(
+          this, method, requestHeaders, connection, requestBody);
+    } else {
+      throw new AssertionError();
+    }
+  }
+
+  /**
+   * Aggressively tries to get the final HTTP response, potentially making
+   * many HTTP requests in the process in order to cope with redirects and
+   * authentication.
+   */
+  private HttpEngine getResponse() throws IOException {
+    initHttpEngine();
+
+    if (httpEngine.hasResponse()) {
+      return httpEngine;
+    }
+
+    while (true) {
+      if (!execute(true)) {
+        continue;
+      }
+
+      Retry retry = processResponseHeaders();
+      if (retry == Retry.NONE) {
+        httpEngine.automaticallyReleaseConnectionToPool();
+        return httpEngine;
+      }
+
+      // The first request was insufficient. Prepare for another...
+      String retryMethod = method;
+      OutputStream requestBody = httpEngine.getRequestBody();
+
+      // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM
+      // redirect should keep the same method, Chrome, Firefox and the
+      // RI all issue GETs when following any redirect.
+      int responseCode = getResponseCode();
+      if (responseCode == HTTP_MULT_CHOICE
+          || responseCode == HTTP_MOVED_PERM
+          || responseCode == HTTP_MOVED_TEMP
+          || responseCode == HTTP_SEE_OTHER) {
+        retryMethod = "GET";
+        requestBody = null;
+      }
+
+      if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
+        throw new HttpRetryException("Cannot retry streamed HTTP body",
+            httpEngine.getResponseCode());
+      }
+
+      if (retry == Retry.DIFFERENT_CONNECTION) {
+        httpEngine.automaticallyReleaseConnectionToPool();
+      }
+
+      httpEngine.release(false);
+
+      httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
+          (RetryableOutputStream) requestBody);
+    }
+  }
+
+  /**
+   * Sends a request and optionally reads a response. Returns true if the
+   * request was successfully executed, and false if the request can be
+   * retried. Throws an exception if the request failed permanently.
+   */
+  private boolean execute(boolean readResponse) throws IOException {
+    try {
+      httpEngine.sendRequest();
+      if (readResponse) {
+        httpEngine.readResponse();
+      }
+      return true;
+    } catch (IOException e) {
+      if (handleFailure(e)) {
+        return false;
+      } else {
+        throw e;
+      }
+    }
+  }
+
+  /**
+   * Report and attempt to recover from {@code e}. Returns true if the HTTP
+   * engine was replaced and the request should be retried. Otherwise the
+   * failure is permanent.
+   */
+  private boolean handleFailure(IOException e) throws IOException {
+    RouteSelector routeSelector = httpEngine.routeSelector;
+    if (routeSelector != null && httpEngine.connection != null) {
+      routeSelector.connectFailed(httpEngine.connection, e);
+    }
+
+    OutputStream requestBody = httpEngine.getRequestBody();
+    boolean canRetryRequestBody = requestBody == null
+        || requestBody instanceof RetryableOutputStream
+        || (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable());
+    if (routeSelector == null && httpEngine.connection == null // No connection.
+        || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
+        || !isRecoverable(e)
+        || !canRetryRequestBody) {
+      httpEngineFailure = e;
+      return false;
+    }
+
+    httpEngine.release(true);
+    RetryableOutputStream retryableOutputStream = requestBody instanceof RetryableOutputStream
+        ? (RetryableOutputStream) requestBody
+        : null;
+    httpEngine = newHttpEngine(method, rawRequestHeaders, null, retryableOutputStream);
+    httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
+    if (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable()) {
+      httpEngine.sendRequest();
+      faultRecoveringRequestBody.replaceStream(httpEngine.getRequestBody());
+    }
+    return true;
+  }
+
+  private boolean isRecoverable(IOException e) {
+    // If the problem was a CertificateException from the X509TrustManager,
+    // do not retry, we didn't have an abrupt server initiated exception.
+    boolean sslFailure =
+        e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException;
+    boolean protocolFailure = e instanceof ProtocolException;
+    return !sslFailure && !protocolFailure;
+  }
+
+  public HttpEngine getHttpEngine() {
+    return httpEngine;
+  }
+
+  enum Retry {
+    NONE,
+    SAME_CONNECTION,
+    DIFFERENT_CONNECTION
+  }
+
+  /**
+   * Returns the retry action to take for the current response headers. The
+   * headers, proxy and target URL or this connection may be adjusted to
+   * prepare for a follow up request.
+   */
+  private Retry processResponseHeaders() throws IOException {
+    Proxy selectedProxy = httpEngine.connection != null
+        ? httpEngine.connection.getRoute().getProxy()
+        : requestedProxy;
+    final int responseCode = getResponseCode();
+    switch (responseCode) {
+      case HTTP_PROXY_AUTH:
+        if (selectedProxy.type() != Proxy.Type.HTTP) {
+          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
+        }
+        // fall-through
+      case HTTP_UNAUTHORIZED:
+        boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(),
+            httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, selectedProxy, url);
+        return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
+
+      case HTTP_MULT_CHOICE:
+      case HTTP_MOVED_PERM:
+      case HTTP_MOVED_TEMP:
+      case HTTP_SEE_OTHER:
+      case HTTP_TEMP_REDIRECT:
+        if (!getInstanceFollowRedirects()) {
+          return Retry.NONE;
+        }
+        if (++redirectionCount > MAX_REDIRECTS) {
+          throw new ProtocolException("Too many redirects: " + redirectionCount);
+        }
+        if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) {
+          // "If the 307 status code is received in response to a request other than GET or HEAD,
+          // the user agent MUST NOT automatically redirect the request"
+          return Retry.NONE;
+        }
+        String location = getHeaderField("Location");
+        if (location == null) {
+          return Retry.NONE;
+        }
+        URL previousUrl = url;
+        url = new URL(previousUrl, location);
+        if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) {
+          return Retry.NONE; // Don't follow redirects to unsupported protocols.
+        }
+        boolean sameProtocol = previousUrl.getProtocol().equals(url.getProtocol());
+        if (!sameProtocol && !followProtocolRedirects) {
+          return Retry.NONE; // This client doesn't follow redirects across protocols.
+        }
+        boolean sameHost = previousUrl.getHost().equals(url.getHost());
+        boolean samePort = getEffectivePort(previousUrl) == getEffectivePort(url);
+        if (sameHost && samePort && sameProtocol) {
+          return Retry.SAME_CONNECTION;
+        } else {
+          return Retry.DIFFERENT_CONNECTION;
+        }
+
+      default:
+        return Retry.NONE;
+    }
+  }
+
+  /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
+  final int getFixedContentLength() {
+    return fixedContentLength;
+  }
+
+  /** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
+  final int getChunkLength() {
+    return chunkLength;
+  }
+
+  @Override public final boolean usingProxy() {
+    return (requestedProxy != null && requestedProxy.type() != Proxy.Type.DIRECT);
+  }
+
+  @Override public String getResponseMessage() throws IOException {
+    return getResponse().getResponseHeaders().getHeaders().getResponseMessage();
+  }
+
+  @Override public final int getResponseCode() throws IOException {
+    return getResponse().getResponseCode();
+  }
+
+  @Override public final void setRequestProperty(String field, String newValue) {
+    if (connected) {
+      throw new IllegalStateException("Cannot set request property after connection is made");
+    }
+    if (field == null) {
+      throw new NullPointerException("field == null");
+    }
+    rawRequestHeaders.set(field, newValue);
+  }
+
+  @Override public final void addRequestProperty(String field, String value) {
+    if (connected) {
+      throw new IllegalStateException("Cannot add request property after connection is made");
+    }
+    if (field == null) {
+      throw new NullPointerException("field == null");
+    }
+    rawRequestHeaders.add(field, value);
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
new file mode 100644
index 0000000..235f862
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
@@ -0,0 +1,461 @@
+/*
+ *  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.okhttp.OkHttpClient;
+import com.squareup.okhttp.Route;
+import com.squareup.okhttp.TunnelRequest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.net.SecureCacheResponse;
+import java.net.URL;
+import java.security.Permission;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
+
+public final class HttpsURLConnectionImpl extends HttpsURLConnection {
+
+  /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
+  private final HttpUrlConnectionDelegate delegate;
+
+  public HttpsURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
+      Set<Route> failedRoutes) {
+    super(url);
+    delegate = new HttpUrlConnectionDelegate(url, client, responseCache, failedRoutes);
+  }
+
+  @Override public String getCipherSuite() {
+    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
+    if (cacheResponse != null) {
+      return cacheResponse.getCipherSuite();
+    }
+    SSLSocket sslSocket = getSslSocket();
+    if (sslSocket != null) {
+      return sslSocket.getSession().getCipherSuite();
+    }
+    return null;
+  }
+
+  @Override public Certificate[] getLocalCertificates() {
+    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
+    if (cacheResponse != null) {
+      List<Certificate> result = cacheResponse.getLocalCertificateChain();
+      return result != null ? result.toArray(new Certificate[result.size()]) : null;
+    }
+    SSLSocket sslSocket = getSslSocket();
+    if (sslSocket != null) {
+      return sslSocket.getSession().getLocalCertificates();
+    }
+    return null;
+  }
+
+  @Override public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
+    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
+    if (cacheResponse != null) {
+      List<Certificate> result = cacheResponse.getServerCertificateChain();
+      return result != null ? result.toArray(new Certificate[result.size()]) : null;
+    }
+    SSLSocket sslSocket = getSslSocket();
+    if (sslSocket != null) {
+      return sslSocket.getSession().getPeerCertificates();
+    }
+    return null;
+  }
+
+  @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
+    if (cacheResponse != null) {
+      return cacheResponse.getPeerPrincipal();
+    }
+    SSLSocket sslSocket = getSslSocket();
+    if (sslSocket != null) {
+      return sslSocket.getSession().getPeerPrincipal();
+    }
+    return null;
+  }
+
+  @Override public Principal getLocalPrincipal() {
+    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
+    if (cacheResponse != null) {
+      return cacheResponse.getLocalPrincipal();
+    }
+    SSLSocket sslSocket = getSslSocket();
+    if (sslSocket != null) {
+      return sslSocket.getSession().getLocalPrincipal();
+    }
+    return null;
+  }
+
+  public HttpEngine getHttpEngine() {
+    return delegate.getHttpEngine();
+  }
+
+  private SSLSocket getSslSocket() {
+    if (delegate.httpEngine == null || delegate.httpEngine.sentRequestMillis == -1) {
+      throw new IllegalStateException("Connection has not yet been established");
+    }
+    return delegate.httpEngine instanceof HttpsEngine
+        ? ((HttpsEngine) delegate.httpEngine).sslSocket
+        : null; // Not HTTPS! Probably an https:// to http:// redirect.
+  }
+
+  @Override
+  public void disconnect() {
+    delegate.disconnect();
+  }
+
+  @Override
+  public InputStream getErrorStream() {
+    return delegate.getErrorStream();
+  }
+
+  @Override
+  public String getRequestMethod() {
+    return delegate.getRequestMethod();
+  }
+
+  @Override
+  public int getResponseCode() throws IOException {
+    return delegate.getResponseCode();
+  }
+
+  @Override
+  public String getResponseMessage() throws IOException {
+    return delegate.getResponseMessage();
+  }
+
+  @Override
+  public void setRequestMethod(String method) throws ProtocolException {
+    delegate.setRequestMethod(method);
+  }
+
+  @Override
+  public boolean usingProxy() {
+    return delegate.usingProxy();
+  }
+
+  @Override
+  public boolean getInstanceFollowRedirects() {
+    return delegate.getInstanceFollowRedirects();
+  }
+
+  @Override
+  public void setInstanceFollowRedirects(boolean followRedirects) {
+    delegate.setInstanceFollowRedirects(followRedirects);
+  }
+
+  @Override
+  public void connect() throws IOException {
+    connected = true;
+    delegate.connect();
+  }
+
+  @Override
+  public boolean getAllowUserInteraction() {
+    return delegate.getAllowUserInteraction();
+  }
+
+  @Override
+  public Object getContent() throws IOException {
+    return delegate.getContent();
+  }
+
+  @SuppressWarnings("unchecked") // Spec does not generify
+  @Override
+  public Object getContent(Class[] types) throws IOException {
+    return delegate.getContent(types);
+  }
+
+  @Override
+  public String getContentEncoding() {
+    return delegate.getContentEncoding();
+  }
+
+  @Override
+  public int getContentLength() {
+    return delegate.getContentLength();
+  }
+
+  @Override
+  public String getContentType() {
+    return delegate.getContentType();
+  }
+
+  @Override
+  public long getDate() {
+    return delegate.getDate();
+  }
+
+  @Override
+  public boolean getDefaultUseCaches() {
+    return delegate.getDefaultUseCaches();
+  }
+
+  @Override
+  public boolean getDoInput() {
+    return delegate.getDoInput();
+  }
+
+  @Override
+  public boolean getDoOutput() {
+    return delegate.getDoOutput();
+  }
+
+  @Override
+  public long getExpiration() {
+    return delegate.getExpiration();
+  }
+
+  @Override
+  public String getHeaderField(int pos) {
+    return delegate.getHeaderField(pos);
+  }
+
+  @Override
+  public Map<String, List<String>> getHeaderFields() {
+    return delegate.getHeaderFields();
+  }
+
+  @Override
+  public Map<String, List<String>> getRequestProperties() {
+    return delegate.getRequestProperties();
+  }
+
+  @Override
+  public void addRequestProperty(String field, String newValue) {
+    delegate.addRequestProperty(field, newValue);
+  }
+
+  @Override
+  public String getHeaderField(String key) {
+    return delegate.getHeaderField(key);
+  }
+
+  @Override
+  public long getHeaderFieldDate(String field, long defaultValue) {
+    return delegate.getHeaderFieldDate(field, defaultValue);
+  }
+
+  @Override
+  public int getHeaderFieldInt(String field, int defaultValue) {
+    return delegate.getHeaderFieldInt(field, defaultValue);
+  }
+
+  @Override
+  public String getHeaderFieldKey(int position) {
+    return delegate.getHeaderFieldKey(position);
+  }
+
+  @Override
+  public long getIfModifiedSince() {
+    return delegate.getIfModifiedSince();
+  }
+
+  @Override
+  public InputStream getInputStream() throws IOException {
+    return delegate.getInputStream();
+  }
+
+  @Override
+  public long getLastModified() {
+    return delegate.getLastModified();
+  }
+
+  @Override
+  public OutputStream getOutputStream() throws IOException {
+    return delegate.getOutputStream();
+  }
+
+  @Override
+  public Permission getPermission() throws IOException {
+    return delegate.getPermission();
+  }
+
+  @Override
+  public String getRequestProperty(String field) {
+    return delegate.getRequestProperty(field);
+  }
+
+  @Override
+  public URL getURL() {
+    return delegate.getURL();
+  }
+
+  @Override
+  public boolean getUseCaches() {
+    return delegate.getUseCaches();
+  }
+
+  @Override
+  public void setAllowUserInteraction(boolean newValue) {
+    delegate.setAllowUserInteraction(newValue);
+  }
+
+  @Override
+  public void setDefaultUseCaches(boolean newValue) {
+    delegate.setDefaultUseCaches(newValue);
+  }
+
+  @Override
+  public void setDoInput(boolean newValue) {
+    delegate.setDoInput(newValue);
+  }
+
+  @Override
+  public void setDoOutput(boolean newValue) {
+    delegate.setDoOutput(newValue);
+  }
+
+  @Override
+  public void setIfModifiedSince(long newValue) {
+    delegate.setIfModifiedSince(newValue);
+  }
+
+  @Override
+  public void setRequestProperty(String field, String newValue) {
+    delegate.setRequestProperty(field, newValue);
+  }
+
+  @Override
+  public void setUseCaches(boolean newValue) {
+    delegate.setUseCaches(newValue);
+  }
+
+  @Override
+  public void setConnectTimeout(int timeoutMillis) {
+    delegate.setConnectTimeout(timeoutMillis);
+  }
+
+  @Override
+  public int getConnectTimeout() {
+    return delegate.getConnectTimeout();
+  }
+
+  @Override
+  public void setReadTimeout(int timeoutMillis) {
+    delegate.setReadTimeout(timeoutMillis);
+  }
+
+  @Override
+  public int getReadTimeout() {
+    return delegate.getReadTimeout();
+  }
+
+  @Override
+  public String toString() {
+    return delegate.toString();
+  }
+
+  @Override
+  public void setFixedLengthStreamingMode(int contentLength) {
+    delegate.setFixedLengthStreamingMode(contentLength);
+  }
+
+  @Override
+  public void setChunkedStreamingMode(int chunkLength) {
+    delegate.setChunkedStreamingMode(chunkLength);
+  }
+
+  @Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
+    delegate.hostnameVerifier = hostnameVerifier;
+  }
+
+  @Override public HostnameVerifier getHostnameVerifier() {
+    return delegate.hostnameVerifier;
+  }
+
+  @Override public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
+    delegate.sslSocketFactory = sslSocketFactory;
+  }
+
+  @Override public SSLSocketFactory getSSLSocketFactory() {
+    return delegate.sslSocketFactory;
+  }
+
+  private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
+    private HttpUrlConnectionDelegate(URL url, OkHttpClient client, OkResponseCache responseCache,
+        Set<Route> failedRoutes) {
+      super(url, client, responseCache, failedRoutes);
+    }
+
+    @Override protected HttpURLConnection getHttpConnectionToCache() {
+      return HttpsURLConnectionImpl.this;
+    }
+
+    public SecureCacheResponse getSecureCacheResponse() {
+      return httpEngine instanceof HttpsEngine
+          ? (SecureCacheResponse) httpEngine.getCacheResponse()
+          : null;
+    }
+  }
+
+  public static final class HttpsEngine extends HttpEngine {
+    /**
+     * Stash of HttpsEngine.connection.socket to implement requests like
+     * {@link #getCipherSuite} even after the connection has been recycled.
+     */
+    private SSLSocket sslSocket;
+
+    /**
+     * @param policy the HttpURLConnectionImpl with connection configuration
+     */
+    public HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
+        Connection connection, RetryableOutputStream requestBody) throws IOException {
+      super(policy, method, requestHeaders, connection, requestBody);
+      this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
+    }
+
+    @Override protected void connected(Connection connection) {
+      this.sslSocket = (SSLSocket) connection.getSocket();
+    }
+
+    @Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
+      return cacheResponse instanceof SecureCacheResponse;
+    }
+
+    @Override protected boolean includeAuthorityInRequestLine() {
+      // Even if there is a proxy, it isn't involved. Always request just the file.
+      return false;
+    }
+
+    @Override protected TunnelRequest getTunnelConfig() {
+      String userAgent = requestHeaders.getUserAgent();
+      if (userAgent == null) {
+        userAgent = getDefaultUserAgent();
+      }
+
+      URL url = policy.getURL();
+      return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent,
+          requestHeaders.getProxyAuthorization());
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/OkResponseCache.java b/framework/src/com/squareup/okhttp/internal/http/OkResponseCache.java
new file mode 100644
index 0000000..5829f02
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/OkResponseCache.java
@@ -0,0 +1,55 @@
+/*
+ * 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.http;
+
+import com.squareup.okhttp.ResponseSource;
+import java.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An extended response cache API. Unlike {@link java.net.ResponseCache}, this
+ * interface supports conditional caching and statistics.
+ *
+ * <p>Along with the rest of the {@code internal} package, this is not a public
+ * API. Applications wishing to supply their own caches must use the more
+ * limited {@link java.net.ResponseCache} interface.
+ */
+public interface OkResponseCache {
+  CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders)
+      throws IOException;
+
+  CacheRequest put(URI uri, URLConnection urlConnection) throws IOException;
+
+  /**
+   * 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.
+   */
+  void update(CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException;
+
+  /** Track an conditional GET that was satisfied by this cache. */
+  void trackConditionalCacheHit();
+
+  /** Track an HTTP response being satisfied by {@code source}. */
+  void trackResponse(ResponseSource source);
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java b/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java
new file mode 100644
index 0000000..2ac915a
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java
@@ -0,0 +1,53 @@
+/*
+ * 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.http;
+
+import com.squareup.okhttp.ResponseSource;
+import java.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+
+public final class OkResponseCacheAdapter implements OkResponseCache {
+  private final ResponseCache responseCache;
+  public OkResponseCacheAdapter(ResponseCache responseCache) {
+    this.responseCache = responseCache;
+  }
+
+  @Override public CacheResponse get(URI uri, String requestMethod,
+      Map<String, List<String>> requestHeaders) throws IOException {
+    return responseCache.get(uri, requestMethod, requestHeaders);
+  }
+
+  @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+    return responseCache.put(uri, urlConnection);
+  }
+
+  @Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection connection)
+      throws IOException {
+  }
+
+  @Override public void trackConditionalCacheHit() {
+  }
+
+  @Override public void trackResponse(ResponseSource source) {
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java b/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java
new file mode 100644
index 0000000..eba887e
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java
@@ -0,0 +1,424 @@
+/*
+ *  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.internal.Util;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.ProtocolException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * The HTTP status and unparsed header fields of a single HTTP message. Values
+ * are represented as uninterpreted strings; use {@link RequestHeaders} and
+ * {@link ResponseHeaders} for interpreted headers. This class maintains the
+ * order of the header fields within the HTTP message.
+ *
+ * <p>This class tracks fields line-by-line. A field with multiple comma-
+ * separated values on the same line will be treated as a field with a single
+ * value by this class. It is the caller's responsibility to detect and split
+ * on commas if their field permits multiple values. This simplifies use of
+ * single-valued fields whose values routinely contain commas, such as cookies
+ * or dates.
+ *
+ * <p>This class trims whitespace from values. It never returns values with
+ * leading or trailing whitespace.
+ */
+public final class RawHeaders {
+  private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
+    // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
+    @Override public int compare(String a, String b) {
+      if (a == b) {
+        return 0;
+      } else if (a == null) {
+        return -1;
+      } else if (b == null) {
+        return 1;
+      } else {
+        return String.CASE_INSENSITIVE_ORDER.compare(a, b);
+      }
+    }
+  };
+
+  private final List<String> namesAndValues = new ArrayList<String>(20);
+  private String requestLine;
+  private String statusLine;
+  private int httpMinorVersion = 1;
+  private int responseCode = -1;
+  private String responseMessage;
+
+  public RawHeaders() {
+  }
+
+  public RawHeaders(RawHeaders copyFrom) {
+    namesAndValues.addAll(copyFrom.namesAndValues);
+    requestLine = copyFrom.requestLine;
+    statusLine = copyFrom.statusLine;
+    httpMinorVersion = copyFrom.httpMinorVersion;
+    responseCode = copyFrom.responseCode;
+    responseMessage = copyFrom.responseMessage;
+  }
+
+  /** Sets the request line (like "GET / HTTP/1.1"). */
+  public void setRequestLine(String requestLine) {
+    requestLine = requestLine.trim();
+    this.requestLine = requestLine;
+  }
+
+  /** Sets the response status line (like "HTTP/1.0 200 OK"). */
+  public void setStatusLine(String statusLine) throws IOException {
+    // H T T P / 1 . 1   2 0 0   T e m p o r a r y   R e d i r e c t
+    // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
+    if (this.responseMessage != null) {
+      throw new IllegalStateException("statusLine is already set");
+    }
+    // We allow empty message without leading white space since some servers
+    // do not send the white space when the message is empty.
+    boolean hasMessage = statusLine.length() > 13;
+    if (!statusLine.startsWith("HTTP/1.")
+        || statusLine.length() < 12
+        || statusLine.charAt(8) != ' '
+        || (hasMessage && statusLine.charAt(12) != ' ')) {
+      throw new ProtocolException("Unexpected status line: " + statusLine);
+    }
+    int httpMinorVersion = statusLine.charAt(7) - '0';
+    if (httpMinorVersion < 0 || httpMinorVersion > 9) {
+      throw new ProtocolException("Unexpected status line: " + statusLine);
+    }
+    int responseCode;
+    try {
+      responseCode = Integer.parseInt(statusLine.substring(9, 12));
+    } catch (NumberFormatException e) {
+      throw new ProtocolException("Unexpected status line: " + statusLine);
+    }
+    this.responseMessage = hasMessage ? statusLine.substring(13) : "";
+    this.responseCode = responseCode;
+    this.statusLine = statusLine;
+    this.httpMinorVersion = httpMinorVersion;
+  }
+
+  public void computeResponseStatusLineFromSpdyHeaders() throws IOException {
+    String status = null;
+    String version = null;
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      String name = namesAndValues.get(i);
+      if (":status".equals(name)) {
+        status = namesAndValues.get(i + 1);
+      } else if (":version".equals(name)) {
+        version = namesAndValues.get(i + 1);
+      }
+    }
+    if (status == null || version == null) {
+      throw new ProtocolException("Expected ':status' and ':version' headers not present");
+    }
+    setStatusLine(version + " " + status);
+  }
+
+  /**
+   * @param method like "GET", "POST", "HEAD", etc.
+   * @param path like "/foo/bar.html"
+   * @param version like "HTTP/1.1"
+   * @param host like "www.android.com:1234"
+   * @param scheme like "https"
+   */
+  public void addSpdyRequestHeaders(String method, String path, String version, String host,
+      String scheme) {
+    // TODO: populate the statusLine for the client's benefit?
+    add(":method", method);
+    add(":scheme", scheme);
+    add(":path", path);
+    add(":version", version);
+    add(":host", host);
+  }
+
+  public String getStatusLine() {
+    return statusLine;
+  }
+
+  /**
+   * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0
+   * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown.
+   */
+  public int getHttpMinorVersion() {
+    return httpMinorVersion != -1 ? httpMinorVersion : 1;
+  }
+
+  /** Returns the HTTP status code or -1 if it is unknown. */
+  public int getResponseCode() {
+    return responseCode;
+  }
+
+  /** Returns the HTTP status message or null if it is unknown. */
+  public String getResponseMessage() {
+    return responseMessage;
+  }
+
+  /**
+   * Add an HTTP header line containing a field name, a literal colon, and a
+   * value.
+   */
+  public void addLine(String line) {
+    int index = line.indexOf(":");
+    if (index == -1) {
+      addLenient("", line);
+    } else {
+      addLenient(line.substring(0, index), line.substring(index + 1));
+    }
+  }
+
+  /** Add a field with the specified value. */
+  public void add(String fieldName, String value) {
+    if (fieldName == null) throw new IllegalArgumentException("fieldname == null");
+    if (value == null) throw new IllegalArgumentException("value == null");
+    if (fieldName.length() == 0 || fieldName.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
+      throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value);
+    }
+    addLenient(fieldName, value);
+  }
+
+  /**
+   * Add a field with the specified value without any validation. Only
+   * appropriate for headers from the remote peer.
+   */
+  private void addLenient(String fieldName, String value) {
+    namesAndValues.add(fieldName);
+    namesAndValues.add(value.trim());
+  }
+
+  public void removeAll(String fieldName) {
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
+        namesAndValues.remove(i); // field name
+        namesAndValues.remove(i); // value
+      }
+    }
+  }
+
+  public void addAll(String fieldName, List<String> headerFields) {
+    for (String value : headerFields) {
+      add(fieldName, value);
+    }
+  }
+
+  /**
+   * Set a field with the specified value. If the field is not found, it is
+   * added. If the field is found, the existing values are replaced.
+   */
+  public void set(String fieldName, String value) {
+    removeAll(fieldName);
+    add(fieldName, value);
+  }
+
+  /** Returns the number of field values. */
+  public int length() {
+    return namesAndValues.size() / 2;
+  }
+
+  /** Returns the field at {@code position} or null if that is out of range. */
+  public String getFieldName(int index) {
+    int fieldNameIndex = index * 2;
+    if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) {
+      return null;
+    }
+    return namesAndValues.get(fieldNameIndex);
+  }
+
+  /** Returns the value at {@code index} or null if that is out of range. */
+  public String getValue(int index) {
+    int valueIndex = index * 2 + 1;
+    if (valueIndex < 0 || valueIndex >= namesAndValues.size()) {
+      return null;
+    }
+    return namesAndValues.get(valueIndex);
+  }
+
+  /** Returns the last value corresponding to the specified field, or null. */
+  public String get(String fieldName) {
+    for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
+      if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
+        return namesAndValues.get(i + 1);
+      }
+    }
+    return null;
+  }
+
+  /** @param fieldNames a case-insensitive set of HTTP header field names. */
+  public RawHeaders getAll(Set<String> fieldNames) {
+    RawHeaders result = new RawHeaders();
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      String fieldName = namesAndValues.get(i);
+      if (fieldNames.contains(fieldName)) {
+        result.add(fieldName, namesAndValues.get(i + 1));
+      }
+    }
+    return result;
+  }
+
+  /** Returns bytes of a request header for sending on an HTTP transport. */
+  public byte[] toBytes() throws UnsupportedEncodingException {
+    StringBuilder result = new StringBuilder(256);
+    result.append(requestLine).append("\r\n");
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      result.append(namesAndValues.get(i))
+          .append(": ")
+          .append(namesAndValues.get(i + 1))
+          .append("\r\n");
+    }
+    result.append("\r\n");
+    return result.toString().getBytes("ISO-8859-1");
+  }
+
+  /** Parses bytes of a response header from an HTTP transport. */
+  public static RawHeaders fromBytes(InputStream in) throws IOException {
+    RawHeaders headers;
+    do {
+      headers = new RawHeaders();
+      headers.setStatusLine(Util.readAsciiLine(in));
+      readHeaders(in, headers);
+    } while (headers.getResponseCode() == HttpEngine.HTTP_CONTINUE);
+    return headers;
+  }
+
+  /** Reads headers or trailers into {@code out}. */
+  public static void readHeaders(InputStream in, RawHeaders out) throws IOException {
+    // parse the result headers until the first blank line
+    String line;
+    while ((line = Util.readAsciiLine(in)).length() != 0) {
+      out.addLine(line);
+    }
+  }
+
+  /**
+   * Returns an immutable map containing each field to its list of values. The
+   * status line is mapped to null.
+   */
+  public Map<String, List<String>> toMultimap(boolean response) {
+    Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      String fieldName = namesAndValues.get(i);
+      String value = namesAndValues.get(i + 1);
+
+      List<String> allValues = new ArrayList<String>();
+      List<String> otherValues = result.get(fieldName);
+      if (otherValues != null) {
+        allValues.addAll(otherValues);
+      }
+      allValues.add(value);
+      result.put(fieldName, Collections.unmodifiableList(allValues));
+    }
+    if (response && statusLine != null) {
+      result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine)));
+    } else if (requestLine != null) {
+      result.put(null, Collections.unmodifiableList(Collections.singletonList(requestLine)));
+    }
+    return Collections.unmodifiableMap(result);
+  }
+
+  /**
+   * Creates a new instance from the given map of fields to values. If
+   * present, the null field's last element will be used to set the status
+   * line.
+   */
+  public static RawHeaders fromMultimap(Map<String, List<String>> map, boolean response)
+      throws IOException {
+    if (!response) throw new UnsupportedOperationException();
+    RawHeaders result = new RawHeaders();
+    for (Entry<String, List<String>> entry : map.entrySet()) {
+      String fieldName = entry.getKey();
+      List<String> values = entry.getValue();
+      if (fieldName != null) {
+        for (String value : values) {
+          result.addLenient(fieldName, value);
+        }
+      } else if (!values.isEmpty()) {
+        result.setStatusLine(values.get(values.size() - 1));
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Returns a list of alternating names and values. Names are all lower case.
+   * No names are repeated. If any name has multiple values, they are
+   * concatenated using "\0" as a delimiter.
+   */
+  public List<String> toNameValueBlock() {
+    Set<String> names = new HashSet<String>();
+    List<String> result = new ArrayList<String>();
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      String name = namesAndValues.get(i).toLowerCase(Locale.US);
+      String value = namesAndValues.get(i + 1);
+
+      // Drop headers that are forbidden when layering HTTP over SPDY.
+      if (name.equals("connection")
+          || name.equals("host")
+          || name.equals("keep-alive")
+          || name.equals("proxy-connection")
+          || name.equals("transfer-encoding")) {
+        continue;
+      }
+
+      // If we haven't seen this name before, add the pair to the end of the list...
+      if (names.add(name)) {
+        result.add(name);
+        result.add(value);
+        continue;
+      }
+
+      // ...otherwise concatenate the existing values and this value.
+      for (int j = 0; j < result.size(); j += 2) {
+        if (name.equals(result.get(j))) {
+          result.set(j + 1, result.get(j + 1) + "\0" + value);
+          break;
+        }
+      }
+    }
+    return result;
+  }
+
+  public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) {
+    if (nameValueBlock.size() % 2 != 0) {
+      throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock);
+    }
+    RawHeaders result = new RawHeaders();
+    for (int i = 0; i < nameValueBlock.size(); i += 2) {
+      String name = nameValueBlock.get(i);
+      String values = nameValueBlock.get(i + 1);
+      for (int start = 0; start < values.length(); ) {
+        int end = values.indexOf('\0', start);
+        if (end == -1) {
+          end = values.length();
+        }
+        result.namesAndValues.add(name);
+        result.namesAndValues.add(values.substring(start, end));
+        start = end + 1;
+      }
+    }
+    return result;
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java b/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java
new file mode 100644
index 0000000..5ec4fcc
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java
@@ -0,0 +1,290 @@
+/*
+ * 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.net.URI;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/** Parsed HTTP request headers. */
+public final class RequestHeaders {
+  private final URI uri;
+  private final RawHeaders headers;
+
+  /** Don't use a cache to satisfy this request. */
+  private boolean noCache;
+  private int maxAgeSeconds = -1;
+  private int maxStaleSeconds = -1;
+  private int minFreshSeconds = -1;
+
+  /**
+   * This field's name "only-if-cached" is misleading. It actually means "do
+   * not use the network". It is set by a client who only wants to make a
+   * request if it can be fully satisfied by the cache. Cached responses that
+   * would require validation (ie. conditional gets) are not permitted if this
+   * header is set.
+   */
+  private boolean onlyIfCached;
+
+  /**
+   * True if the request contains an authorization field. Although this isn't
+   * necessarily a shared cache, it follows the spec's strict requirements for
+   * shared caches.
+   */
+  private boolean hasAuthorization;
+
+  private int contentLength = -1;
+  private String transferEncoding;
+  private String userAgent;
+  private String host;
+  private String connection;
+  private String acceptEncoding;
+  private String contentType;
+  private String ifModifiedSince;
+  private String ifNoneMatch;
+  private String proxyAuthorization;
+
+  public RequestHeaders(URI uri, RawHeaders headers) {
+    this.uri = uri;
+    this.headers = headers;
+
+    HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
+      @Override public void handle(String directive, String parameter) {
+        if ("no-cache".equalsIgnoreCase(directive)) {
+          noCache = true;
+        } else if ("max-age".equalsIgnoreCase(directive)) {
+          maxAgeSeconds = HeaderParser.parseSeconds(parameter);
+        } else if ("max-stale".equalsIgnoreCase(directive)) {
+          maxStaleSeconds = HeaderParser.parseSeconds(parameter);
+        } else if ("min-fresh".equalsIgnoreCase(directive)) {
+          minFreshSeconds = HeaderParser.parseSeconds(parameter);
+        } else if ("only-if-cached".equalsIgnoreCase(directive)) {
+          onlyIfCached = true;
+        }
+      }
+    };
+
+    for (int i = 0; i < headers.length(); i++) {
+      String fieldName = headers.getFieldName(i);
+      String value = headers.getValue(i);
+      if ("Cache-Control".equalsIgnoreCase(fieldName)) {
+        HeaderParser.parseCacheControl(value, handler);
+      } else if ("Pragma".equalsIgnoreCase(fieldName)) {
+        if ("no-cache".equalsIgnoreCase(value)) {
+          noCache = true;
+        }
+      } else if ("If-None-Match".equalsIgnoreCase(fieldName)) {
+        ifNoneMatch = value;
+      } else if ("If-Modified-Since".equalsIgnoreCase(fieldName)) {
+        ifModifiedSince = value;
+      } else if ("Authorization".equalsIgnoreCase(fieldName)) {
+        hasAuthorization = true;
+      } else if ("Content-Length".equalsIgnoreCase(fieldName)) {
+        try {
+          contentLength = Integer.parseInt(value);
+        } catch (NumberFormatException ignored) {
+        }
+      } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
+        transferEncoding = value;
+      } else if ("User-Agent".equalsIgnoreCase(fieldName)) {
+        userAgent = value;
+      } else if ("Host".equalsIgnoreCase(fieldName)) {
+        host = value;
+      } else if ("Connection".equalsIgnoreCase(fieldName)) {
+        connection = value;
+      } else if ("Accept-Encoding".equalsIgnoreCase(fieldName)) {
+        acceptEncoding = value;
+      } else if ("Content-Type".equalsIgnoreCase(fieldName)) {
+        contentType = value;
+      } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) {
+        proxyAuthorization = value;
+      }
+    }
+  }
+
+  public boolean isChunked() {
+    return "chunked".equalsIgnoreCase(transferEncoding);
+  }
+
+  public boolean hasConnectionClose() {
+    return "close".equalsIgnoreCase(connection);
+  }
+
+  public URI getUri() {
+    return uri;
+  }
+
+  public RawHeaders getHeaders() {
+    return headers;
+  }
+
+  public boolean isNoCache() {
+    return noCache;
+  }
+
+  public int getMaxAgeSeconds() {
+    return maxAgeSeconds;
+  }
+
+  public int getMaxStaleSeconds() {
+    return maxStaleSeconds;
+  }
+
+  public int getMinFreshSeconds() {
+    return minFreshSeconds;
+  }
+
+  public boolean isOnlyIfCached() {
+    return onlyIfCached;
+  }
+
+  public boolean hasAuthorization() {
+    return hasAuthorization;
+  }
+
+  public int getContentLength() {
+    return contentLength;
+  }
+
+  public String getTransferEncoding() {
+    return transferEncoding;
+  }
+
+  public String getUserAgent() {
+    return userAgent;
+  }
+
+  public String getHost() {
+    return host;
+  }
+
+  public String getConnection() {
+    return connection;
+  }
+
+  public String getAcceptEncoding() {
+    return acceptEncoding;
+  }
+
+  public String getContentType() {
+    return contentType;
+  }
+
+  public String getIfModifiedSince() {
+    return ifModifiedSince;
+  }
+
+  public String getIfNoneMatch() {
+    return ifNoneMatch;
+  }
+
+  public String getProxyAuthorization() {
+    return proxyAuthorization;
+  }
+
+  public void setChunked() {
+    if (this.transferEncoding != null) {
+      headers.removeAll("Transfer-Encoding");
+    }
+    headers.add("Transfer-Encoding", "chunked");
+    this.transferEncoding = "chunked";
+  }
+
+  public void setContentLength(int contentLength) {
+    if (this.contentLength != -1) {
+      headers.removeAll("Content-Length");
+    }
+    headers.add("Content-Length", Integer.toString(contentLength));
+    this.contentLength = contentLength;
+  }
+
+  public void setUserAgent(String userAgent) {
+    if (this.userAgent != null) {
+      headers.removeAll("User-Agent");
+    }
+    headers.add("User-Agent", userAgent);
+    this.userAgent = userAgent;
+  }
+
+  public void setHost(String host) {
+    if (this.host != null) {
+      headers.removeAll("Host");
+    }
+    headers.add("Host", host);
+    this.host = host;
+  }
+
+  public void setConnection(String connection) {
+    if (this.connection != null) {
+      headers.removeAll("Connection");
+    }
+    headers.add("Connection", connection);
+    this.connection = connection;
+  }
+
+  public void setAcceptEncoding(String acceptEncoding) {
+    if (this.acceptEncoding != null) {
+      headers.removeAll("Accept-Encoding");
+    }
+    headers.add("Accept-Encoding", acceptEncoding);
+    this.acceptEncoding = acceptEncoding;
+  }
+
+  public void setContentType(String contentType) {
+    if (this.contentType != null) {
+      headers.removeAll("Content-Type");
+    }
+    headers.add("Content-Type", contentType);
+    this.contentType = contentType;
+  }
+
+  public void setIfModifiedSince(Date date) {
+    if (ifModifiedSince != null) {
+      headers.removeAll("If-Modified-Since");
+    }
+    String formattedDate = HttpDate.format(date);
+    headers.add("If-Modified-Since", formattedDate);
+    ifModifiedSince = formattedDate;
+  }
+
+  public void setIfNoneMatch(String ifNoneMatch) {
+    if (this.ifNoneMatch != null) {
+      headers.removeAll("If-None-Match");
+    }
+    headers.add("If-None-Match", ifNoneMatch);
+    this.ifNoneMatch = ifNoneMatch;
+  }
+
+  /**
+   * Returns true if the request contains conditions that save the server from
+   * sending a response that the client has locally. When the caller adds
+   * conditions, this cache won't participate in the request.
+   */
+  public boolean hasConditions() {
+    return ifModifiedSince != null || ifNoneMatch != null;
+  }
+
+  public void addCookies(Map<String, List<String>> allCookieHeaders) {
+    for (Map.Entry<String, List<String>> entry : allCookieHeaders.entrySet()) {
+      String key = entry.getKey();
+      if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) {
+        headers.addAll(key, entry.getValue());
+      }
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java b/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java
new file mode 100644
index 0000000..2ab564d
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java
@@ -0,0 +1,497 @@
+/*
+ * 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.ResponseSource;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+
+import static com.squareup.okhttp.internal.Util.equal;
+
+/** Parsed HTTP response headers. */
+public final class ResponseHeaders {
+
+  /** HTTP header name for the local time when the request was sent. */
+  private static final String SENT_MILLIS = "X-Android-Sent-Millis";
+
+  /** HTTP header name for the local time when the response was received. */
+  private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
+
+  /** HTTP synthetic header with the response source. */
+  static final String RESPONSE_SOURCE = "X-Android-Response-Source";
+
+  private final URI uri;
+  private final RawHeaders headers;
+
+  /** The server's time when this response was served, if known. */
+  private Date servedDate;
+
+  /** The last modified date of the response, if known. */
+  private Date lastModified;
+
+  /**
+   * The expiration date of the response, if known. If both this field and the
+   * max age are set, the max age is preferred.
+   */
+  private Date expires;
+
+  /**
+   * Extension header set by HttpURLConnectionImpl specifying the timestamp
+   * when the HTTP request was first initiated.
+   */
+  private long sentRequestMillis;
+
+  /**
+   * Extension header set by HttpURLConnectionImpl specifying the timestamp
+   * when the HTTP response was first received.
+   */
+  private long receivedResponseMillis;
+
+  /**
+   * In the response, this field's name "no-cache" is misleading. It doesn't
+   * prevent us from caching the response; it only means we have to validate
+   * the response with the origin server before returning it. We can do this
+   * with a conditional get.
+   */
+  private boolean noCache;
+
+  /** If true, this response should not be cached. */
+  private boolean noStore;
+
+  /**
+   * The duration past the response's served date that it can be served
+   * without validation.
+   */
+  private int maxAgeSeconds = -1;
+
+  /**
+   * The "s-maxage" directive is the max age for shared caches. Not to be
+   * confused with "max-age" for non-shared caches, As in Firefox and Chrome,
+   * this directive is not honored by this cache.
+   */
+  private int sMaxAgeSeconds = -1;
+
+  /**
+   * This request header field's name "only-if-cached" is misleading. It
+   * actually means "do not use the network". It is set by a client who only
+   * wants to make a request if it can be fully satisfied by the cache.
+   * Cached responses that would require validation (ie. conditional gets) are
+   * not permitted if this header is set.
+   */
+  private boolean isPublic;
+  private boolean mustRevalidate;
+  private String etag;
+  private int ageSeconds = -1;
+
+  /** Case-insensitive set of field names. */
+  private Set<String> varyFields = Collections.emptySet();
+
+  private String contentEncoding;
+  private String transferEncoding;
+  private int contentLength = -1;
+  private String connection;
+
+  public ResponseHeaders(URI uri, RawHeaders headers) {
+    this.uri = uri;
+    this.headers = headers;
+
+    HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
+      @Override public void handle(String directive, String parameter) {
+        if ("no-cache".equalsIgnoreCase(directive)) {
+          noCache = true;
+        } else if ("no-store".equalsIgnoreCase(directive)) {
+          noStore = true;
+        } else if ("max-age".equalsIgnoreCase(directive)) {
+          maxAgeSeconds = HeaderParser.parseSeconds(parameter);
+        } else if ("s-maxage".equalsIgnoreCase(directive)) {
+          sMaxAgeSeconds = HeaderParser.parseSeconds(parameter);
+        } else if ("public".equalsIgnoreCase(directive)) {
+          isPublic = true;
+        } else if ("must-revalidate".equalsIgnoreCase(directive)) {
+          mustRevalidate = true;
+        }
+      }
+    };
+
+    for (int i = 0; i < headers.length(); i++) {
+      String fieldName = headers.getFieldName(i);
+      String value = headers.getValue(i);
+      if ("Cache-Control".equalsIgnoreCase(fieldName)) {
+        HeaderParser.parseCacheControl(value, handler);
+      } else if ("Date".equalsIgnoreCase(fieldName)) {
+        servedDate = HttpDate.parse(value);
+      } else if ("Expires".equalsIgnoreCase(fieldName)) {
+        expires = HttpDate.parse(value);
+      } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
+        lastModified = HttpDate.parse(value);
+      } else if ("ETag".equalsIgnoreCase(fieldName)) {
+        etag = value;
+      } else if ("Pragma".equalsIgnoreCase(fieldName)) {
+        if ("no-cache".equalsIgnoreCase(value)) {
+          noCache = true;
+        }
+      } else if ("Age".equalsIgnoreCase(fieldName)) {
+        ageSeconds = HeaderParser.parseSeconds(value);
+      } else if ("Vary".equalsIgnoreCase(fieldName)) {
+        // Replace the immutable empty set with something we can mutate.
+        if (varyFields.isEmpty()) {
+          varyFields = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+        }
+        for (String varyField : value.split(",")) {
+          varyFields.add(varyField.trim());
+        }
+      } else if ("Content-Encoding".equalsIgnoreCase(fieldName)) {
+        contentEncoding = value;
+      } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
+        transferEncoding = value;
+      } else if ("Content-Length".equalsIgnoreCase(fieldName)) {
+        try {
+          contentLength = Integer.parseInt(value);
+        } catch (NumberFormatException ignored) {
+        }
+      } else if ("Connection".equalsIgnoreCase(fieldName)) {
+        connection = value;
+      } else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) {
+        sentRequestMillis = Long.parseLong(value);
+      } else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
+        receivedResponseMillis = Long.parseLong(value);
+      }
+    }
+  }
+
+  public boolean isContentEncodingGzip() {
+    return "gzip".equalsIgnoreCase(contentEncoding);
+  }
+
+  public void stripContentEncoding() {
+    contentEncoding = null;
+    headers.removeAll("Content-Encoding");
+  }
+
+  public void stripContentLength() {
+    contentLength = -1;
+    headers.removeAll("Content-Length");
+  }
+
+  public boolean isChunked() {
+    return "chunked".equalsIgnoreCase(transferEncoding);
+  }
+
+  public boolean hasConnectionClose() {
+    return "close".equalsIgnoreCase(connection);
+  }
+
+  public URI getUri() {
+    return uri;
+  }
+
+  public RawHeaders getHeaders() {
+    return headers;
+  }
+
+  public Date getServedDate() {
+    return servedDate;
+  }
+
+  public Date getLastModified() {
+    return lastModified;
+  }
+
+  public Date getExpires() {
+    return expires;
+  }
+
+  public boolean isNoCache() {
+    return noCache;
+  }
+
+  public boolean isNoStore() {
+    return noStore;
+  }
+
+  public int getMaxAgeSeconds() {
+    return maxAgeSeconds;
+  }
+
+  public int getSMaxAgeSeconds() {
+    return sMaxAgeSeconds;
+  }
+
+  public boolean isPublic() {
+    return isPublic;
+  }
+
+  public boolean isMustRevalidate() {
+    return mustRevalidate;
+  }
+
+  public String getEtag() {
+    return etag;
+  }
+
+  public Set<String> getVaryFields() {
+    return varyFields;
+  }
+
+  public String getContentEncoding() {
+    return contentEncoding;
+  }
+
+  public int getContentLength() {
+    return contentLength;
+  }
+
+  public String getConnection() {
+    return connection;
+  }
+
+  public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) {
+    this.sentRequestMillis = sentRequestMillis;
+    headers.add(SENT_MILLIS, Long.toString(sentRequestMillis));
+    this.receivedResponseMillis = receivedResponseMillis;
+    headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
+  }
+
+  public void setResponseSource(ResponseSource responseSource) {
+    headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + headers.getResponseCode());
+  }
+
+  /**
+   * Returns the current age of the response, in milliseconds. The calculation
+   * is specified by RFC 2616, 13.2.3 Age Calculations.
+   */
+  private long computeAge(long nowMillis) {
+    long apparentReceivedAge =
+        servedDate != null ? Math.max(0, receivedResponseMillis - servedDate.getTime()) : 0;
+    long receivedAge =
+        ageSeconds != -1 ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds))
+            : apparentReceivedAge;
+    long responseDuration = receivedResponseMillis - sentRequestMillis;
+    long residentDuration = nowMillis - receivedResponseMillis;
+    return receivedAge + responseDuration + residentDuration;
+  }
+
+  /**
+   * Returns the number of milliseconds that the response was fresh for,
+   * starting from the served date.
+   */
+  private long computeFreshnessLifetime() {
+    if (maxAgeSeconds != -1) {
+      return TimeUnit.SECONDS.toMillis(maxAgeSeconds);
+    } else if (expires != null) {
+      long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;
+      long delta = expires.getTime() - servedMillis;
+      return delta > 0 ? delta : 0;
+    } else if (lastModified != null && uri.getRawQuery() == null) {
+      // As recommended by the HTTP RFC and implemented in Firefox, the
+      // max age of a document should be defaulted to 10% of the
+      // document's age at the time it was served. Default expiration
+      // dates aren't used for URIs containing a query.
+      long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
+      long delta = servedMillis - lastModified.getTime();
+      return delta > 0 ? (delta / 10) : 0;
+    }
+    return 0;
+  }
+
+  /**
+   * Returns true if computeFreshnessLifetime used a heuristic. If we used a
+   * heuristic to serve a cached response older than 24 hours, we are required
+   * to attach a warning.
+   */
+  private boolean isFreshnessLifetimeHeuristic() {
+    return maxAgeSeconds == -1 && expires == null;
+  }
+
+  /**
+   * Returns true if this response can be stored to later serve another
+   * request.
+   */
+  public boolean isCacheable(RequestHeaders request) {
+    // Always go to network for uncacheable response codes (RFC 2616, 13.4),
+    // This implementation doesn't support caching partial content.
+    int responseCode = headers.getResponseCode();
+    if (responseCode != HttpURLConnection.HTTP_OK
+        && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
+        && responseCode != HttpURLConnection.HTTP_MULT_CHOICE
+        && responseCode != HttpURLConnection.HTTP_MOVED_PERM
+        && responseCode != HttpURLConnection.HTTP_GONE) {
+      return false;
+    }
+
+    // Responses to authorized requests aren't cacheable unless they include
+    // a 'public', 'must-revalidate' or 's-maxage' directive.
+    if (request.hasAuthorization() && !isPublic && !mustRevalidate && sMaxAgeSeconds == -1) {
+      return false;
+    }
+
+    if (noStore) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Returns true if a Vary header contains an asterisk. Such responses cannot
+   * be cached.
+   */
+  public boolean hasVaryAll() {
+    return varyFields.contains("*");
+  }
+
+  /**
+   * Returns true if none of the Vary headers on this response have changed
+   * between {@code cachedRequest} and {@code newRequest}.
+   */
+  public boolean varyMatches(Map<String, List<String>> cachedRequest,
+      Map<String, List<String>> newRequest) {
+    for (String field : varyFields) {
+      if (!equal(cachedRequest.get(field), newRequest.get(field))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /** Returns the source to satisfy {@code request} given this cached response. */
+  public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) {
+    // If this response shouldn't have been stored, it should never be used
+    // as a response source. This check should be redundant as long as the
+    // persistence store is well-behaved and the rules are constant.
+    if (!isCacheable(request)) {
+      return ResponseSource.NETWORK;
+    }
+
+    if (request.isNoCache() || request.hasConditions()) {
+      return ResponseSource.NETWORK;
+    }
+
+    long ageMillis = computeAge(nowMillis);
+    long freshMillis = computeFreshnessLifetime();
+
+    if (request.getMaxAgeSeconds() != -1) {
+      freshMillis = Math.min(freshMillis, TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds()));
+    }
+
+    long minFreshMillis = 0;
+    if (request.getMinFreshSeconds() != -1) {
+      minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds());
+    }
+
+    long maxStaleMillis = 0;
+    if (!mustRevalidate && request.getMaxStaleSeconds() != -1) {
+      maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds());
+    }
+
+    if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
+      if (ageMillis + minFreshMillis >= freshMillis) {
+        headers.add("Warning", "110 HttpURLConnection \"Response is stale\"");
+      }
+      long oneDayMillis = 24 * 60 * 60 * 1000L;
+      if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
+        headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
+      }
+      return ResponseSource.CACHE;
+    }
+
+    if (lastModified != null) {
+      request.setIfModifiedSince(lastModified);
+    } else if (servedDate != null) {
+      request.setIfModifiedSince(servedDate);
+    }
+
+    if (etag != null) {
+      request.setIfNoneMatch(etag);
+    }
+
+    return request.hasConditions() ? ResponseSource.CONDITIONAL_CACHE : ResponseSource.NETWORK;
+  }
+
+  /**
+   * Returns true if this cached response should be used; false if the
+   * network response should be used.
+   */
+  public boolean validate(ResponseHeaders networkResponse) {
+    if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
+      return true;
+    }
+
+    // The HTTP spec says that if the network's response is older than our
+    // cached response, we may return the cache's response. Like Chrome (but
+    // unlike Firefox), this client prefers to return the newer response.
+    if (lastModified != null
+        && networkResponse.lastModified != null
+        && networkResponse.lastModified.getTime() < lastModified.getTime()) {
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Combines this cached header with a network header as defined by RFC 2616,
+   * 13.5.3.
+   */
+  public ResponseHeaders combine(ResponseHeaders network) throws IOException {
+    RawHeaders result = new RawHeaders();
+    result.setStatusLine(headers.getStatusLine());
+
+    for (int i = 0; i < headers.length(); i++) {
+      String fieldName = headers.getFieldName(i);
+      String value = headers.getValue(i);
+      if ("Warning".equals(fieldName) && value.startsWith("1")) {
+        continue; // drop 100-level freshness warnings
+      }
+      if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) {
+        result.add(fieldName, value);
+      }
+    }
+
+    for (int i = 0; i < network.headers.length(); i++) {
+      String fieldName = network.headers.getFieldName(i);
+      if (isEndToEnd(fieldName)) {
+        result.add(fieldName, network.headers.getValue(i));
+      }
+    }
+
+    return new ResponseHeaders(uri, result);
+  }
+
+  /**
+   * Returns true if {@code fieldName} is an end-to-end HTTP header, as
+   * defined by RFC 2616, 13.5.1.
+   */
+  private static boolean isEndToEnd(String fieldName) {
+    return !"Connection".equalsIgnoreCase(fieldName)
+        && !"Keep-Alive".equalsIgnoreCase(fieldName)
+        && !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
+        && !"Proxy-Authorization".equalsIgnoreCase(fieldName)
+        && !"TE".equalsIgnoreCase(fieldName)
+        && !"Trailers".equalsIgnoreCase(fieldName)
+        && !"Transfer-Encoding".equalsIgnoreCase(fieldName)
+        && !"Upgrade".equalsIgnoreCase(fieldName);
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java b/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java
new file mode 100644
index 0000000..5eb6b76
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java
@@ -0,0 +1,75 @@
+/*
+ * 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.AbstractOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.ProtocolException;
+
+import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+
+/**
+ * An HTTP request body that's completely buffered in memory. This allows
+ * the post body to be transparently re-sent if the HTTP request must be
+ * sent multiple times.
+ */
+final class RetryableOutputStream extends AbstractOutputStream {
+  private final int limit;
+  private final ByteArrayOutputStream content;
+
+  public RetryableOutputStream(int limit) {
+    this.limit = limit;
+    this.content = new ByteArrayOutputStream(limit);
+  }
+
+  public RetryableOutputStream() {
+    this.limit = -1;
+    this.content = new ByteArrayOutputStream();
+  }
+
+  @Override public synchronized void close() throws IOException {
+    if (closed) {
+      return;
+    }
+    closed = true;
+    if (content.size() < limit) {
+      throw new ProtocolException(
+          "content-length promised " + limit + " bytes, but received " + content.size());
+    }
+  }
+
+  @Override public synchronized void write(byte[] buffer, int offset, int count)
+      throws IOException {
+    checkNotClosed();
+    checkOffsetAndCount(buffer.length, offset, count);
+    if (limit != -1 && content.size() > limit - count) {
+      throw new ProtocolException("exceeded content-length limit of " + limit + " bytes");
+    }
+    content.write(buffer, offset, count);
+  }
+
+  public synchronized int contentLength() throws IOException {
+    close();
+    return content.size();
+  }
+
+  public void writeToSocket(OutputStream socketOut) throws IOException {
+    content.writeTo(socketOut);
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java b/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java
new file mode 100644
index 0000000..ce0a71d
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java
@@ -0,0 +1,275 @@
+/*
+ * 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.http;
+
+import com.squareup.okhttp.Address;
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.Route;
+import com.squareup.okhttp.internal.Dns;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import javax.net.ssl.SSLHandshakeException;
+
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
+
+/**
+ * Selects routes to connect to an origin server. Each connection requires a
+ * choice of proxy server, IP address, and TLS mode. Connections may also be
+ * recycled.
+ */
+public final class RouteSelector {
+  /** Uses {@link com.squareup.okhttp.internal.Platform#enableTlsExtensions}. */
+  private static final int TLS_MODE_MODERN = 1;
+  /** Uses {@link com.squareup.okhttp.internal.Platform#supportTlsIntolerantServer}. */
+  private static final int TLS_MODE_COMPATIBLE = 0;
+  /** No TLS mode. */
+  private static final int TLS_MODE_NULL = -1;
+
+  private final Address address;
+  private final URI uri;
+  private final ProxySelector proxySelector;
+  private final ConnectionPool pool;
+  private final Dns dns;
+  private final Set<Route> failedRoutes;
+
+  /* The most recently attempted route. */
+  private Proxy lastProxy;
+  private InetSocketAddress lastInetSocketAddress;
+
+  /* State for negotiating the next proxy to use. */
+  private boolean hasNextProxy;
+  private Proxy userSpecifiedProxy;
+  private Iterator<Proxy> proxySelectorProxies;
+
+  /* State for negotiating the next InetSocketAddress to use. */
+  private InetAddress[] socketAddresses;
+  private int nextSocketAddressIndex;
+  private int socketPort;
+
+  /* State for negotiating the next TLS configuration */
+  private int nextTlsMode = TLS_MODE_NULL;
+
+  /* State for negotiating failed routes */
+  private final List<Route> postponedRoutes;
+
+  public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
+      Dns dns, Set<Route> failedRoutes) {
+    this.address = address;
+    this.uri = uri;
+    this.proxySelector = proxySelector;
+    this.pool = pool;
+    this.dns = dns;
+    this.failedRoutes = failedRoutes;
+    this.postponedRoutes = new LinkedList<Route>();
+
+    resetNextProxy(uri, address.getProxy());
+  }
+
+  /**
+   * Returns true if there's another route to attempt. Every address has at
+   * least one route.
+   */
+  public boolean hasNext() {
+    return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy() || hasNextPostponed();
+  }
+
+  /**
+   * Returns the next route address to attempt.
+   *
+   * @throws NoSuchElementException if there are no more routes to attempt.
+   */
+  public Connection next() throws IOException {
+    // Always prefer pooled connections over new connections.
+    Connection pooled = pool.get(address);
+    if (pooled != null) {
+      return pooled;
+    }
+
+    // Compute the next route to attempt.
+    if (!hasNextTlsMode()) {
+      if (!hasNextInetSocketAddress()) {
+        if (!hasNextProxy()) {
+          if (!hasNextPostponed()) {
+            throw new NoSuchElementException();
+          }
+          return new Connection(nextPostponed());
+        }
+        lastProxy = nextProxy();
+        resetNextInetSocketAddress(lastProxy);
+      }
+      lastInetSocketAddress = nextInetSocketAddress();
+      resetNextTlsMode();
+    }
+
+    boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
+    Route route = new Route(address, lastProxy, lastInetSocketAddress, modernTls);
+    if (failedRoutes.contains(route)) {
+      postponedRoutes.add(route);
+      // We will only recurse in order to skip previously failed routes. They will be
+      // tried last.
+      return next();
+    }
+
+    return new Connection(route);
+  }
+
+  /**
+   * Clients should invoke this method when they encounter a connectivity
+   * failure on a connection returned by this route selector.
+   */
+  public void connectFailed(Connection connection, IOException failure) {
+    Route failedRoute = connection.getRoute();
+    if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) {
+      // Tell the proxy selector when we fail to connect on a fresh connection.
+      proxySelector.connectFailed(uri, failedRoute.getProxy().address(), failure);
+    }
+
+    failedRoutes.add(failedRoute);
+    if (!(failure instanceof SSLHandshakeException)) {
+      // If the problem was not related to SSL then it will also fail with
+      // a different Tls mode therefore we can be proactive about it.
+      failedRoutes.add(failedRoute.flipTlsMode());
+    }
+  }
+
+  /** Resets {@link #nextProxy} to the first option. */
+  private void resetNextProxy(URI uri, Proxy proxy) {
+    this.hasNextProxy = true; // This includes NO_PROXY!
+    if (proxy != null) {
+      this.userSpecifiedProxy = proxy;
+    } else {
+      List<Proxy> proxyList = proxySelector.select(uri);
+      if (proxyList != null) {
+        this.proxySelectorProxies = proxyList.iterator();
+      }
+    }
+  }
+
+  /** Returns true if there's another proxy to try. */
+  private boolean hasNextProxy() {
+    return hasNextProxy;
+  }
+
+  /** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */
+  private Proxy nextProxy() {
+    // If the user specifies a proxy, try that and only that.
+    if (userSpecifiedProxy != null) {
+      hasNextProxy = false;
+      return userSpecifiedProxy;
+    }
+
+    // Try each of the ProxySelector choices until one connection succeeds. If none succeed
+    // then we'll try a direct connection below.
+    if (proxySelectorProxies != null) {
+      while (proxySelectorProxies.hasNext()) {
+        Proxy candidate = proxySelectorProxies.next();
+        if (candidate.type() != Proxy.Type.DIRECT) {
+          return candidate;
+        }
+      }
+    }
+
+    // Finally try a direct connection.
+    hasNextProxy = false;
+    return Proxy.NO_PROXY;
+  }
+
+  /** Resets {@link #nextInetSocketAddress} to the first option. */
+  private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException {
+    socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws!
+
+    String socketHost;
+    if (proxy.type() == Proxy.Type.DIRECT) {
+      socketHost = uri.getHost();
+      socketPort = getEffectivePort(uri);
+    } else {
+      SocketAddress proxyAddress = proxy.address();
+      if (!(proxyAddress instanceof InetSocketAddress)) {
+        throw new IllegalArgumentException(
+            "Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
+      }
+      InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
+      socketHost = proxySocketAddress.getHostName();
+      socketPort = proxySocketAddress.getPort();
+    }
+
+    // Try each address for best behavior in mixed IPv4/IPv6 environments.
+    socketAddresses = dns.getAllByName(socketHost);
+    nextSocketAddressIndex = 0;
+  }
+
+  /** Returns true if there's another socket address to try. */
+  private boolean hasNextInetSocketAddress() {
+    return socketAddresses != null;
+  }
+
+  /** Returns the next socket address to try. */
+  private InetSocketAddress nextInetSocketAddress() throws UnknownHostException {
+    InetSocketAddress result =
+        new InetSocketAddress(socketAddresses[nextSocketAddressIndex++], socketPort);
+    if (nextSocketAddressIndex == socketAddresses.length) {
+      socketAddresses = null; // So that hasNextInetSocketAddress() returns false.
+      nextSocketAddressIndex = 0;
+    }
+
+    return result;
+  }
+
+  /** Resets {@link #nextTlsMode} to the first option. */
+  private void resetNextTlsMode() {
+    nextTlsMode = (address.getSslSocketFactory() != null) ? TLS_MODE_MODERN : TLS_MODE_COMPATIBLE;
+  }
+
+  /** Returns true if there's another TLS mode to try. */
+  private boolean hasNextTlsMode() {
+    return nextTlsMode != TLS_MODE_NULL;
+  }
+
+  /** Returns the next TLS mode to try. */
+  private int nextTlsMode() {
+    if (nextTlsMode == TLS_MODE_MODERN) {
+      nextTlsMode = TLS_MODE_COMPATIBLE;
+      return TLS_MODE_MODERN;
+    } else if (nextTlsMode == TLS_MODE_COMPATIBLE) {
+      nextTlsMode = TLS_MODE_NULL;  // So that hasNextTlsMode() returns false.
+      return TLS_MODE_COMPATIBLE;
+    } else {
+      throw new AssertionError();
+    }
+  }
+
+  /** Returns true if there is another postponed route to try. */
+  private boolean hasNextPostponed() {
+    return !postponedRoutes.isEmpty();
+  }
+
+  /** Returns the next postponed route to try. */
+  private Route nextPostponed() {
+    return postponedRoutes.remove(0);
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/SpdyTransport.java b/framework/src/com/squareup/okhttp/internal/http/SpdyTransport.java
new file mode 100644
index 0000000..18ab566
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/SpdyTransport.java
@@ -0,0 +1,96 @@
+/*
+ * 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.internal.spdy.SpdyConnection;
+import com.squareup.okhttp.internal.spdy.SpdyStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheRequest;
+import java.net.URL;
+import java.util.List;
+
+public final class SpdyTransport implements Transport {
+  private final HttpEngine httpEngine;
+  private final SpdyConnection spdyConnection;
+  private SpdyStream stream;
+
+  public SpdyTransport(HttpEngine httpEngine, SpdyConnection spdyConnection) {
+    this.httpEngine = httpEngine;
+    this.spdyConnection = spdyConnection;
+  }
+
+  @Override public OutputStream createRequestBody() throws IOException {
+    // TODO: if we aren't streaming up to the server, we should buffer the whole request
+    writeRequestHeaders();
+    return stream.getOutputStream();
+  }
+
+  @Override public void writeRequestHeaders() throws IOException {
+    if (stream != null) {
+      return;
+    }
+    httpEngine.writingRequestHeaders();
+    RawHeaders requestHeaders = httpEngine.requestHeaders.getHeaders();
+    String version = httpEngine.connection.getHttpMinorVersion() == 1 ? "HTTP/1.1" : "HTTP/1.0";
+    URL url = httpEngine.policy.getURL();
+    requestHeaders.addSpdyRequestHeaders(httpEngine.method, HttpEngine.requestPath(url), version,
+        HttpEngine.getOriginAddress(url), httpEngine.uri.getScheme());
+    boolean hasRequestBody = httpEngine.hasRequestBody();
+    boolean hasResponseBody = true;
+    stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(), hasRequestBody,
+        hasResponseBody);
+    stream.setReadTimeout(httpEngine.policy.getReadTimeout());
+  }
+
+  @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override public void flushRequest() throws IOException {
+    stream.getOutputStream().close();
+  }
+
+  @Override public ResponseHeaders readResponseHeaders() throws IOException {
+    List<String> nameValueBlock = stream.getResponseHeaders();
+    RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
+    rawHeaders.computeResponseStatusLineFromSpdyHeaders();
+    httpEngine.receiveHeaders(rawHeaders);
+    return new ResponseHeaders(httpEngine.uri, rawHeaders);
+  }
+
+  @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
+    return new UnknownLengthHttpInputStream(stream.getInputStream(), cacheRequest, httpEngine);
+  }
+
+  @Override public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
+      InputStream responseBodyIn) {
+    if (streamCancelled) {
+      if (stream != null) {
+        stream.closeLater(SpdyStream.RST_CANCEL);
+        return true;
+      } else {
+        // If stream is null, it either means that writeRequestHeaders wasn't called
+        // or that SpdyConnection#newStream threw an IOEXception. In both cases there's
+        // nothing to do here and this stream can't be reused.
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/Transport.java b/framework/src/com/squareup/okhttp/internal/http/Transport.java
new file mode 100644
index 0000000..518827e
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/Transport.java
@@ -0,0 +1,64 @@
+/*
+ * 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 java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheRequest;
+
+interface Transport {
+  /**
+   * Returns an output stream where the request body can be written. The
+   * returned stream will of one of two types:
+   * <ul>
+   * <li><strong>Direct.</strong> Bytes are written to the socket and
+   * forgotten. This is most efficient, particularly for large request
+   * bodies. The returned stream may be buffered; the caller must call
+   * {@link #flushRequest} before reading the response.</li>
+   * <li><strong>Buffered.</strong> Bytes are written to an in memory
+   * buffer, and must be explicitly flushed with a call to {@link
+   * #writeRequestBody}. This allows HTTP authorization (401, 407)
+   * responses to be retransmitted transparently.</li>
+   * </ul>
+   */
+  // TODO: don't bother retransmitting the request body? It's quite a corner
+  // case and there's uncertainty whether Firefox or Chrome do this
+  OutputStream createRequestBody() throws IOException;
+
+  /** This should update the HTTP engine's sentRequestMillis field. */
+  void writeRequestHeaders() throws IOException;
+
+  /**
+   * Sends the request body returned by {@link #createRequestBody} to the
+   * remote peer.
+   */
+  void writeRequestBody(RetryableOutputStream requestBody) throws IOException;
+
+  /** Flush the request body to the underlying socket. */
+  void flushRequest() throws IOException;
+
+  /** Read response headers and update the cookie manager. */
+  ResponseHeaders readResponseHeaders() throws IOException;
+
+  // TODO: make this the content stream?
+  InputStream getTransferStream(CacheRequest cacheRequest) throws IOException;
+
+  /** Returns true if the underlying connection can be recycled. */
+  boolean makeReusable(boolean streamReusable, OutputStream requestBodyOut,
+      InputStream responseBodyIn);
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java b/framework/src/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java
new file mode 100644
index 0000000..729e0b9
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java
@@ -0,0 +1,63 @@
+/*
+ * 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 java.io.IOException;
+import java.io.InputStream;
+import java.net.CacheRequest;
+
+import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+
+/** An HTTP message body terminated by the end of the underlying stream. */
+final class UnknownLengthHttpInputStream extends AbstractHttpInputStream {
+  private boolean inputExhausted;
+
+  UnknownLengthHttpInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine)
+      throws IOException {
+    super(is, httpEngine, cacheRequest);
+  }
+
+  @Override public int read(byte[] buffer, int offset, int count) throws IOException {
+    checkOffsetAndCount(buffer.length, offset, count);
+    checkNotClosed();
+    if (in == null || inputExhausted) {
+      return -1;
+    }
+    int read = in.read(buffer, offset, count);
+    if (read == -1) {
+      inputExhausted = true;
+      endOfInput(false);
+      return -1;
+    }
+    cacheWrite(buffer, offset, read);
+    return read;
+  }
+
+  @Override public int available() throws IOException {
+    checkNotClosed();
+    return in == null ? 0 : in.available();
+  }
+
+  @Override public void close() throws IOException {
+    if (closed) {
+      return;
+    }
+    closed = true;
+    if (!inputExhausted) {
+      unexpectedEndOfInput();
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java b/framework/src/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java
new file mode 100644
index 0000000..875fff0
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java
@@ -0,0 +1,36 @@
+/*
+ * 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.spdy;
+
+import java.io.IOException;
+
+/** Listener to be notified when a connected peer creates a new stream. */
+public interface IncomingStreamHandler {
+  IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() {
+    @Override public void receive(SpdyStream stream) throws IOException {
+      stream.close(SpdyStream.RST_REFUSED_STREAM);
+    }
+  };
+
+  /**
+   * Handle a new stream from this connection's peer. Implementations should
+   * respond by either {@link SpdyStream#reply replying to the stream} or
+   * {@link SpdyStream#close closing it}. This response does not need to be
+   * synchronous.
+   */
+  void receive(SpdyStream stream) throws IOException;
+}
diff --git a/framework/src/com/squareup/okhttp/internal/spdy/Ping.java b/framework/src/com/squareup/okhttp/internal/spdy/Ping.java
new file mode 100644
index 0000000..c585255
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/spdy/Ping.java
@@ -0,0 +1,71 @@
+/*
+ * 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.spdy;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A locally-originated ping.
+ */
+public final class Ping {
+  private final CountDownLatch latch = new CountDownLatch(1);
+  private long sent = -1;
+  private long received = -1;
+
+  Ping() {
+  }
+
+  void send() {
+    if (sent != -1) throw new IllegalStateException();
+    sent = System.nanoTime();
+  }
+
+  void receive() {
+    if (received != -1 || sent == -1) throw new IllegalStateException();
+    received = System.nanoTime();
+    latch.countDown();
+  }
+
+  void cancel() {
+    if (received != -1 || sent == -1) throw new IllegalStateException();
+    received = sent - 1;
+    latch.countDown();
+  }
+
+  /**
+   * Returns the round trip time for this ping in nanoseconds, waiting for the
+   * response to arrive if necessary. Returns -1 if the response was
+   * cancelled.
+   */
+  public long roundTripTime() throws InterruptedException {
+    latch.await();
+    return received - sent;
+  }
+
+  /**
+   * Returns the round trip time for this ping in nanoseconds, or -1 if the
+   * response was cancelled, or -2 if the timeout elapsed before the round
+   * trip completed.
+   */
+  public long roundTripTime(long timeout, TimeUnit unit) throws InterruptedException {
+    if (latch.await(timeout, unit)) {
+      return received - sent;
+    } else {
+      return -2;
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/spdy/Settings.java b/framework/src/com/squareup/okhttp/internal/spdy/Settings.java
new file mode 100644
index 0000000..774d791
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/spdy/Settings.java
@@ -0,0 +1,174 @@
+/*
+ * 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.spdy;
+
+final class Settings {
+  /**
+   * From the spdy/3 spec, the default initial window size for all streams is
+   * 64 KiB. (Chrome 25 uses 10 MiB).
+   */
+  static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024;
+
+  /** Peer request to clear durable settings. */
+  static final int FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS = 0x1;
+
+  /** Sent by servers only. The peer requests this setting persisted for future connections. */
+  static final int PERSIST_VALUE = 0x1;
+  /** Sent by clients only. The client is reminding the server of a persisted value. */
+  static final int PERSISTED = 0x2;
+
+  /** Sender's estimate of max incoming kbps. */
+  static final int UPLOAD_BANDWIDTH = 0x1;
+  /** Sender's estimate of max outgoing kbps. */
+  static final int DOWNLOAD_BANDWIDTH = 0x2;
+  /** Sender's estimate of milliseconds between sending a request and receiving a response. */
+  static final int ROUND_TRIP_TIME = 0x3;
+  /** Sender's maximum number of concurrent streams. */
+  static final int MAX_CONCURRENT_STREAMS = 0x4;
+  /** Current CWND in Packets. */
+  static final int CURRENT_CWND = 0x5;
+  /** Retransmission rate. Percentage */
+  static final int DOWNLOAD_RETRANS_RATE = 0x6;
+  /** Window size in bytes. */
+  static final int INITIAL_WINDOW_SIZE = 0x7;
+  /** Window size in bytes. */
+  static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 0x8;
+  /** Total number of settings. */
+  static final int COUNT = 0x9;
+
+  /** Bitfield of which flags that values. */
+  private int set;
+
+  /** Bitfield of flags that have {@link #PERSIST_VALUE}. */
+  private int persistValue;
+
+  /** Bitfield of flags that have {@link #PERSISTED}. */
+  private int persisted;
+
+  /** Flag values. */
+  private final int[] values = new int[COUNT];
+
+  void set(int id, int idFlags, int value) {
+    if (id >= values.length) {
+      return; // Discard unknown settings.
+    }
+
+    int bit = 1 << id;
+    set |= bit;
+    if ((idFlags & PERSIST_VALUE) != 0) {
+      persistValue |= bit;
+    } else {
+      persistValue &= ~bit;
+    }
+    if ((idFlags & PERSISTED) != 0) {
+      persisted |= bit;
+    } else {
+      persisted &= ~bit;
+    }
+
+    values[id] = value;
+  }
+
+  /** Returns true if a value has been assigned for the setting {@code id}. */
+  boolean isSet(int id) {
+    int bit = 1 << id;
+    return (set & bit) != 0;
+  }
+
+  /** Returns the value for the setting {@code id}, or 0 if unset. */
+  int get(int id) {
+    return values[id];
+  }
+
+  /** Returns the flags for the setting {@code id}, or 0 if unset. */
+  int flags(int id) {
+    int result = 0;
+    if (isPersisted(id)) result |= Settings.PERSISTED;
+    if (persistValue(id)) result |= Settings.PERSIST_VALUE;
+    return result;
+  }
+
+  /** Returns the number of settings that have values assigned. */
+  int size() {
+    return Integer.bitCount(set);
+  }
+
+  int getUploadBandwidth(int defaultValue) {
+    int bit = 1 << UPLOAD_BANDWIDTH;
+    return (bit & set) != 0 ? values[UPLOAD_BANDWIDTH] : defaultValue;
+  }
+
+  int getDownloadBandwidth(int defaultValue) {
+    int bit = 1 << DOWNLOAD_BANDWIDTH;
+    return (bit & set) != 0 ? values[DOWNLOAD_BANDWIDTH] : defaultValue;
+  }
+
+  int getRoundTripTime(int defaultValue) {
+    int bit = 1 << ROUND_TRIP_TIME;
+    return (bit & set) != 0 ? values[ROUND_TRIP_TIME] : defaultValue;
+  }
+
+  int getMaxConcurrentStreams(int defaultValue) {
+    int bit = 1 << MAX_CONCURRENT_STREAMS;
+    return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue;
+  }
+
+  int getCurrentCwnd(int defaultValue) {
+    int bit = 1 << CURRENT_CWND;
+    return (bit & set) != 0 ? values[CURRENT_CWND] : defaultValue;
+  }
+
+  int getDownloadRetransRate(int defaultValue) {
+    int bit = 1 << DOWNLOAD_RETRANS_RATE;
+    return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue;
+  }
+
+  int getInitialWindowSize(int defaultValue) {
+    int bit = 1 << INITIAL_WINDOW_SIZE;
+    return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue;
+  }
+
+  int getClientCertificateVectorSize(int defaultValue) {
+    int bit = 1 << CLIENT_CERTIFICATE_VECTOR_SIZE;
+    return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue;
+  }
+
+  /**
+   * Returns true if this user agent should use this setting in future SPDY
+   * connections to the same host.
+   */
+  boolean persistValue(int id) {
+    int bit = 1 << id;
+    return (persistValue & bit) != 0;
+  }
+
+  /** Returns true if this setting was persisted. */
+  boolean isPersisted(int id) {
+    int bit = 1 << id;
+    return (persisted & bit) != 0;
+  }
+
+  /**
+   * Writes {@code other} into this. If any setting is populated by this and
+   * {@code other}, the value and flags from {@code other} will be kept.
+   */
+  void merge(Settings other) {
+    for (int i = 0; i < COUNT; i++) {
+      if (!other.isSet(i)) continue;
+      set(i, other.flags(i), other.get(i));
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java
new file mode 100644
index 0000000..fccd14f
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java
@@ -0,0 +1,579 @@
+/*
+ * 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.spdy;
+
+import com.squareup.okhttp.internal.NamedRunnable;
+import com.squareup.okhttp.internal.Util;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import static java.util.concurrent.Executors.defaultThreadFactory;
+
+/**
+ * A socket connection to a remote peer. A connection hosts streams which can
+ * send and receive data.
+ *
+ * <p>Many methods in this API are <strong>synchronous:</strong> the call is
+ * completed before the method returns. This is typical for Java but atypical
+ * for SPDY. This is motivated by exception transparency: an IOException that
+ * was triggered by a certain caller can be caught and handled by that caller.
+ */
+public final class SpdyConnection implements Closeable {
+
+  // Internal state of this connection is guarded by 'this'. No blocking
+  // operations may be performed while holding this lock!
+  //
+  // Socket writes are guarded by spdyWriter.
+  //
+  // Socket reads are unguarded but are only made by the reader thread.
+  //
+  // Certain operations (like SYN_STREAM) need to synchronize on both the
+  // spdyWriter (to do blocking I/O) and this (to create streams). Such
+  // operations must synchronize on 'this' last. This ensures that we never
+  // wait for a blocking operation while holding 'this'.
+
+  static final int FLAG_FIN = 0x1;
+  static final int FLAG_UNIDIRECTIONAL = 0x2;
+
+  static final int TYPE_DATA = 0x0;
+  static final int TYPE_SYN_STREAM = 0x1;
+  static final int TYPE_SYN_REPLY = 0x2;
+  static final int TYPE_RST_STREAM = 0x3;
+  static final int TYPE_SETTINGS = 0x4;
+  static final int TYPE_NOOP = 0x5;
+  static final int TYPE_PING = 0x6;
+  static final int TYPE_GOAWAY = 0x7;
+  static final int TYPE_HEADERS = 0x8;
+  static final int TYPE_WINDOW_UPDATE = 0x9;
+  static final int TYPE_CREDENTIAL = 0x10;
+  static final int VERSION = 3;
+
+  static final int GOAWAY_OK = 0;
+  static final int GOAWAY_PROTOCOL_ERROR = 1;
+  static final int GOAWAY_INTERNAL_ERROR = 2;
+
+  private static final ExecutorService executor =
+      new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
+          new SynchronousQueue<Runnable>(), defaultThreadFactory());
+
+  /** True if this peer initiated the connection. */
+  final boolean client;
+
+  /**
+   * User code to run in response to an incoming stream. Callbacks must not be
+   * run on the callback executor.
+   */
+  private final IncomingStreamHandler handler;
+  private final SpdyReader spdyReader;
+  private final SpdyWriter spdyWriter;
+
+  private final Map<Integer, SpdyStream> streams = new HashMap<Integer, SpdyStream>();
+  private final String hostName;
+  private int lastGoodStreamId;
+  private int nextStreamId;
+  private boolean shutdown;
+  private long idleStartTimeNs = System.nanoTime();
+
+  /** Lazily-created map of in-flight pings awaiting a response. Guarded by this. */
+  private Map<Integer, Ping> pings;
+  private int nextPingId;
+
+  /** Lazily-created settings for this connection. */
+  Settings settings;
+
+  private SpdyConnection(Builder builder) {
+    client = builder.client;
+    handler = builder.handler;
+    spdyReader = new SpdyReader(builder.in);
+    spdyWriter = new SpdyWriter(builder.out);
+    nextStreamId = builder.client ? 1 : 2;
+    nextPingId = builder.client ? 1 : 2;
+
+    hostName = builder.hostName;
+
+    new Thread(new Reader(), "Spdy Reader " + hostName).start();
+  }
+
+  /**
+   * Returns the number of {@link SpdyStream#isOpen() open streams} on this
+   * connection.
+   */
+  public synchronized int openStreamCount() {
+    return streams.size();
+  }
+
+  private synchronized SpdyStream getStream(int id) {
+    return streams.get(id);
+  }
+
+  synchronized SpdyStream removeStream(int streamId) {
+    SpdyStream stream = streams.remove(streamId);
+    if (stream != null && streams.isEmpty()) {
+      setIdle(true);
+    }
+    return stream;
+  }
+
+  private synchronized void setIdle(boolean value) {
+    idleStartTimeNs = value ? System.nanoTime() : 0L;
+  }
+
+  /** Returns true if this connection is idle. */
+  public synchronized boolean isIdle() {
+    return idleStartTimeNs != 0L;
+  }
+
+  /** Returns the time in ns when this connection became idle or 0L if connection is not idle. */
+  public synchronized long getIdleStartTimeNs() {
+    return idleStartTimeNs;
+  }
+
+  /**
+   * Returns a new locally-initiated stream.
+   *
+   * @param out true to create an output stream that we can use to send data
+   * to the remote peer. Corresponds to {@code FLAG_FIN}.
+   * @param in true to create an input stream that the remote peer can use to
+   * send data to us. Corresponds to {@code FLAG_UNIDIRECTIONAL}.
+   */
+  public SpdyStream newStream(List<String> requestHeaders, boolean out, boolean in)
+      throws IOException {
+    int flags = (out ? 0 : FLAG_FIN) | (in ? 0 : FLAG_UNIDIRECTIONAL);
+    int associatedStreamId = 0;  // TODO: permit the caller to specify an associated stream?
+    int priority = 0; // TODO: permit the caller to specify a priority?
+    int slot = 0; // TODO: permit the caller to specify a slot?
+    SpdyStream stream;
+    int streamId;
+
+    synchronized (spdyWriter) {
+      synchronized (this) {
+        if (shutdown) {
+          throw new IOException("shutdown");
+        }
+        streamId = nextStreamId;
+        nextStreamId += 2;
+        stream = new SpdyStream(streamId, this, flags, priority, slot, requestHeaders, settings);
+        if (stream.isOpen()) {
+          streams.put(streamId, stream);
+          setIdle(false);
+        }
+      }
+
+      spdyWriter.synStream(flags, streamId, associatedStreamId, priority, slot, requestHeaders);
+    }
+
+    return stream;
+  }
+
+  void writeSynReply(int streamId, int flags, List<String> alternating) throws IOException {
+    spdyWriter.synReply(flags, streamId, alternating);
+  }
+
+  /** Writes a complete data frame. */
+  void writeFrame(byte[] bytes, int offset, int length) throws IOException {
+    synchronized (spdyWriter) {
+      spdyWriter.out.write(bytes, offset, length);
+    }
+  }
+
+  void writeSynResetLater(final int streamId, final int statusCode) {
+    executor.submit(
+        new NamedRunnable(String.format("Spdy Writer %s stream %d", hostName, streamId)) {
+          @Override public void execute() {
+            try {
+              writeSynReset(streamId, statusCode);
+            } catch (IOException ignored) {
+            }
+          }
+        });
+  }
+
+  void writeSynReset(int streamId, int statusCode) throws IOException {
+    spdyWriter.rstStream(streamId, statusCode);
+  }
+
+  void writeWindowUpdateLater(final int streamId, final int deltaWindowSize) {
+    executor.submit(
+        new NamedRunnable(String.format("Spdy Writer %s stream %d", hostName, streamId)) {
+          @Override public void execute() {
+            try {
+              writeWindowUpdate(streamId, deltaWindowSize);
+            } catch (IOException ignored) {
+            }
+          }
+        });
+  }
+
+  void writeWindowUpdate(int streamId, int deltaWindowSize) throws IOException {
+    spdyWriter.windowUpdate(streamId, deltaWindowSize);
+  }
+
+  /**
+   * Sends a ping frame to the peer. Use the returned object to await the
+   * ping's response and observe its round trip time.
+   */
+  public Ping ping() throws IOException {
+    Ping ping = new Ping();
+    int pingId;
+    synchronized (this) {
+      if (shutdown) {
+        throw new IOException("shutdown");
+      }
+      pingId = nextPingId;
+      nextPingId += 2;
+      if (pings == null) pings = new HashMap<Integer, Ping>();
+      pings.put(pingId, ping);
+    }
+    writePing(pingId, ping);
+    return ping;
+  }
+
+  private void writePingLater(final int streamId, final Ping ping) {
+    executor.submit(new NamedRunnable(String.format("Spdy Writer %s ping %d", hostName, streamId)) {
+      @Override public void execute() {
+        try {
+          writePing(streamId, ping);
+        } catch (IOException ignored) {
+        }
+      }
+    });
+  }
+
+  private void writePing(int id, Ping ping) throws IOException {
+    synchronized (spdyWriter) {
+      // Observe the sent time immediately before performing I/O.
+      if (ping != null) ping.send();
+      spdyWriter.ping(0, id);
+    }
+  }
+
+  private synchronized Ping removePing(int id) {
+    return pings != null ? pings.remove(id) : null;
+  }
+
+  /** Sends a noop frame to the peer. */
+  public void noop() throws IOException {
+    spdyWriter.noop();
+  }
+
+  public void flush() throws IOException {
+    synchronized (spdyWriter) {
+      spdyWriter.out.flush();
+    }
+  }
+
+  /**
+   * Degrades this connection such that new streams can neither be created
+   * locally, nor accepted from the remote peer. Existing streams are not
+   * impacted. This is intended to permit an endpoint to gracefully stop
+   * accepting new requests without harming previously established streams.
+   *
+   * @param statusCode one of {@link #GOAWAY_OK}, {@link
+   * #GOAWAY_INTERNAL_ERROR} or {@link #GOAWAY_PROTOCOL_ERROR}.
+   */
+  public void shutdown(int statusCode) throws IOException {
+    synchronized (spdyWriter) {
+      int lastGoodStreamId;
+      synchronized (this) {
+        if (shutdown) {
+          return;
+        }
+        shutdown = true;
+        lastGoodStreamId = this.lastGoodStreamId;
+      }
+      spdyWriter.goAway(0, lastGoodStreamId, statusCode);
+    }
+  }
+
+  /**
+   * Closes this connection. This cancels all open streams and unanswered
+   * pings. It closes the underlying input and output streams and shuts down
+   * internal executor services.
+   */
+  @Override public void close() throws IOException {
+    close(GOAWAY_OK, SpdyStream.RST_CANCEL);
+  }
+
+  private void close(int shutdownStatusCode, int rstStatusCode) throws IOException {
+    assert (!Thread.holdsLock(this));
+    IOException thrown = null;
+    try {
+      shutdown(shutdownStatusCode);
+    } catch (IOException e) {
+      thrown = e;
+    }
+
+    SpdyStream[] streamsToClose = null;
+    Ping[] pingsToCancel = null;
+    synchronized (this) {
+      if (!streams.isEmpty()) {
+        streamsToClose = streams.values().toArray(new SpdyStream[streams.size()]);
+        streams.clear();
+        setIdle(false);
+      }
+      if (pings != null) {
+        pingsToCancel = pings.values().toArray(new Ping[pings.size()]);
+        pings = null;
+      }
+    }
+
+    if (streamsToClose != null) {
+      for (SpdyStream stream : streamsToClose) {
+        try {
+          stream.close(rstStatusCode);
+        } catch (IOException e) {
+          if (thrown != null) thrown = e;
+        }
+      }
+    }
+
+    if (pingsToCancel != null) {
+      for (Ping ping : pingsToCancel) {
+        ping.cancel();
+      }
+    }
+
+    try {
+      spdyReader.close();
+    } catch (IOException e) {
+      thrown = e;
+    }
+    try {
+      spdyWriter.close();
+    } catch (IOException e) {
+      if (thrown == null) thrown = e;
+    }
+
+    if (thrown != null) throw thrown;
+  }
+
+  public static class Builder {
+    private String hostName;
+    private InputStream in;
+    private OutputStream out;
+    private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS;
+    public boolean client;
+
+    public Builder(boolean client, Socket socket) throws IOException {
+      this("", client, socket.getInputStream(), socket.getOutputStream());
+    }
+
+    public Builder(boolean client, InputStream in, OutputStream out) {
+      this("", client, in, out);
+    }
+
+    /**
+     * @param client true if this peer initiated the connection; false if
+     * this peer accepted the connection.
+     */
+    public Builder(String hostName, boolean client, Socket socket) throws IOException {
+      this(hostName, client, socket.getInputStream(), socket.getOutputStream());
+    }
+
+    /**
+     * @param client true if this peer initiated the connection; false if this
+     * peer accepted the connection.
+     */
+    public Builder(String hostName, boolean client, InputStream in, OutputStream out) {
+      this.hostName = hostName;
+      this.client = client;
+      this.in = in;
+      this.out = out;
+    }
+
+    public Builder handler(IncomingStreamHandler handler) {
+      this.handler = handler;
+      return this;
+    }
+
+    public SpdyConnection build() {
+      return new SpdyConnection(this);
+    }
+  }
+
+  private class Reader implements Runnable, SpdyReader.Handler {
+    @Override public void run() {
+      int shutdownStatusCode = GOAWAY_INTERNAL_ERROR;
+      int rstStatusCode = SpdyStream.RST_INTERNAL_ERROR;
+      try {
+        while (spdyReader.nextFrame(this)) {
+        }
+        shutdownStatusCode = GOAWAY_OK;
+        rstStatusCode = SpdyStream.RST_CANCEL;
+      } catch (IOException e) {
+        shutdownStatusCode = GOAWAY_PROTOCOL_ERROR;
+        rstStatusCode = SpdyStream.RST_PROTOCOL_ERROR;
+      } finally {
+        try {
+          close(shutdownStatusCode, rstStatusCode);
+        } catch (IOException ignored) {
+        }
+      }
+    }
+
+    @Override public void data(int flags, int streamId, InputStream in, int length)
+        throws IOException {
+      SpdyStream dataStream = getStream(streamId);
+      if (dataStream == null) {
+        writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM);
+        Util.skipByReading(in, length);
+        return;
+      }
+      dataStream.receiveData(in, length);
+      if ((flags & SpdyConnection.FLAG_FIN) != 0) {
+        dataStream.receiveFin();
+      }
+    }
+
+    @Override
+    public void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot,
+        List<String> nameValueBlock) {
+      final SpdyStream synStream;
+      final SpdyStream previous;
+      synchronized (SpdyConnection.this) {
+        synStream =
+            new SpdyStream(streamId, SpdyConnection.this, flags, priority, slot, nameValueBlock,
+                settings);
+        if (shutdown) {
+          return;
+        }
+        lastGoodStreamId = streamId;
+        previous = streams.put(streamId, synStream);
+      }
+      if (previous != null) {
+        previous.closeLater(SpdyStream.RST_PROTOCOL_ERROR);
+        removeStream(streamId);
+        return;
+      }
+
+      executor.submit(
+          new NamedRunnable(String.format("Callback %s stream %d", hostName, streamId)) {
+        @Override public void execute() {
+          try {
+            handler.receive(synStream);
+          } catch (IOException e) {
+            throw new RuntimeException(e);
+          }
+        }
+      });
+    }
+
+    @Override public void synReply(int flags, int streamId, List<String> nameValueBlock)
+        throws IOException {
+      SpdyStream replyStream = getStream(streamId);
+      if (replyStream == null) {
+        writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM);
+        return;
+      }
+      replyStream.receiveReply(nameValueBlock);
+      if ((flags & SpdyConnection.FLAG_FIN) != 0) {
+        replyStream.receiveFin();
+      }
+    }
+
+    @Override public void headers(int flags, int streamId, List<String> nameValueBlock)
+        throws IOException {
+      SpdyStream replyStream = getStream(streamId);
+      if (replyStream != null) {
+        replyStream.receiveHeaders(nameValueBlock);
+      }
+    }
+
+    @Override public void rstStream(int flags, int streamId, int statusCode) {
+      SpdyStream rstStream = removeStream(streamId);
+      if (rstStream != null) {
+        rstStream.receiveRstStream(statusCode);
+      }
+    }
+
+    @Override public void settings(int flags, Settings newSettings) {
+      SpdyStream[] streamsToNotify = null;
+      synchronized (SpdyConnection.this) {
+        if (settings == null || (flags & Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS) != 0) {
+          settings = newSettings;
+        } else {
+          settings.merge(newSettings);
+        }
+        if (!streams.isEmpty()) {
+          streamsToNotify = streams.values().toArray(new SpdyStream[streams.size()]);
+        }
+      }
+      if (streamsToNotify != null) {
+        for (SpdyStream stream : streamsToNotify) {
+          // The synchronization here is ugly. We need to synchronize on 'this' to guard
+          // reads to 'settings'. We synchronize on 'stream' to guard the state change.
+          // And we need to acquire the 'stream' lock first, since that may block.
+          synchronized (stream) {
+            synchronized (this) {
+              stream.receiveSettings(settings);
+            }
+          }
+        }
+      }
+    }
+
+    @Override public void noop() {
+    }
+
+    @Override public void ping(int flags, int streamId) {
+      if (client != (streamId % 2 == 1)) {
+        // Respond to a client ping if this is a server and vice versa.
+        writePingLater(streamId, null);
+      } else {
+        Ping ping = removePing(streamId);
+        if (ping != null) {
+          ping.receive();
+        }
+      }
+    }
+
+    @Override public void goAway(int flags, int lastGoodStreamId, int statusCode) {
+      synchronized (SpdyConnection.this) {
+        shutdown = true;
+
+        // Fail all streams created after the last good stream ID.
+        for (Iterator<Map.Entry<Integer, SpdyStream>> i = streams.entrySet().iterator();
+            i.hasNext(); ) {
+          Map.Entry<Integer, SpdyStream> entry = i.next();
+          int streamId = entry.getKey();
+          if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) {
+            entry.getValue().receiveRstStream(SpdyStream.RST_REFUSED_STREAM);
+            i.remove();
+          }
+        }
+      }
+    }
+
+    @Override public void windowUpdate(int flags, int streamId, int deltaWindowSize) {
+      SpdyStream stream = getStream(streamId);
+      if (stream != null) {
+        stream.receiveWindowUpdate(deltaWindowSize);
+      }
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java b/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java
new file mode 100644
index 0000000..7d3f2bd
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java
@@ -0,0 +1,326 @@
+/*
+ * 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.spdy;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.ProtocolException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/** Read spdy/3 frames. */
+final class SpdyReader implements Closeable {
+  static final byte[] DICTIONARY;
+  static {
+    try {
+      DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
+          + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
+          + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
+          + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
+          + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
+          + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
+          + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000"
+          + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000"
+          + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000"
+          + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type"
+          + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe"
+          + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000"
+          + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since"
+          + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000"
+          + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati"
+          + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000"
+          + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000"
+          + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after"
+          + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai"
+          + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000"
+          + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via"
+          + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000"
+          + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000"
+          + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1"
+          + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo"
+          + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
+          + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
+          + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
+          + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
+          + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th"
+          + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml"
+          + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate,"
+          + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8.name());
+    } catch (UnsupportedEncodingException e) {
+      throw new AssertionError();
+    }
+  }
+
+  private final DataInputStream in;
+  private final DataInputStream nameValueBlockIn;
+  private int compressedLimit;
+
+  SpdyReader(InputStream in) {
+    this.in = new DataInputStream(in);
+    this.nameValueBlockIn = newNameValueBlockStream();
+  }
+
+  /**
+   * Send the next frame to {@code handler}. Returns true unless there are no
+   * more frames on the stream.
+   */
+  public boolean nextFrame(Handler handler) throws IOException {
+    int w1;
+    try {
+      w1 = in.readInt();
+    } catch (IOException e) {
+      return false; // This might be a normal socket close.
+    }
+    int w2 = in.readInt();
+
+    boolean control = (w1 & 0x80000000) != 0;
+    int flags = (w2 & 0xff000000) >>> 24;
+    int length = (w2 & 0xffffff);
+
+    if (control) {
+      int version = (w1 & 0x7fff0000) >>> 16;
+      int type = (w1 & 0xffff);
+
+      if (version != 3) {
+        throw new ProtocolException("version != 3: " + version);
+      }
+
+      switch (type) {
+        case SpdyConnection.TYPE_SYN_STREAM:
+          readSynStream(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_SYN_REPLY:
+          readSynReply(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_RST_STREAM:
+          readRstStream(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_SETTINGS:
+          readSettings(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_NOOP:
+          if (length != 0) throw ioException("TYPE_NOOP length: %d != 0", length);
+          handler.noop();
+          return true;
+
+        case SpdyConnection.TYPE_PING:
+          readPing(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_GOAWAY:
+          readGoAway(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_HEADERS:
+          readHeaders(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_WINDOW_UPDATE:
+          readWindowUpdate(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_CREDENTIAL:
+          Util.skipByReading(in, length);
+          throw new UnsupportedOperationException("TODO"); // TODO: implement
+
+        default:
+          throw new IOException("Unexpected frame");
+      }
+    } else {
+      int streamId = w1 & 0x7fffffff;
+      handler.data(flags, streamId, in, length);
+      return true;
+    }
+  }
+
+  private void readSynStream(Handler handler, int flags, int length) throws IOException {
+    int w1 = in.readInt();
+    int w2 = in.readInt();
+    int s3 = in.readShort();
+    int streamId = w1 & 0x7fffffff;
+    int associatedStreamId = w2 & 0x7fffffff;
+    int priority = (s3 & 0xe000) >>> 13;
+    int slot = s3 & 0xff;
+    List<String> nameValueBlock = readNameValueBlock(length - 10);
+    handler.synStream(flags, streamId, associatedStreamId, priority, slot, nameValueBlock);
+  }
+
+  private void readSynReply(Handler handler, int flags, int length) throws IOException {
+    int w1 = in.readInt();
+    int streamId = w1 & 0x7fffffff;
+    List<String> nameValueBlock = readNameValueBlock(length - 4);
+    handler.synReply(flags, streamId, nameValueBlock);
+  }
+
+  private void readRstStream(Handler handler, int flags, int length) throws IOException {
+    if (length != 8) throw ioException("TYPE_RST_STREAM length: %d != 8", length);
+    int streamId = in.readInt() & 0x7fffffff;
+    int statusCode = in.readInt();
+    handler.rstStream(flags, streamId, statusCode);
+  }
+
+  private void readHeaders(Handler handler, int flags, int length) throws IOException {
+    int w1 = in.readInt();
+    int streamId = w1 & 0x7fffffff;
+    List<String> nameValueBlock = readNameValueBlock(length - 4);
+    handler.headers(flags, streamId, nameValueBlock);
+  }
+
+  private void readWindowUpdate(Handler handler, int flags, int length) throws IOException {
+    if (length != 8) throw ioException("TYPE_WINDOW_UPDATE length: %d != 8", length);
+    int w1 = in.readInt();
+    int w2 = in.readInt();
+    int streamId = w1 & 0x7fffffff;
+    int deltaWindowSize = w2 & 0x7fffffff;
+    handler.windowUpdate(flags, streamId, deltaWindowSize);
+  }
+
+  private DataInputStream newNameValueBlockStream() {
+    // Limit the inflater input stream to only those bytes in the Name/Value block.
+    final InputStream throttleStream = new InputStream() {
+      @Override public int read() throws IOException {
+        return Util.readSingleByte(this);
+      }
+
+      @Override public int read(byte[] buffer, int offset, int byteCount) throws IOException {
+        byteCount = Math.min(byteCount, compressedLimit);
+        int consumed = in.read(buffer, offset, byteCount);
+        compressedLimit -= consumed;
+        return consumed;
+      }
+
+      @Override public void close() throws IOException {
+        in.close();
+      }
+    };
+
+    // Subclass inflater to install a dictionary when it's needed.
+    Inflater inflater = new Inflater() {
+      @Override
+      public int inflate(byte[] buffer, int offset, int count) throws DataFormatException {
+        int result = super.inflate(buffer, offset, count);
+        if (result == 0 && needsDictionary()) {
+          setDictionary(DICTIONARY);
+          result = super.inflate(buffer, offset, count);
+        }
+        return result;
+      }
+    };
+
+    return new DataInputStream(new InflaterInputStream(throttleStream, inflater));
+  }
+
+  private List<String> readNameValueBlock(int length) throws IOException {
+    this.compressedLimit += length;
+    try {
+      int numberOfPairs = nameValueBlockIn.readInt();
+      if (numberOfPairs < 0) {
+        Logger.getLogger(getClass().getName()).warning("numberOfPairs < 0: " + numberOfPairs);
+        throw ioException("numberOfPairs < 0");
+      }
+      List<String> entries = new ArrayList<String>(numberOfPairs * 2);
+      for (int i = 0; i < numberOfPairs; i++) {
+        String name = readString();
+        String values = readString();
+        if (name.length() == 0) throw ioException("name.length == 0");
+        if (values.length() == 0) throw ioException("values.length == 0");
+        entries.add(name);
+        entries.add(values);
+      }
+
+      if (compressedLimit != 0) {
+        Logger.getLogger(getClass().getName()).warning("compressedLimit > 0: " + compressedLimit);
+      }
+
+      return entries;
+    } catch (DataFormatException e) {
+      throw new IOException(e.getMessage());
+    }
+  }
+
+  private String readString() throws DataFormatException, IOException {
+    int length = nameValueBlockIn.readInt();
+    byte[] bytes = new byte[length];
+    Util.readFully(nameValueBlockIn, bytes);
+    return new String(bytes, 0, length, "UTF-8");
+  }
+
+  private void readPing(Handler handler, int flags, int length) throws IOException {
+    if (length != 4) throw ioException("TYPE_PING length: %d != 4", length);
+    int id = in.readInt();
+    handler.ping(flags, id);
+  }
+
+  private void readGoAway(Handler handler, int flags, int length) throws IOException {
+    if (length != 8) throw ioException("TYPE_GOAWAY length: %d != 8", length);
+    int lastGoodStreamId = in.readInt() & 0x7fffffff;
+    int statusCode = in.readInt();
+    handler.goAway(flags, lastGoodStreamId, statusCode);
+  }
+
+  private void readSettings(Handler handler, int flags, int length) throws IOException {
+    int numberOfEntries = in.readInt();
+    if (length != 4 + 8 * numberOfEntries) {
+      throw ioException("TYPE_SETTINGS length: %d != 4 + 8 * %d", length, numberOfEntries);
+    }
+    Settings settings = new Settings();
+    for (int i = 0; i < numberOfEntries; i++) {
+      int w1 = in.readInt();
+      int value = in.readInt();
+      int idFlags = (w1 & 0xff000000) >>> 24;
+      int id = w1 & 0xffffff;
+      settings.set(id, idFlags, value);
+    }
+    handler.settings(flags, settings);
+  }
+
+  private static IOException ioException(String message, Object... args) throws IOException {
+    throw new IOException(String.format(message, args));
+  }
+
+  @Override public void close() throws IOException {
+    Util.closeAll(in, nameValueBlockIn);
+  }
+
+  public interface Handler {
+    void data(int flags, int streamId, InputStream in, int length) throws IOException;
+
+    void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot,
+        List<String> nameValueBlock);
+
+    void synReply(int flags, int streamId, List<String> nameValueBlock) throws IOException;
+    void headers(int flags, int streamId, List<String> nameValueBlock) throws IOException;
+    void rstStream(int flags, int streamId, int statusCode);
+    void settings(int flags, Settings settings);
+    void noop();
+    void ping(int flags, int streamId);
+    void goAway(int flags, int lastGoodStreamId, int statusCode);
+    void windowUpdate(int flags, int streamId, int deltaWindowSize);
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/spdy/SpdyStream.java b/framework/src/com/squareup/okhttp/internal/spdy/SpdyStream.java
new file mode 100644
index 0000000..744a04e
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/spdy/SpdyStream.java
@@ -0,0 +1,733 @@
+/*
+ * 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.spdy;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+import static com.squareup.okhttp.internal.Util.pokeInt;
+import static java.nio.ByteOrder.BIG_ENDIAN;
+
+/** A logical bidirectional stream. */
+public final class SpdyStream {
+
+  // Internal state is guarded by this. No long-running or potentially
+  // blocking operations are performed while the lock is held.
+
+  private static final int DATA_FRAME_HEADER_LENGTH = 8;
+
+  private static final String[] STATUS_CODE_NAMES = {
+      null,
+      "PROTOCOL_ERROR",
+      "INVALID_STREAM",
+      "REFUSED_STREAM",
+      "UNSUPPORTED_VERSION",
+      "CANCEL",
+      "INTERNAL_ERROR",
+      "FLOW_CONTROL_ERROR",
+      "STREAM_IN_USE",
+      "STREAM_ALREADY_CLOSED",
+      "INVALID_CREDENTIALS",
+      "FRAME_TOO_LARGE"
+  };
+
+  public static final int RST_PROTOCOL_ERROR = 1;
+  public static final int RST_INVALID_STREAM = 2;
+  public static final int RST_REFUSED_STREAM = 3;
+  public static final int RST_UNSUPPORTED_VERSION = 4;
+  public static final int RST_CANCEL = 5;
+  public static final int RST_INTERNAL_ERROR = 6;
+  public static final int RST_FLOW_CONTROL_ERROR = 7;
+  public static final int RST_STREAM_IN_USE = 8;
+  public static final int RST_STREAM_ALREADY_CLOSED = 9;
+  public static final int RST_INVALID_CREDENTIALS = 10;
+  public static final int RST_FRAME_TOO_LARGE = 11;
+
+  /**
+   * The number of unacknowledged bytes at which the input stream will send
+   * the peer a {@code WINDOW_UPDATE} frame. Must be less than this client's
+   * window size, otherwise the remote peer will stop sending data on this
+   * stream. (Chrome 25 uses 5 MiB.)
+   */
+  public static final int WINDOW_UPDATE_THRESHOLD = Settings.DEFAULT_INITIAL_WINDOW_SIZE / 2;
+
+  private final int id;
+  private final SpdyConnection connection;
+  private final int priority;
+  private final int slot;
+  private long readTimeoutMillis = 0;
+  private int writeWindowSize;
+
+  /** Headers sent by the stream initiator. Immutable and non null. */
+  private final List<String> requestHeaders;
+
+  /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */
+  private List<String> responseHeaders;
+
+  private final SpdyDataInputStream in = new SpdyDataInputStream();
+  private final SpdyDataOutputStream out = new SpdyDataOutputStream();
+
+  /**
+   * The reason why this stream was abnormally closed. If there are multiple
+   * reasons to abnormally close this stream (such as both peers closing it
+   * near-simultaneously) then this is the first reason known to this peer.
+   */
+  private int rstStatusCode = -1;
+
+  SpdyStream(int id, SpdyConnection connection, int flags, int priority, int slot,
+      List<String> requestHeaders, Settings settings) {
+    if (connection == null) throw new NullPointerException("connection == null");
+    if (requestHeaders == null) throw new NullPointerException("requestHeaders == null");
+    this.id = id;
+    this.connection = connection;
+    this.priority = priority;
+    this.slot = slot;
+    this.requestHeaders = requestHeaders;
+
+    if (isLocallyInitiated()) {
+      // I am the sender
+      in.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0;
+      out.finished = (flags & SpdyConnection.FLAG_FIN) != 0;
+    } else {
+      // I am the receiver
+      in.finished = (flags & SpdyConnection.FLAG_FIN) != 0;
+      out.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0;
+    }
+
+    setSettings(settings);
+  }
+
+  /**
+   * Returns true if this stream is open. A stream is open until either:
+   * <ul>
+   * <li>A {@code SYN_RESET} frame abnormally terminates the stream.
+   * <li>Both input and output streams have transmitted all data and
+   * headers.
+   * </ul>
+   * Note that the input stream may continue to yield data even after a stream
+   * reports itself as not open. This is because input data is buffered.
+   */
+  public synchronized boolean isOpen() {
+    if (rstStatusCode != -1) {
+      return false;
+    }
+    if ((in.finished || in.closed) && (out.finished || out.closed) && responseHeaders != null) {
+      return false;
+    }
+    return true;
+  }
+
+  /** Returns true if this stream was created by this peer. */
+  public boolean isLocallyInitiated() {
+    boolean streamIsClient = (id % 2 == 1);
+    return connection.client == streamIsClient;
+  }
+
+  public SpdyConnection getConnection() {
+    return connection;
+  }
+
+  public List<String> getRequestHeaders() {
+    return requestHeaders;
+  }
+
+  /**
+   * Returns the stream's response headers, blocking if necessary if they
+   * have not been received yet.
+   */
+  public synchronized List<String> getResponseHeaders() throws IOException {
+    try {
+      while (responseHeaders == null && rstStatusCode == -1) {
+        wait();
+      }
+      if (responseHeaders != null) {
+        return responseHeaders;
+      }
+      throw new IOException("stream was reset: " + rstStatusString());
+    } catch (InterruptedException e) {
+      InterruptedIOException rethrow = new InterruptedIOException();
+      rethrow.initCause(e);
+      throw rethrow;
+    }
+  }
+
+  /**
+   * Returns the reason why this stream was closed, or -1 if it closed
+   * normally or has not yet been closed. Valid reasons are {@link
+   * #RST_PROTOCOL_ERROR}, {@link #RST_INVALID_STREAM}, {@link
+   * #RST_REFUSED_STREAM}, {@link #RST_UNSUPPORTED_VERSION}, {@link
+   * #RST_CANCEL}, {@link #RST_INTERNAL_ERROR} and {@link
+   * #RST_FLOW_CONTROL_ERROR}.
+   */
+  public synchronized int getRstStatusCode() {
+    return rstStatusCode;
+  }
+
+  /**
+   * Sends a reply to an incoming stream.
+   *
+   * @param out true to create an output stream that we can use to send data
+   * to the remote peer. Corresponds to {@code FLAG_FIN}.
+   */
+  public void reply(List<String> responseHeaders, boolean out) throws IOException {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    int flags = 0;
+    synchronized (this) {
+      if (responseHeaders == null) {
+        throw new NullPointerException("responseHeaders == null");
+      }
+      if (isLocallyInitiated()) {
+        throw new IllegalStateException("cannot reply to a locally initiated stream");
+      }
+      if (this.responseHeaders != null) {
+        throw new IllegalStateException("reply already sent");
+      }
+      this.responseHeaders = responseHeaders;
+      if (!out) {
+        this.out.finished = true;
+        flags |= SpdyConnection.FLAG_FIN;
+      }
+    }
+    connection.writeSynReply(id, flags, responseHeaders);
+  }
+
+  /**
+   * Sets the maximum time to wait on input stream reads before failing with a
+   * {@code SocketTimeoutException}, or {@code 0} to wait indefinitely.
+   */
+  public void setReadTimeout(long readTimeoutMillis) {
+    this.readTimeoutMillis = readTimeoutMillis;
+  }
+
+  public long getReadTimeoutMillis() {
+    return readTimeoutMillis;
+  }
+
+  /** Returns an input stream that can be used to read data from the peer. */
+  public InputStream getInputStream() {
+    return in;
+  }
+
+  /**
+   * Returns an output stream that can be used to write data to the peer.
+   *
+   * @throws IllegalStateException if this stream was initiated by the peer
+   * and a {@link #reply} has not yet been sent.
+   */
+  public OutputStream getOutputStream() {
+    synchronized (this) {
+      if (responseHeaders == null && !isLocallyInitiated()) {
+        throw new IllegalStateException("reply before requesting the output stream");
+      }
+    }
+    return out;
+  }
+
+  /**
+   * Abnormally terminate this stream. This blocks until the {@code RST_STREAM}
+   * frame has been transmitted.
+   */
+  public void close(int rstStatusCode) throws IOException {
+    if (!closeInternal(rstStatusCode)) {
+      return; // Already closed.
+    }
+    connection.writeSynReset(id, rstStatusCode);
+  }
+
+  /**
+   * Abnormally terminate this stream. This enqueues a {@code RST_STREAM}
+   * frame and returns immediately.
+   */
+  public void closeLater(int rstStatusCode) {
+    if (!closeInternal(rstStatusCode)) {
+      return; // Already closed.
+    }
+    connection.writeSynResetLater(id, rstStatusCode);
+  }
+
+  /** Returns true if this stream was closed. */
+  private boolean closeInternal(int rstStatusCode) {
+    assert (!Thread.holdsLock(this));
+    synchronized (this) {
+      if (this.rstStatusCode != -1) {
+        return false;
+      }
+      if (in.finished && out.finished) {
+        return false;
+      }
+      this.rstStatusCode = rstStatusCode;
+      notifyAll();
+    }
+    connection.removeStream(id);
+    return true;
+  }
+
+  void receiveReply(List<String> strings) throws IOException {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    boolean streamInUseError = false;
+    boolean open = true;
+    synchronized (this) {
+      if (isLocallyInitiated() && responseHeaders == null) {
+        responseHeaders = strings;
+        open = isOpen();
+        notifyAll();
+      } else {
+        streamInUseError = true;
+      }
+    }
+    if (streamInUseError) {
+      closeLater(SpdyStream.RST_STREAM_IN_USE);
+    } else if (!open) {
+      connection.removeStream(id);
+    }
+  }
+
+  void receiveHeaders(List<String> headers) throws IOException {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    boolean protocolError = false;
+    synchronized (this) {
+      if (responseHeaders != null) {
+        List<String> newHeaders = new ArrayList<String>();
+        newHeaders.addAll(responseHeaders);
+        newHeaders.addAll(headers);
+        this.responseHeaders = newHeaders;
+      } else {
+        protocolError = true;
+      }
+    }
+    if (protocolError) {
+      closeLater(SpdyStream.RST_PROTOCOL_ERROR);
+    }
+  }
+
+  void receiveData(InputStream in, int length) throws IOException {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    this.in.receive(in, length);
+  }
+
+  void receiveFin() {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    boolean open;
+    synchronized (this) {
+      this.in.finished = true;
+      open = isOpen();
+      notifyAll();
+    }
+    if (!open) {
+      connection.removeStream(id);
+    }
+  }
+
+  synchronized void receiveRstStream(int statusCode) {
+    if (rstStatusCode == -1) {
+      rstStatusCode = statusCode;
+      notifyAll();
+    }
+  }
+
+  private void setSettings(Settings settings) {
+    assert (Thread.holdsLock(connection)); // Because 'settings' is guarded by 'connection'.
+    this.writeWindowSize =
+        settings != null ? settings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE)
+            : Settings.DEFAULT_INITIAL_WINDOW_SIZE;
+  }
+
+  void receiveSettings(Settings settings) {
+    assert (Thread.holdsLock(this));
+    setSettings(settings);
+    notifyAll();
+  }
+
+  synchronized void receiveWindowUpdate(int deltaWindowSize) {
+    out.unacknowledgedBytes -= deltaWindowSize;
+    notifyAll();
+  }
+
+  private String rstStatusString() {
+    return rstStatusCode > 0 && rstStatusCode < STATUS_CODE_NAMES.length
+        ? STATUS_CODE_NAMES[rstStatusCode] : Integer.toString(rstStatusCode);
+  }
+
+  int getPriority() {
+    return priority;
+  }
+
+  int getSlot() {
+    return slot;
+  }
+
+  /**
+   * An input stream that reads the incoming data frames of a stream. Although
+   * this class uses synchronization to safely receive incoming data frames,
+   * it is not intended for use by multiple readers.
+   */
+  private final class SpdyDataInputStream extends InputStream {
+    // Store incoming data bytes in a circular buffer. When the buffer is
+    // empty, pos == -1. Otherwise pos is the first byte to read and limit
+    // is the first byte to write.
+    //
+    // { - - - X X X X - - - }
+    //         ^       ^
+    //        pos    limit
+    //
+    // { X X X - - - - X X X }
+    //         ^       ^
+    //       limit    pos
+
+    private final byte[] buffer = new byte[Settings.DEFAULT_INITIAL_WINDOW_SIZE];
+
+    /** the next byte to be read, or -1 if the buffer is empty. Never buffer.length */
+    private int pos = -1;
+
+    /** the last byte to be read. Never buffer.length */
+    private int limit;
+
+    /** True if the caller has closed this stream. */
+    private boolean closed;
+
+    /**
+     * True if either side has cleanly shut down this stream. We will
+     * receive no more bytes beyond those already in the buffer.
+     */
+    private boolean finished;
+
+    /**
+     * The total number of bytes consumed by the application (with {@link
+     * #read}), but not yet acknowledged by sending a {@code WINDOW_UPDATE}
+     * frame.
+     */
+    private int unacknowledgedBytes = 0;
+
+    @Override public int available() throws IOException {
+      synchronized (SpdyStream.this) {
+        checkNotClosed();
+        if (pos == -1) {
+          return 0;
+        } else if (limit > pos) {
+          return limit - pos;
+        } else {
+          return limit + (buffer.length - pos);
+        }
+      }
+    }
+
+    @Override public int read() throws IOException {
+      return Util.readSingleByte(this);
+    }
+
+    @Override public int read(byte[] b, int offset, int count) throws IOException {
+      synchronized (SpdyStream.this) {
+        checkOffsetAndCount(b.length, offset, count);
+        waitUntilReadable();
+        checkNotClosed();
+
+        if (pos == -1) {
+          return -1;
+        }
+
+        int copied = 0;
+
+        // drain from [pos..buffer.length)
+        if (limit <= pos) {
+          int bytesToCopy = Math.min(count, buffer.length - pos);
+          System.arraycopy(buffer, pos, b, offset, bytesToCopy);
+          pos += bytesToCopy;
+          copied += bytesToCopy;
+          if (pos == buffer.length) {
+            pos = 0;
+          }
+        }
+
+        // drain from [pos..limit)
+        if (copied < count) {
+          int bytesToCopy = Math.min(limit - pos, count - copied);
+          System.arraycopy(buffer, pos, b, offset + copied, bytesToCopy);
+          pos += bytesToCopy;
+          copied += bytesToCopy;
+        }
+
+        // Flow control: notify the peer that we're ready for more data!
+        unacknowledgedBytes += copied;
+        if (unacknowledgedBytes >= WINDOW_UPDATE_THRESHOLD) {
+          connection.writeWindowUpdateLater(id, unacknowledgedBytes);
+          unacknowledgedBytes = 0;
+        }
+
+        if (pos == limit) {
+          pos = -1;
+          limit = 0;
+        }
+
+        return copied;
+      }
+    }
+
+    /**
+     * Returns once the input stream is either readable or finished. Throws
+     * a {@link SocketTimeoutException} if the read timeout elapses before
+     * that happens.
+     */
+    private void waitUntilReadable() throws IOException {
+      long start = 0;
+      long remaining = 0;
+      if (readTimeoutMillis != 0) {
+        start = (System.nanoTime() / 1000000);
+        remaining = readTimeoutMillis;
+      }
+      try {
+        while (pos == -1 && !finished && !closed && rstStatusCode == -1) {
+          if (readTimeoutMillis == 0) {
+            SpdyStream.this.wait();
+          } else if (remaining > 0) {
+            SpdyStream.this.wait(remaining);
+            remaining = start + readTimeoutMillis - (System.nanoTime() / 1000000);
+          } else {
+            throw new SocketTimeoutException();
+          }
+        }
+      } catch (InterruptedException e) {
+        throw new InterruptedIOException();
+      }
+    }
+
+    void receive(InputStream in, int byteCount) throws IOException {
+      assert (!Thread.holdsLock(SpdyStream.this));
+
+      if (byteCount == 0) {
+        return;
+      }
+
+      int pos;
+      int limit;
+      int firstNewByte;
+      boolean finished;
+      boolean flowControlError;
+      synchronized (SpdyStream.this) {
+        finished = this.finished;
+        pos = this.pos;
+        firstNewByte = this.limit;
+        limit = this.limit;
+        flowControlError = byteCount > buffer.length - available();
+      }
+
+      // If the peer sends more data than we can handle, discard it and close the connection.
+      if (flowControlError) {
+        Util.skipByReading(in, byteCount);
+        closeLater(SpdyStream.RST_FLOW_CONTROL_ERROR);
+        return;
+      }
+
+      // Discard data received after the stream is finished. It's probably a benign race.
+      if (finished) {
+        Util.skipByReading(in, byteCount);
+        return;
+      }
+
+      // Fill the buffer without holding any locks. First fill [limit..buffer.length) if that
+      // won't overwrite unread data. Then fill [limit..pos). We can't hold a lock, otherwise
+      // writes will be blocked until reads complete.
+      if (pos < limit) {
+        int firstCopyCount = Math.min(byteCount, buffer.length - limit);
+        Util.readFully(in, buffer, limit, firstCopyCount);
+        limit += firstCopyCount;
+        byteCount -= firstCopyCount;
+        if (limit == buffer.length) {
+          limit = 0;
+        }
+      }
+      if (byteCount > 0) {
+        Util.readFully(in, buffer, limit, byteCount);
+        limit += byteCount;
+      }
+
+      synchronized (SpdyStream.this) {
+        // Update the new limit, and mark the position as readable if necessary.
+        this.limit = limit;
+        if (this.pos == -1) {
+          this.pos = firstNewByte;
+          SpdyStream.this.notifyAll();
+        }
+      }
+    }
+
+    @Override public void close() throws IOException {
+      synchronized (SpdyStream.this) {
+        closed = true;
+        SpdyStream.this.notifyAll();
+      }
+      cancelStreamIfNecessary();
+    }
+
+    private void checkNotClosed() throws IOException {
+      if (closed) {
+        throw new IOException("stream closed");
+      }
+      if (rstStatusCode != -1) {
+        throw new IOException("stream was reset: " + rstStatusString());
+      }
+    }
+  }
+
+  private void cancelStreamIfNecessary() throws IOException {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    boolean open;
+    boolean cancel;
+    synchronized (this) {
+      cancel = !in.finished && in.closed && (out.finished || out.closed);
+      open = isOpen();
+    }
+    if (cancel) {
+      // RST this stream to prevent additional data from being sent. This
+      // is safe because the input stream is closed (we won't use any
+      // further bytes) and the output stream is either finished or closed
+      // (so RSTing both streams doesn't cause harm).
+      SpdyStream.this.close(RST_CANCEL);
+    } else if (!open) {
+      connection.removeStream(id);
+    }
+  }
+
+  /**
+   * An output stream that writes outgoing data frames of a stream. This class
+   * is not thread safe.
+   */
+  private final class SpdyDataOutputStream extends OutputStream {
+    private final byte[] buffer = new byte[8192];
+    private int pos = DATA_FRAME_HEADER_LENGTH;
+
+    /** True if the caller has closed this stream. */
+    private boolean closed;
+
+    /**
+     * True if either side has cleanly shut down this stream. We shall send
+     * no more bytes.
+     */
+    private boolean finished;
+
+    /**
+     * The total number of bytes written out to the peer, but not yet
+     * acknowledged with an incoming {@code WINDOW_UPDATE} frame. Writes
+     * block if they cause this to exceed the {@code WINDOW_SIZE}.
+     */
+    private int unacknowledgedBytes = 0;
+
+    @Override public void write(int b) throws IOException {
+      Util.writeSingleByte(this, b);
+    }
+
+    @Override public void write(byte[] bytes, int offset, int count) throws IOException {
+      assert (!Thread.holdsLock(SpdyStream.this));
+      checkOffsetAndCount(bytes.length, offset, count);
+      checkNotClosed();
+
+      while (count > 0) {
+        if (pos == buffer.length) {
+          writeFrame(false);
+        }
+        int bytesToCopy = Math.min(count, buffer.length - pos);
+        System.arraycopy(bytes, offset, buffer, pos, bytesToCopy);
+        pos += bytesToCopy;
+        offset += bytesToCopy;
+        count -= bytesToCopy;
+      }
+    }
+
+    @Override public void flush() throws IOException {
+      assert (!Thread.holdsLock(SpdyStream.this));
+      checkNotClosed();
+      if (pos > DATA_FRAME_HEADER_LENGTH) {
+        writeFrame(false);
+        connection.flush();
+      }
+    }
+
+    @Override public void close() throws IOException {
+      assert (!Thread.holdsLock(SpdyStream.this));
+      synchronized (SpdyStream.this) {
+        if (closed) {
+          return;
+        }
+        closed = true;
+      }
+      writeFrame(true);
+      connection.flush();
+      cancelStreamIfNecessary();
+    }
+
+    private void writeFrame(boolean last) throws IOException {
+      assert (!Thread.holdsLock(SpdyStream.this));
+
+      int length = pos - DATA_FRAME_HEADER_LENGTH;
+      synchronized (SpdyStream.this) {
+        waitUntilWritable(length, last);
+        unacknowledgedBytes += length;
+      }
+      int flags = 0;
+      if (last) {
+        flags |= SpdyConnection.FLAG_FIN;
+      }
+      pokeInt(buffer, 0, id & 0x7fffffff, BIG_ENDIAN);
+      pokeInt(buffer, 4, (flags & 0xff) << 24 | length & 0xffffff, BIG_ENDIAN);
+      connection.writeFrame(buffer, 0, pos);
+      pos = DATA_FRAME_HEADER_LENGTH;
+    }
+
+    /**
+     * Returns once the peer is ready to receive {@code count} bytes.
+     *
+     * @throws IOException if the stream was finished or closed, or the
+     * thread was interrupted.
+     */
+    private void waitUntilWritable(int count, boolean last) throws IOException {
+      try {
+        while (unacknowledgedBytes + count >= writeWindowSize) {
+          SpdyStream.this.wait(); // Wait until we receive a WINDOW_UPDATE.
+
+          // The stream may have been closed or reset while we were waiting!
+          if (!last && closed) {
+            throw new IOException("stream closed");
+          } else if (finished) {
+            throw new IOException("stream finished");
+          } else if (rstStatusCode != -1) {
+            throw new IOException("stream was reset: " + rstStatusString());
+          }
+        }
+      } catch (InterruptedException e) {
+        throw new InterruptedIOException();
+      }
+    }
+
+    private void checkNotClosed() throws IOException {
+      synchronized (SpdyStream.this) {
+        if (closed) {
+          throw new IOException("stream closed");
+        } else if (finished) {
+          throw new IOException("stream finished");
+        } else if (rstStatusCode != -1) {
+          throw new IOException("stream was reset: " + rstStatusString());
+        }
+      }
+    }
+  }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/spdy/SpdyWriter.java b/framework/src/com/squareup/okhttp/internal/spdy/SpdyWriter.java
new file mode 100644
index 0000000..b3d1d1f
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/spdy/SpdyWriter.java
@@ -0,0 +1,176 @@
+/*
+ * 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.spdy;
+
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Util;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.zip.Deflater;
+
+/** Write spdy/3 frames. */
+final class SpdyWriter implements Closeable {
+  final DataOutputStream out;
+  private final ByteArrayOutputStream nameValueBlockBuffer;
+  private final DataOutputStream nameValueBlockOut;
+
+  SpdyWriter(OutputStream out) {
+    this.out = new DataOutputStream(out);
+
+    Deflater deflater = new Deflater();
+    deflater.setDictionary(SpdyReader.DICTIONARY);
+    nameValueBlockBuffer = new ByteArrayOutputStream();
+    nameValueBlockOut = new DataOutputStream(
+        Platform.get().newDeflaterOutputStream(nameValueBlockBuffer, deflater, true));
+  }
+
+  public synchronized void synStream(int flags, int streamId, int associatedStreamId, int priority,
+      int slot, List<String> nameValueBlock) throws IOException {
+    writeNameValueBlockToBuffer(nameValueBlock);
+    int length = 10 + nameValueBlockBuffer.size();
+    int type = SpdyConnection.TYPE_SYN_STREAM;
+
+    int unused = 0;
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(streamId & 0x7fffffff);
+    out.writeInt(associatedStreamId & 0x7fffffff);
+    out.writeShort((priority & 0x7) << 13 | (unused & 0x1f) << 8 | (slot & 0xff));
+    nameValueBlockBuffer.writeTo(out);
+    out.flush();
+  }
+
+  public synchronized void synReply(int flags, int streamId, List<String> nameValueBlock)
+      throws IOException {
+    writeNameValueBlockToBuffer(nameValueBlock);
+    int type = SpdyConnection.TYPE_SYN_REPLY;
+    int length = nameValueBlockBuffer.size() + 4;
+
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(streamId & 0x7fffffff);
+    nameValueBlockBuffer.writeTo(out);
+    out.flush();
+  }
+
+  public synchronized void headers(int flags, int streamId, List<String> nameValueBlock)
+      throws IOException {
+    writeNameValueBlockToBuffer(nameValueBlock);
+    int type = SpdyConnection.TYPE_HEADERS;
+    int length = nameValueBlockBuffer.size() + 4;
+
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(streamId & 0x7fffffff);
+    nameValueBlockBuffer.writeTo(out);
+    out.flush();
+  }
+
+  public synchronized void rstStream(int streamId, int statusCode) throws IOException {
+    int flags = 0;
+    int type = SpdyConnection.TYPE_RST_STREAM;
+    int length = 8;
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(streamId & 0x7fffffff);
+    out.writeInt(statusCode);
+    out.flush();
+  }
+
+  public synchronized void data(int flags, int streamId, byte[] data) throws IOException {
+    int length = data.length;
+    out.writeInt(streamId & 0x7fffffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.write(data);
+    out.flush();
+  }
+
+  private void writeNameValueBlockToBuffer(List<String> nameValueBlock) throws IOException {
+    nameValueBlockBuffer.reset();
+    int numberOfPairs = nameValueBlock.size() / 2;
+    nameValueBlockOut.writeInt(numberOfPairs);
+    for (String s : nameValueBlock) {
+      nameValueBlockOut.writeInt(s.length());
+      nameValueBlockOut.write(s.getBytes("UTF-8"));
+    }
+    nameValueBlockOut.flush();
+  }
+
+  public synchronized void settings(int flags, Settings settings) throws IOException {
+    int type = SpdyConnection.TYPE_SETTINGS;
+    int size = settings.size();
+    int length = 4 + size * 8;
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(size);
+    for (int i = 0; i <= Settings.COUNT; i++) {
+      if (!settings.isSet(i)) continue;
+      int settingsFlags = settings.flags(i);
+      out.writeInt((settingsFlags & 0xff) << 24 | (i & 0xffffff));
+      out.writeInt(settings.get(i));
+    }
+    out.flush();
+  }
+
+  public synchronized void noop() throws IOException {
+    int type = SpdyConnection.TYPE_NOOP;
+    int length = 0;
+    int flags = 0;
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.flush();
+  }
+
+  public synchronized void ping(int flags, int id) throws IOException {
+    int type = SpdyConnection.TYPE_PING;
+    int length = 4;
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(id);
+    out.flush();
+  }
+
+  public synchronized void goAway(int flags, int lastGoodStreamId, int statusCode)
+      throws IOException {
+    int type = SpdyConnection.TYPE_GOAWAY;
+    int length = 8;
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(lastGoodStreamId);
+    out.writeInt(statusCode);
+    out.flush();
+  }
+
+  public synchronized void windowUpdate(int streamId, int deltaWindowSize) throws IOException {
+    int type = SpdyConnection.TYPE_WINDOW_UPDATE;
+    int flags = 0;
+    int length = 8;
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(streamId);
+    out.writeInt(deltaWindowSize);
+    out.flush();
+  }
+
+  @Override public void close() throws IOException {
+    Util.closeAll(out, nameValueBlockOut);
+  }
+}
diff --git a/framework/src/org/apache/cordova/App.java b/framework/src/org/apache/cordova/App.java
new file mode 100755
index 0000000..86c6572
--- /dev/null
+++ b/framework/src/org/apache/cordova/App.java
@@ -0,0 +1,225 @@
+/*
+       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 org.apache.cordova;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.LOG;
+import org.apache.cordova.PluginResult;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+
+/**
+ * This class exposes methods in Cordova that can be called from JavaScript.
+ */
+public class App extends CordovaPlugin {
+
+    /**
+     * Executes the request and returns PluginResult.
+     *
+     * @param action            The action to execute.
+     * @param args              JSONArry of arguments for the plugin.
+     * @param callbackContext   The callback context from which we were invoked.
+     * @return                  A PluginResult object with a status and message.
+     */
+    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+        PluginResult.Status status = PluginResult.Status.OK;
+        String result = "";
+
+        try {
+            if (action.equals("clearCache")) {
+                this.clearCache();
+            }
+            else if (action.equals("show")) {
+                // This gets called from JavaScript onCordovaReady to show the webview.
+                // I recommend we change the name of the Message as spinner/stop is not
+                // indicative of what this actually does (shows the webview).
+                cordova.getActivity().runOnUiThread(new Runnable() {
+                    public void run() {
+                        webView.postMessage("spinner", "stop");
+                    }
+                });
+            }
+            else if (action.equals("loadUrl")) {
+                this.loadUrl(args.getString(0), args.optJSONObject(1));
+            }
+            else if (action.equals("cancelLoadUrl")) {
+                //this.cancelLoadUrl();
+            }
+            else if (action.equals("clearHistory")) {
+                this.clearHistory();
+            }
+            else if (action.equals("backHistory")) {
+                this.backHistory();
+            }
+            else if (action.equals("overrideButton")) {
+                this.overrideButton(args.getString(0), args.getBoolean(1));
+            }
+            else if (action.equals("overrideBackbutton")) {
+                this.overrideBackbutton(args.getBoolean(0));
+            }
+            else if (action.equals("exitApp")) {
+                this.exitApp();
+            }
+            callbackContext.sendPluginResult(new PluginResult(status, result));
+            return true;
+        } catch (JSONException e) {
+            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
+            return false;
+        }
+    }
+
+    //--------------------------------------------------------------------------
+    // LOCAL METHODS
+    //--------------------------------------------------------------------------
+
+    /**
+     * Clear the resource cache.
+     */
+    public void clearCache() {
+        cordova.getActivity().runOnUiThread(new Runnable() {
+            public void run() {
+                webView.clearCache(true);
+            }
+        });
+    }
+
+    /**
+     * Load the url into the webview.
+     *
+     * @param url
+     * @param props			Properties that can be passed in to the Cordova activity (i.e. loadingDialog, wait, ...)
+     * @throws JSONException
+     */
+    public void loadUrl(String url, JSONObject props) throws JSONException {
+        LOG.d("App", "App.loadUrl("+url+","+props+")");
+        int wait = 0;
+        boolean openExternal = false;
+        boolean clearHistory = false;
+
+        // If there are properties, then set them on the Activity
+        HashMap<String, Object> params = new HashMap<String, Object>();
+        if (props != null) {
+            JSONArray keys = props.names();
+            for (int i = 0; i < keys.length(); i++) {
+                String key = keys.getString(i);
+                if (key.equals("wait")) {
+                    wait = props.getInt(key);
+                }
+                else if (key.equalsIgnoreCase("openexternal")) {
+                    openExternal = props.getBoolean(key);
+                }
+                else if (key.equalsIgnoreCase("clearhistory")) {
+                    clearHistory = props.getBoolean(key);
+                }
+                else {
+                    Object value = props.get(key);
+                    if (value == null) {
+
+                    }
+                    else if (value.getClass().equals(String.class)) {
+                        params.put(key, (String)value);
+                    }
+                    else if (value.getClass().equals(Boolean.class)) {
+                        params.put(key, (Boolean)value);
+                    }
+                    else if (value.getClass().equals(Integer.class)) {
+                        params.put(key, (Integer)value);
+                    }
+                }
+            }
+        }
+
+        // If wait property, then delay loading
+
+        if (wait > 0) {
+            try {
+                synchronized(this) {
+                    this.wait(wait);
+                }
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+        this.webView.showWebPage(url, openExternal, clearHistory, params);
+    }
+
+    /**
+     * Clear page history for the app.
+     */
+    public void clearHistory() {
+        this.webView.clearHistory();
+    }
+
+    /**
+     * Go to previous page displayed.
+     * This is the same as pressing the backbutton on Android device.
+     */
+    public void backHistory() {
+        cordova.getActivity().runOnUiThread(new Runnable() {
+            public void run() {
+                webView.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.
+     *
+     * @param override		T=override, F=cancel override
+     */
+    public void overrideBackbutton(boolean override) {
+        LOG.i("App", "WARNING: Back Button Default Behaviour will be overridden.  The backbutton event will be fired!");
+        webView.bindButton(override);
+    }
+
+    /**
+     * Override the default behavior of the Android volume buttons.
+     * If overridden, when the volume button is pressed, the "volume[up|down]button" JavaScript event will be fired.
+     *
+     * @param button        volumeup, volumedown
+     * @param override      T=override, F=cancel override
+     */
+    public void overrideButton(String button, boolean override) {
+        LOG.i("App", "WARNING: Volume Button Default Behaviour will be overridden.  The volume event will be fired!");
+        webView.bindButton(button, override);
+    }
+
+    /**
+     * Return whether the Android back button is overridden by the user.
+     *
+     * @return boolean
+     */
+    public boolean isBackbuttonOverridden() {
+        return webView.isBackButtonBound();
+    }
+
+    /**
+     * Exit the Android application.
+     */
+    public void exitApp() {
+        this.webView.postMessage("exit", null);
+    }
+
+}
diff --git a/framework/src/org/apache/cordova/AuthenticationToken.java b/framework/src/org/apache/cordova/AuthenticationToken.java
new file mode 100644
index 0000000..d3a231a
--- /dev/null
+++ b/framework/src/org/apache/cordova/AuthenticationToken.java
@@ -0,0 +1,69 @@
+/*
+       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 org.apache.cordova;
+
+/**
+ * The Class AuthenticationToken defines the userName and password to be used for authenticating a web resource
+ */
+public class AuthenticationToken {
+    private String userName;
+    private String password;
+
+    /**
+     * Gets the user name.
+     *
+     * @return the user name
+     */
+    public String getUserName() {
+        return userName;
+    }
+
+    /**
+     * Sets the user name.
+     *
+     * @param userName
+     *            the new user name
+     */
+    public void setUserName(String userName) {
+        this.userName = userName;
+    }
+
+    /**
+     * Gets the password.
+     *
+     * @return the password
+     */
+    public String getPassword() {
+        return password;
+    }
+
+    /**
+     * Sets the password.
+     *
+     * @param password
+     *            the new password
+     */
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+
+
+
+}
diff --git a/framework/src/org/apache/cordova/CallbackContext.java b/framework/src/org/apache/cordova/CallbackContext.java
new file mode 100644
index 0000000..446c37d
--- /dev/null
+++ b/framework/src/org/apache/cordova/CallbackContext.java
@@ -0,0 +1,144 @@
+/*
+       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 org.apache.cordova;
+
+import org.json.JSONArray;
+
+import android.util.Log;
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.PluginResult;
+import org.json.JSONObject;
+
+public class CallbackContext {
+    private static final String LOG_TAG = "CordovaPlugin";
+
+    private String callbackId;
+    private CordovaWebView webView;
+    private boolean finished;
+    private int changingThreads;
+
+    public CallbackContext(String callbackId, CordovaWebView webView) {
+        this.callbackId = callbackId;
+        this.webView = webView;
+    }
+    
+    public boolean isFinished() {
+        return finished;
+    }
+    
+    public boolean isChangingThreads() {
+        return changingThreads > 0;
+    }
+    
+    public String getCallbackId() {
+        return callbackId;
+    }
+
+    public void sendPluginResult(PluginResult pluginResult) {
+        synchronized (this) {
+            if (finished) {
+                Log.w(LOG_TAG, "Attempted to send a second callback for ID: " + callbackId + "\nResult was: " + pluginResult.getMessage());
+                return;
+            } else {
+                finished = !pluginResult.getKeepCallback();
+            }
+        }
+        webView.sendPluginResult(pluginResult, callbackId);
+    }
+
+    /**
+     * Helper for success callbacks that just returns the Status.OK by default
+     *
+     * @param message           The message to add to the success result.
+     */
+    public void success(JSONObject message) {
+        sendPluginResult(new PluginResult(PluginResult.Status.OK, message));
+    }
+
+    /**
+     * Helper for success callbacks that just returns the Status.OK by default
+     *
+     * @param message           The message to add to the success result.
+     */
+    public void success(String message) {
+        sendPluginResult(new PluginResult(PluginResult.Status.OK, message));
+    }
+
+    /**
+     * Helper for success callbacks that just returns the Status.OK by default
+     *
+     * @param message           The message to add to the success result.
+     */
+    public void success(JSONArray message) {
+        sendPluginResult(new PluginResult(PluginResult.Status.OK, message));
+    }
+
+    /**
+     * Helper for success callbacks that just returns the Status.OK by default
+     *
+     * @param message           The message to add to the success result.
+     */
+    public void success(byte[] message) {
+        sendPluginResult(new PluginResult(PluginResult.Status.OK, message));
+    }
+    
+    /**
+     * Helper for success callbacks that just returns the Status.OK by default
+     *
+     * @param message           The message to add to the success result.
+     */
+    public void success(int message) {
+        sendPluginResult(new PluginResult(PluginResult.Status.OK, message));
+    }
+
+    /**
+     * Helper for success callbacks that just returns the Status.OK by default
+     */
+    public void success() {
+        sendPluginResult(new PluginResult(PluginResult.Status.OK));
+    }
+
+    /**
+     * Helper for error callbacks that just returns the Status.ERROR by default
+     *
+     * @param message           The message to add to the error result.
+     */
+    public void error(JSONObject message) {
+        sendPluginResult(new PluginResult(PluginResult.Status.ERROR, message));
+    }
+
+    /**
+     * Helper for error callbacks that just returns the Status.ERROR by default
+     *
+     * @param message           The message to add to the error result.
+     */
+    public void error(String message) {
+        sendPluginResult(new PluginResult(PluginResult.Status.ERROR, message));
+    }
+
+    /**
+     * Helper for error callbacks that just returns the Status.ERROR by default
+     *
+     * @param message           The message to add to the error result.
+     */
+    public void error(int message) {
+        sendPluginResult(new PluginResult(PluginResult.Status.ERROR, message));
+    }
+}
diff --git a/framework/src/org/apache/cordova/Config.java b/framework/src/org/apache/cordova/Config.java
new file mode 100644
index 0000000..716b795
--- /dev/null
+++ b/framework/src/org/apache/cordova/Config.java
@@ -0,0 +1,230 @@
+/*
+       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 org.apache.cordova;
+
+import java.io.IOException;
+
+import java.util.Locale;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.cordova.LOG;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.Activity;
+
+import android.content.res.XmlResourceParser;
+import android.graphics.Color;
+
+import android.util.Log;
+
+public class Config {
+
+    public static final String TAG = "Config";
+
+    private Whitelist whitelist = new Whitelist();
+    private String startUrl;
+
+    private static Config self = null;
+
+    public static void init(Activity action) {
+        //Just re-initialize this! Seriously, we lose this all the time
+        self = new Config(action);
+    }
+
+    // Intended to be used for testing only; creates an empty configuration.
+    public static void init() {
+        if (self == null) {
+            self = new Config();
+        }
+    }
+
+    // Intended to be used for testing only; creates an empty configuration.
+    private Config() {
+    }
+
+    private Config(Activity action) {
+        if (action == null) {
+            LOG.i("CordovaLog", "There is no activity. Is this on the lock screen?");
+            return;
+        }
+
+        int id = action.getResources().getIdentifier("config", "xml", action.getPackageName());
+        if (id == 0) {
+            id = action.getResources().getIdentifier("cordova", "xml", action.getPackageName());
+            LOG.i("CordovaLog", "config.xml missing, reverting to cordova.xml");
+        }
+        if (id == 0) {
+            LOG.i("CordovaLog", "cordova.xml missing. Ignoring...");
+            return;
+        }
+
+        // Add implicitly allowed URLs
+        whitelist.addWhiteListEntry("file:///*", false);
+        whitelist.addWhiteListEntry("content:///*", false);
+        whitelist.addWhiteListEntry("data:*", false);
+
+        XmlResourceParser xml = action.getResources().getXml(id);
+        int eventType = -1;
+        while (eventType != XmlResourceParser.END_DOCUMENT) {
+            if (eventType == XmlResourceParser.START_TAG) {
+                String strNode = xml.getName();
+
+                if (strNode.equals("access")) {
+                    String origin = xml.getAttributeValue(null, "origin");
+                    String subdomains = xml.getAttributeValue(null, "subdomains");
+                    if (origin != null) {
+                        whitelist.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0));
+                    }
+                }
+                else if (strNode.equals("log")) {
+                    String level = xml.getAttributeValue(null, "level");
+                    Log.d(TAG, "The <log> tags is deprecated. Use <preference name=\"loglevel\" value=\"" + level + "\"/> instead.");
+                    if (level != null) {
+                        LOG.setLogLevel(level);
+                    }
+                }
+                else if (strNode.equals("preference")) {
+                    String name = xml.getAttributeValue(null, "name").toLowerCase(Locale.getDefault());
+                    /* Java 1.6 does not support switch-based strings
+                       Java 7 does, but we're using Dalvik, which is apparently not Java.
+                       Since we're reading XML, this has to be an ugly if/else.
+                       
+                       Also, due to cast issues, each of them has to call their separate putExtra!  
+                       Wheee!!! Isn't Java FUN!?!?!?
+                       
+                       Note: We should probably pass in the classname for the variable splash on splashscreen!
+                       */
+                    if (name.equalsIgnoreCase("LogLevel")) {
+                        String level = xml.getAttributeValue(null, "value");
+                        LOG.setLogLevel(level);
+                    } else if (name.equalsIgnoreCase("SplashScreen")) {
+                        String value = xml.getAttributeValue(null, "value");
+                        int resource = 0;
+                        if (value == null)
+                        {
+                            value = "splash";
+                        }
+                        resource = action.getResources().getIdentifier(value, "drawable", action.getPackageName());
+                        
+                        action.getIntent().putExtra(name, resource);
+                    }
+                    else if(name.equalsIgnoreCase("BackgroundColor")) {
+                        int value = xml.getAttributeIntValue(null, "value", Color.BLACK);
+                        action.getIntent().putExtra(name, value);
+                    }
+                    else if(name.equalsIgnoreCase("LoadUrlTimeoutValue")) {
+                        int value = xml.getAttributeIntValue(null, "value", 20000);
+                        action.getIntent().putExtra(name, value);
+                    }
+                    else if(name.equalsIgnoreCase("SplashScreenDelay")) {
+                        int value = xml.getAttributeIntValue(null, "value", 3000);
+                        action.getIntent().putExtra(name, value);
+                    }
+                    else if(name.equalsIgnoreCase("KeepRunning"))
+                    {
+                        boolean value = xml.getAttributeValue(null, "value").equals("true");
+                        action.getIntent().putExtra(name, value);
+                    }
+                    else if(name.equalsIgnoreCase("InAppBrowserStorageEnabled"))
+                    {
+                        boolean value = xml.getAttributeValue(null, "value").equals("true");
+                        action.getIntent().putExtra(name, value);
+                    }
+                    else if(name.equalsIgnoreCase("DisallowOverscroll"))
+                    {
+                        boolean value = xml.getAttributeValue(null, "value").equals("true");
+                        action.getIntent().putExtra(name, value);
+                    }
+                    else
+                    {
+                        String value = xml.getAttributeValue(null, "value");
+                        action.getIntent().putExtra(name, value);
+                    }
+                    /*
+                    LOG.i("CordovaLog", "Found preference for %s=%s", name, value);
+                     */
+                }
+                else if (strNode.equals("content")) {
+                    String src = xml.getAttributeValue(null, "src");
+
+                    LOG.i("CordovaLog", "Found start page location: %s", src);
+
+                    if (src != null) {
+                        Pattern schemeRegex = Pattern.compile("^[a-z-]+://");
+                        Matcher matcher = schemeRegex.matcher(src);
+                        if (matcher.find()) {
+                            startUrl = src;
+                        } else {
+                            if (src.charAt(0) == '/') {
+                                src = src.substring(1);
+                            }
+                            startUrl = "file:///android_asset/www/" + src;
+                        }
+                    }
+                }
+
+            }
+
+            try {
+                eventType = xml.next();
+            } catch (XmlPullParserException e) {
+                e.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Add entry to approved list of URLs (whitelist)
+     *
+     * @param origin        URL regular expression to allow
+     * @param subdomains    T=include all subdomains under origin
+     */
+    public static void addWhiteListEntry(String origin, boolean subdomains) {
+        if (self == null) {
+            return;
+        }
+        self.whitelist.addWhiteListEntry(origin, subdomains);
+    }
+
+    /**
+     * Determine if URL is in approved list of URLs to load.
+     *
+     * @param url
+     * @return
+     */
+    public static boolean isUrlWhiteListed(String url) {
+        if (self == null) {
+            return false;
+        }
+        return self.whitelist.isUrlWhiteListed(url);
+    }
+
+    public static String getStartUrl() {
+        if (self == null || self.startUrl == null) {
+            return "file:///android_asset/www/index.html";
+        }
+        return self.startUrl;
+    }
+}
diff --git a/framework/src/org/apache/cordova/CordovaActivity.java b/framework/src/org/apache/cordova/CordovaActivity.java
new file mode 100755
index 0000000..897b54e
--- /dev/null
+++ b/framework/src/org/apache/cordova/CordovaActivity.java
@@ -0,0 +1,1251 @@
+/*
+       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 org.apache.cordova;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.LOG;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Display;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.ImageView;
+import com.amazon.android.webkit.AmazonValueCallback;
+import com.amazon.android.webkit.AmazonWebViewClient;
+import com.amazon.android.webkit.AmazonWebKitFactory;
+import com.amazon.android.webkit.AmazonWebKitFactories;
+import android.widget.LinearLayout;
+
+/**
+ * This class is the main Android activity that represents the Cordova
+ * application.  It should be extended by the user to load the specific
+ * html file that contains the application.
+ *
+ * As an example:
+ *
+ *     package org.apache.cordova.examples;
+ *     import android.os.Bundle;
+ *     import org.apache.cordova.*;
+ *
+ *     public class Example extends CordovaActivity {
+ *       @Override
+ *       public void onCreate(Bundle savedInstanceState) {
+ *         super.onCreate(savedInstanceState);
+ *
+ *         // Set properties for activity
+ *         super.setStringProperty("loadingDialog", "Title,Message"); // show loading dialog
+ *         super.setStringProperty("errorUrl", "file:///android_asset/www/error.html"); // if error loading file in super.loadUrl().
+ *
+ *         // Clear cache if you want
+ *         super.appView.clearCache(true);
+ *
+ *         // Load your application
+ *         super.setIntegerProperty("splashscreen", R.drawable.splash); // load splash.jpg image from the resource drawable directory
+ *         super.loadUrl("file:///android_asset/www/index.html", 3000); // show splash screen 3 sec before loading app
+ *       }
+ *     }
+ *
+ * Properties: The application can be configured using the following properties:
+ *
+ *      // Display a native loading dialog when loading app.  Format for value = "Title,Message".
+ *      // (String - default=null)
+ *      super.setStringProperty("loadingDialog", "Wait,Loading Demo...");
+ *
+ *      // Display a native loading dialog when loading sub-pages.  Format for value = "Title,Message".
+ *      // (String - default=null)
+ *      super.setStringProperty("loadingPageDialog", "Loading page...");
+ *
+ *      // Load a splash screen image from the resource drawable directory.
+ *      // (Integer - default=0)
+ *      super.setIntegerProperty("splashscreen", R.drawable.splash);
+ *
+ *      // Set the background color.
+ *      // (Integer - default=0 or BLACK)
+ *      super.setIntegerProperty("backgroundColor", Color.WHITE);
+ *
+ *      // Time in msec to wait before triggering a timeout error when loading
+ *      // with super.loadUrl().  (Integer - default=20000)
+ *      super.setIntegerProperty("loadUrlTimeoutValue", 60000);
+ *
+ *      // URL to load if there's an error loading specified URL with loadUrl().
+ *      // Should be a local URL starting with file://. (String - default=null)
+ *      super.setStringProperty("errorUrl", "file:///android_asset/www/error.html");
+ *
+ *      // Enable app to keep running in background. (Boolean - default=true)
+ *      super.setBooleanProperty("keepRunning", false);
+ *
+ * Cordova.xml configuration:
+ *      Cordova uses a configuration file at res/xml/cordova.xml to specify the following settings.
+ *
+ *      Approved list of URLs that can be loaded into Cordova
+ *          <access origin="http://server regexp" subdomains="true" />
+ *      Log level: ERROR, WARN, INFO, DEBUG, VERBOSE (default=ERROR)
+ *          <log level="DEBUG" />
+ *
+ */
+public class CordovaActivity extends Activity implements CordovaInterface {
+    public static String TAG = "CordovaActivity";
+
+    // The webview for our app
+    protected CordovaWebView appView;
+    protected CordovaWebViewClient webViewClient;
+
+    protected LinearLayout root;
+    protected boolean cancelLoadUrl = false;
+    protected ProgressDialog spinnerDialog = null;
+    private final ExecutorService threadPool = Executors.newCachedThreadPool();
+
+
+    // The initial URL for our app
+    // ie http://server/path/index.html#abc?query
+    //private String url = null;
+
+    private static int ACTIVITY_STARTING = 0;
+    private static int ACTIVITY_RUNNING = 1;
+    private static int ACTIVITY_EXITING = 2;
+    private int activityState = 0;  // 0=starting, 1=running (after 1st resume), 2=shutting down
+
+    // Plugin to call when activity result is received
+    protected CordovaPlugin activityResultCallback = null;
+    protected boolean activityResultKeepRunning;
+
+    // Default background color for activity
+    // (this is not the color for the webview, which is set in HTML)
+    private int backgroundColor = Color.BLACK;
+
+    /*
+     * The variables below are used to cache some of the activity properties.
+     */
+
+    // Draw a splash screen using an image located in the drawable resource directory.
+    // This is not the same as calling super.loadSplashscreen(url)
+    protected int splashscreen = 0;
+    protected int splashscreenTime = 3000;
+
+    // LoadUrl timeout value in msec (default of 20 sec)
+    protected int loadUrlTimeoutValue = 20000;
+
+    // Keep app running when pause is received. (default = true)
+    // If true, then the JavaScript and native code continue to run in the background
+    // when another application (activity) is started.
+    protected boolean keepRunning = true;
+
+    private int lastRequestCode;
+
+    private Object responseCode;
+
+    private Intent lastIntent;
+
+    private Object lastResponseCode;
+
+    private String initCallbackClass;
+
+    private Object LOG_TAG;
+
+    private static boolean sFactoryInit = false;
+    private AmazonWebKitFactory factory = null;
+
+    private static final String AMAZON_WEBVIEW_LIB_PACKAGE="com.amazon.webview";
+    private static final String ANDROID_WEBKIT_FACTORY_MISSING = "AndroidWebKit classes are missing. Please copy android_interface.jar from AmazonWebView SDK to app's libs folder and then rebuild the app.";
+    private static final String ERROR_DIALOG_TITLE = "Application Error";
+    private static final String ERROR_DIALOG_OK_BUTTON = "OK";
+    /**
+    * Sets the authentication token.
+    *
+    * @param authenticationToken
+    * @param host
+    * @param realm
+    */
+    public void setAuthenticationToken(AuthenticationToken authenticationToken, String host, String realm) {
+        if (this.appView != null && this.appView.viewClient != null) {
+            this.appView.viewClient.setAuthenticationToken(authenticationToken, host, realm);
+        }
+    }
+
+    /**
+     * Removes the authentication token.
+     *
+     * @param host
+     * @param realm
+     *
+     * @return the authentication token or null if did not exist
+     */
+    public AuthenticationToken removeAuthenticationToken(String host, String realm) {
+        if (this.appView != null && this.appView.viewClient != null) {
+            return this.appView.viewClient.removeAuthenticationToken(host, realm);
+        }
+        return null;
+    }
+
+    /**
+     * Gets the authentication token.
+     *
+     * In order it tries:
+     * 1- host + realm
+     * 2- host
+     * 3- realm
+     * 4- no host, no realm
+     *
+     * @param host
+     * @param realm
+     *
+     * @return the authentication token
+     */
+    public AuthenticationToken getAuthenticationToken(String host, String realm) {
+        if (this.appView != null && this.appView.viewClient != null) {
+            return this.appView.viewClient.getAuthenticationToken(host, realm);
+        }
+        return null;
+    }
+
+    /**
+     * Clear all authentication tokens.
+     */
+    public void clearAuthenticationTokens() {
+        if (this.appView != null && this.appView.viewClient != null) {
+            this.appView.viewClient.clearAuthenticationTokens();
+        }
+    }
+
+    /**
+     * Called when the activity is first created.
+     *
+     * @param savedInstanceState
+     */
+    @SuppressWarnings("deprecation")
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        Config.init(this);
+        LOG.d(TAG, "CordovaActivity.onCreate()");
+        super.onCreate(savedInstanceState);
+        try {
+        	if (!sFactoryInit) {
+                // To override the default factory (Chromium-based or Android WebKit factory), uncomment one of the following lines:
+                // AmazonWebKitFactories.setDefaultFactory("com.amazon.android.webkit.embedded.EmbeddedWebKitFactory");
+                // AmazonWebKitFactories.setDefaultFactory("com.amazon.android.webkit.android.AndroidWebKitFactory");
+                
+                factory = AmazonWebKitFactories.getDefaultFactory();
+                if (factory.isRenderProcess(this)) {
+                    return; // Do nothing if this is on render process
+                }
+                factory.setNativeLibraryPackage(AMAZON_WEBVIEW_LIB_PACKAGE);
+                factory.initialize(this);
+                factory.disableDeveloperTools();
+                // factory configuration
+                factory.getCookieManager().setAcceptCookie(true);
+
+                sFactoryInit = true;
+            } else {
+                factory = AmazonWebKitFactories.getDefaultFactory();
+            }
+
+        } catch (ExceptionInInitializerError e) {
+        	LOG.e(TAG, "WebKit factory initialization failed. Make sure you have android_interface.jar in libs folder.");
+        	displayError(ERROR_DIALOG_TITLE, ANDROID_WEBKIT_FACTORY_MISSING, ERROR_DIALOG_OK_BUTTON, true);
+        } catch (NoClassDefFoundError e) {
+        	LOG.e(TAG, "WebKit factory initialization failed. Make sure you have android_interface.jar in libs folder.");
+        	displayError(ERROR_DIALOG_TITLE, ANDROID_WEBKIT_FACTORY_MISSING, ERROR_DIALOG_OK_BUTTON, true);
+        } catch (Exception e) {
+        	LOG.e(TAG, "WebKit factory initialization failed.");
+        	displayError(ERROR_DIALOG_TITLE, ANDROID_WEBKIT_FACTORY_MISSING, ERROR_DIALOG_OK_BUTTON, true);
+        }
+        
+        if (savedInstanceState != null)
+        {
+            initCallbackClass = savedInstanceState.getString("callbackClass");
+        }
+
+        if (!this.getBooleanProperty("ShowTitle", false))
+        {
+            getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+        }
+
+        if (this.getBooleanProperty("SetFullscreen", false))
+        {
+            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+                WindowManager.LayoutParams.FLAG_FULLSCREEN);
+        }
+        else
+        {
+            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN,
+                WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
+        }
+        // This builds the view. We could probably get away with NOT having a LinearLayout, but I like having a bucket!
+        Display display = getWindowManager().getDefaultDisplay();
+        int width = display.getWidth();
+        int height = display.getHeight();
+
+        root = new LinearLayoutSoftKeyboardDetect(this, width, height);
+        root.setOrientation(LinearLayout.VERTICAL);
+        root.setBackgroundColor(this.backgroundColor);
+        root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+            ViewGroup.LayoutParams.MATCH_PARENT, 0.0F));
+
+        // Setup the hardware volume controls to handle volume control
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+    }
+
+    /**
+     * Get the Android activity.
+     *
+     * @return
+     */
+    public Activity getActivity() {
+        return this;
+    }
+    
+    /**
+     * Get the WebKit factory.
+     *
+     * @return
+     */
+    public AmazonWebKitFactory getFactory() {
+        return this.factory;
+    }
+
+    /**
+     * Construct the default web view object.
+     *
+     * This is intended to be overridable by subclasses of CordovaIntent which
+     * require a more specialized web view.
+     */
+    protected CordovaWebView makeWebView() {
+        return new CordovaWebView(CordovaActivity.this);
+    }
+
+    /**
+     * Construct the client for the default web view object.
+     *
+     * This is intended to be overridable by subclasses of CordovaIntent which
+     * require a more specialized web view.
+     *
+     * @param webView the default constructed web view object
+     */
+    protected CordovaWebViewClient makeWebViewClient(CordovaWebView webView) {
+        if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) {
+            return new CordovaWebViewClient(this, webView);
+        } else {
+            return new IceCreamCordovaWebViewClient(this, webView);
+        }
+    }
+
+    /**
+     * Construct the chrome client for the default web view object.
+     *
+     * This is intended to be overridable by subclasses of CordovaIntent which
+     * require a more specialized web view.
+     *
+     * @param webView the default constructed web view object
+     */
+    protected CordovaChromeClient makeChromeClient(CordovaWebView webView) {
+        return new CordovaChromeClient(this, webView);
+    }
+
+    /**
+     * Create and initialize web container with default web view objects.
+     */
+    public void init() {
+    	if (factory != null) {
+    		CordovaWebView webView = makeWebView();
+    		this.init(webView, makeWebViewClient(webView), makeChromeClient(webView));
+    	}
+    }
+
+    /**
+     * Initialize web container with web view objects.
+     *
+     * @param webView
+     * @param webViewClient
+     * @param webChromeClient
+     */
+    @SuppressLint("NewApi")
+    public void init(CordovaWebView webView, CordovaWebViewClient webViewClient, CordovaChromeClient webChromeClient) {
+        LOG.d(TAG, "CordovaActivity.init()");
+
+        // Set up web container
+        this.appView = webView;
+        this.appView.setId(100);
+
+        this.appView.setWebViewClient(webViewClient);
+        this.appView.setWebChromeClient(webChromeClient);
+        webViewClient.setWebView(this.appView);
+        webChromeClient.setWebView(this.appView);
+
+        this.appView.setLayoutParams(new LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                1.0F));
+
+        if (this.getBooleanProperty("DisallowOverscroll", false)) {
+            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD) {
+                this.appView.setOverScrollMode(CordovaWebView.OVER_SCROLL_NEVER);
+            }
+        }
+
+        // Add web view but make it invisible while loading URL
+        this.appView.setVisibility(View.INVISIBLE);
+        this.root.addView(this.appView);
+        setContentView(this.root);
+
+        // Clear cancel flag
+        this.cancelLoadUrl = false;
+        
+    }
+
+    /**
+     * Load the url into the webview.
+     *
+     * @param url
+     */
+    public void loadUrl(String url) {
+
+        // Init web view if not already done
+        if (this.appView == null) {
+            this.init();
+            // check again if its still null then exit. Something is wrong - factory is not initialized or webview is not created
+            if (this.appView == null) {
+            	return;
+            }
+        }
+
+        this.splashscreenTime = this.getIntegerProperty("SplashScreenDelay", this.splashscreenTime);
+        if(this.splashscreenTime > 0)
+        {
+            this.splashscreen = this.getIntegerProperty("SplashScreen", 0);
+            if(this.splashscreen != 0)
+            {
+                this.showSplashScreen(this.splashscreenTime);
+            }
+        }
+        
+        // Set backgroundColor
+        this.backgroundColor = this.getIntegerProperty("BackgroundColor", Color.BLACK);
+        this.root.setBackgroundColor(this.backgroundColor);
+
+        // If keepRunning
+        this.keepRunning = this.getBooleanProperty("KeepRunning", true);
+
+        //Check if the view is attached to anything
+        if(appView.getParent() != null)
+        {
+            // Then load the spinner
+            this.loadSpinner();
+        }
+        //Load the correct splashscreen
+        
+        if(this.splashscreen != 0)
+        {
+            this.appView.loadUrl(url, this.splashscreenTime);
+        }
+        else
+        {
+            this.appView.loadUrl(url);
+        }
+    }
+
+    /**
+     * Load the url into the webview after waiting for period of time.
+     * This is used to display the splashscreen for certain amount of time.
+     *
+     * @param url
+     * @param time              The number of ms to wait before loading webview
+     */
+    public void loadUrl(final String url, int time) {
+
+        this.splashscreenTime = time;
+        this.loadUrl(url);
+        
+        /*
+        // Init web view if not already done
+        if (this.appView == null) {
+            this.init();
+        }
+
+        this.splashscreenTime = time;
+        this.splashscreen = this.getIntegerProperty("SplashScreen", 0);
+        this.showSplashScreen(this.splashscreenTime);
+        this.appView.loadUrl(url, time);
+        */
+    }
+    
+    /*
+     * Load the spinner
+     */
+    void loadSpinner() {
+
+        // If loadingDialog property, then show the App loading dialog for first page of app
+        String loading = null;
+        if ((this.appView == null) || !this.appView.canGoBack()) {
+            loading = this.getStringProperty("LoadingDialog", null);
+        }
+        else {
+            loading = this.getStringProperty("LoadingPageDialog", null);
+        }
+        if (loading != null) {
+
+            String title = "";
+            String message = "Loading Application...";
+
+            if (loading.length() > 0) {
+                int comma = loading.indexOf(',');
+                if (comma > 0) {
+                    title = loading.substring(0, comma);
+                    message = loading.substring(comma + 1);
+                }
+                else {
+                    title = "";
+                    message = loading;
+                }
+            }
+            this.spinnerStart(title, message);
+        }
+    }
+
+
+    /**
+     * Cancel loadUrl before it has been loaded.
+     */
+    // TODO NO-OP
+    @Deprecated
+    public void cancelLoadUrl() {
+        this.cancelLoadUrl = true;
+    }
+
+    /**
+     * Clear the resource cache.
+     */
+    public void clearCache() {
+        if (this.appView == null) {
+            this.init();
+        }
+        this.appView.clearCache(true);
+    }
+
+    /**
+     * Clear web history in this web view.
+     */
+    public void clearHistory() {
+        this.appView.clearHistory();
+    }
+
+    /**
+     * Go to previous page in history.  (We manage our own history)
+     *
+     * @return true if we went back, false if we are already at top
+     */
+    public boolean backHistory() {
+        if (this.appView != null) {
+            return appView.backHistory();
+        }
+        return false;
+    }
+
+    @Override
+    /**
+     * Called by the system when the device configuration changes while your activity is running.
+     *
+     * @param Configuration newConfig
+     */
+    public void onConfigurationChanged(Configuration newConfig) {
+        //don't reload the current page when the orientation is changed
+        super.onConfigurationChanged(newConfig);
+    }
+
+    /**
+     * Get boolean property for activity.
+     *
+     * @param name
+     * @param defaultValue
+     * @return
+     */
+    public boolean getBooleanProperty(String name, boolean defaultValue) {
+        Bundle bundle = this.getIntent().getExtras();
+        if (bundle == null) {
+            return defaultValue;
+        }
+        name = name.toLowerCase(Locale.getDefault());
+        Boolean p;
+        try {
+            p = (Boolean) bundle.get(name);
+        } catch (ClassCastException e) {
+            String s = bundle.get(name).toString();
+            if ("true".equals(s)) {
+                p = true;
+            }
+            else {
+                p = false;
+            }
+        }
+        if (p == null) {
+            return defaultValue;
+        }
+        return p.booleanValue();
+    }
+
+    /**
+     * Get int property for activity.
+     *
+     * @param name
+     * @param defaultValue
+     * @return
+     */
+    public int getIntegerProperty(String name, int defaultValue) {
+        Bundle bundle = this.getIntent().getExtras();
+        if (bundle == null) {
+            return defaultValue;
+        }
+        name = name.toLowerCase(Locale.getDefault());
+        Integer p;
+        try {
+            p = (Integer) bundle.get(name);
+        } catch (ClassCastException e) {
+            p = Integer.parseInt(bundle.get(name).toString());
+        }
+        if (p == null) {
+            return defaultValue;
+        }
+        return p.intValue();
+    }
+
+    /**
+     * Get string property for activity.
+     *
+     * @param name
+     * @param defaultValue
+     * @return
+     */
+    public String getStringProperty(String name, String defaultValue) {
+        Bundle bundle = this.getIntent().getExtras();
+        if (bundle == null) {
+            return defaultValue;
+        }
+        name = name.toLowerCase(Locale.getDefault());
+        String p = bundle.getString(name);
+        if (p == null) {
+            return defaultValue;
+        }
+        return p;
+    }
+
+    /**
+     * Get double property for activity.
+     *
+     * @param name
+     * @param defaultValue
+     * @return
+     */
+    public double getDoubleProperty(String name, double defaultValue) {
+        Bundle bundle = this.getIntent().getExtras();
+        if (bundle == null) {
+            return defaultValue;
+        }
+        name = name.toLowerCase(Locale.getDefault());
+        Double p;
+        try {
+            p = (Double) bundle.get(name);
+        } catch (ClassCastException e) {
+            p = Double.parseDouble(bundle.get(name).toString());
+        }
+        if (p == null) {
+            return defaultValue;
+        }
+        return p.doubleValue();
+    }
+
+    /**
+     * Set boolean property on activity.
+     *
+     * @param name
+     * @param value
+     */
+    public void setBooleanProperty(String name, boolean value) {
+        Log.d(TAG, "Setting boolean properties in CordovaActivity will be deprecated in 3.0 on July 2013, please use config.xml");
+        this.getIntent().putExtra(name.toLowerCase(), value);
+    }
+
+    /**
+     * Set int property on activity.
+     *
+     * @param name
+     * @param value
+     */
+    public void setIntegerProperty(String name, int value) {
+        Log.d(TAG, "Setting integer properties in CordovaActivity will be deprecated in 3.0 on July 2013, please use config.xml");
+        this.getIntent().putExtra(name.toLowerCase(), value);
+    }
+
+    /**
+     * Set string property on activity.
+     *
+     * @param name
+     * @param value
+     */
+    public void setStringProperty(String name, String value) {
+        Log.d(TAG, "Setting string properties in CordovaActivity will be deprecated in 3.0 on July 2013, please use config.xml");
+        this.getIntent().putExtra(name.toLowerCase(), value);
+    }
+
+    /**
+     * Set double property on activity.
+     *
+     * @param name
+     * @param value
+     */
+    public void setDoubleProperty(String name, double value) {
+        Log.d(TAG, "Setting double properties in CordovaActivity will be deprecated in 3.0 on July 2013, please use config.xml");
+        this.getIntent().putExtra(name.toLowerCase(), value);
+    }
+
+    @Override
+    /**
+     * Called when the system is about to start resuming a previous activity.
+     */
+    protected void onPause() {
+        super.onPause();
+
+        LOG.d(TAG, "Paused the application!");
+
+        // Don't process pause if shutting down, since onDestroy() will be called
+        if (this.activityState == ACTIVITY_EXITING) {
+            return;
+        }
+
+        if (this.appView == null) {
+            return;
+        }
+        else
+        {
+            this.appView.handlePause(this.keepRunning);
+        }
+
+        // hide the splash screen to avoid leaking a window
+        this.removeSplashScreen();
+    }
+
+    @Override
+    /**
+     * Called when the activity receives a new intent
+     **/
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        //Forward to plugins
+        if (this.appView != null)
+           this.appView.onNewIntent(intent);
+    }
+
+    @Override
+    /**
+     * Called when the activity will start interacting with the user.
+     */
+    protected void onResume() {
+        super.onResume();
+        //Reload the configuration
+        Config.init(this);
+
+        LOG.d(TAG, "Resuming the App");
+        
+
+        //Code to test CB-3064
+        String errorUrl = this.getStringProperty("ErrorUrl", null);
+        LOG.d(TAG, "CB-3064: The errorUrl is " + errorUrl);
+          
+        if (this.activityState == ACTIVITY_STARTING) {
+            this.activityState = ACTIVITY_RUNNING;
+            return;
+        }
+
+        if (this.appView == null) {
+            return;
+        }
+
+        this.appView.handleResume(this.keepRunning, this.activityResultKeepRunning);
+
+        // If app doesn't want to run in background
+        if (!this.keepRunning || this.activityResultKeepRunning) {
+
+            // Restore multitasking state
+            if (this.activityResultKeepRunning) {
+                this.keepRunning = this.activityResultKeepRunning;
+                this.activityResultKeepRunning = false;
+            }
+        }
+    }
+
+    @Override
+    /**
+     * The final call you receive before your activity is destroyed.
+     */
+    public void onDestroy() {
+        LOG.d(TAG, "CordovaActivity.onDestroy()");
+        super.onDestroy();
+
+        // hide the splash screen to avoid leaking a window
+        this.removeSplashScreen();
+
+        if (this.appView != null) {
+            appView.handleDestroy();
+        }
+        else {
+            this.activityState = ACTIVITY_EXITING; 
+        }
+    }
+
+    /**
+     * Send a message to all plugins.
+     *
+     * @param id            The message id
+     * @param data          The message data
+     */
+    public void postMessage(String id, Object data) {
+        if (this.appView != null) {
+            this.appView.postMessage(id, data);
+        }
+    }
+
+    /**
+     * @deprecated
+     * Add services to res/xml/plugins.xml instead.
+     *
+     * Add a class that implements a service.
+     *
+     * @param serviceType
+     * @param className
+     */
+    public void addService(String serviceType, String className) {
+        if (this.appView != null && this.appView.pluginManager != null) {
+            this.appView.pluginManager.addService(serviceType, className);
+        }
+    }
+
+    /**
+     * Send JavaScript statement back to JavaScript.
+     * (This is a convenience method)
+     *
+     * @param statement
+     */
+    public void sendJavascript(String statement) {
+        if (this.appView != null) {
+            this.appView.jsMessageQueue.addJavaScript(statement);
+        }
+    }
+
+    /**
+     * Show the spinner.  Must be called from the UI thread.
+     *
+     * @param title         Title of the dialog
+     * @param message       The message of the dialog
+     */
+    public void spinnerStart(final String title, final String message) {
+        if (this.spinnerDialog != null) {
+            this.spinnerDialog.dismiss();
+            this.spinnerDialog = null;
+        }
+        final CordovaActivity me = this;
+        this.spinnerDialog = ProgressDialog.show(CordovaActivity.this, title, message, true, true,
+                new DialogInterface.OnCancelListener() {
+                    public void onCancel(DialogInterface dialog) {
+                        me.spinnerDialog = null;
+                    }
+                });
+    }
+
+    /**
+     * Stop spinner - Must be called from UI thread
+     */
+    public void spinnerStop() {
+        if (this.spinnerDialog != null && this.spinnerDialog.isShowing()) {
+            this.spinnerDialog.dismiss();
+            this.spinnerDialog = null;
+        }
+    }
+
+    /**
+     * End this activity by calling finish for activity
+     */
+    public void endActivity() {
+        this.activityState = ACTIVITY_EXITING;
+        super.finish();
+    }
+
+
+    /**
+     * Launch an activity for which you would like a result when it finished. When this activity exits,
+     * your onActivityResult() method will be called.
+     *
+     * @param command           The command object
+     * @param intent            The intent to start
+     * @param requestCode       The request code that is passed to callback to identify the activity
+     */
+    public void startActivityForResult(CordovaPlugin command, Intent intent, int requestCode) {
+        this.activityResultCallback = command;
+        this.activityResultKeepRunning = this.keepRunning;
+
+        // If multitasking turned on, then disable it for activities that return results
+        if (command != null) {
+            this.keepRunning = false;
+        }
+
+        // Start activity
+        super.startActivityForResult(intent, requestCode);
+    }
+
+    @Override
+    /**
+     * Called when an activity you launched exits, giving you the requestCode you started it with,
+     * the resultCode it returned, and any additional data from it.
+     *
+     * @param requestCode       The request code originally supplied to startActivityForResult(),
+     *                          allowing you to identify who this result came from.
+     * @param resultCode        The integer result code returned by the child activity through its setResult().
+     * @param data              An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
+     */
+    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        LOG.d(TAG, "Incoming Result");
+        super.onActivityResult(requestCode, resultCode, intent);
+        Log.d(TAG, "Request code = " + requestCode);
+        if (appView != null && requestCode == CordovaChromeClient.FILECHOOSER_RESULTCODE) {
+            AmazonValueCallback<Uri> mUploadMessage = this.appView.getWebChromeClient().getValueCallback();
+            Log.d(TAG, "did we get here?");
+            if (null == mUploadMessage)
+                return;
+            Uri result = intent == null || resultCode != Activity.RESULT_OK ? null : intent.getData();
+            Log.d(TAG, "result = " + result);
+//            Uri filepath = Uri.parse("file://" + FileUtils.getRealPathFromURI(result, this));
+//            Log.d(TAG, "result = " + filepath);
+            mUploadMessage.onReceiveValue(result);
+            mUploadMessage = null;
+        }
+        CordovaPlugin callback = this.activityResultCallback;
+        if(callback == null && initCallbackClass != null) {
+            // The application was restarted, but had defined an initial callback
+            // before being shut down.
+            this.activityResultCallback = appView.pluginManager.getPlugin(initCallbackClass);
+            callback = this.activityResultCallback;
+        }
+        if(callback != null) {
+            LOG.d(TAG, "We have a callback to send this result to");
+            callback.onActivityResult(requestCode, resultCode, intent);
+        }
+    }
+
+    public void setActivityResultCallback(CordovaPlugin plugin) {
+        this.activityResultCallback = plugin;
+    }
+
+    /**
+     * Report an error to the host application. These errors are unrecoverable (i.e. the main resource is unavailable).
+     * The errorCode parameter corresponds to one of the ERROR_* constants.
+     *
+     * @param errorCode    The error code corresponding to an ERROR_* value.
+     * @param description  A String describing the error.
+     * @param failingUrl   The url that failed to load.
+     */
+    public void onReceivedError(final int errorCode, final String description, final String failingUrl) {
+        final CordovaActivity me = this;
+
+        // If errorUrl specified, then load it
+        final String errorUrl = me.getStringProperty("errorUrl", null);
+        if ((errorUrl != null) && (errorUrl.startsWith("file://") || Config.isUrlWhiteListed(errorUrl)) && (!failingUrl.equals(errorUrl))) {
+
+            // Load URL on UI thread
+            me.runOnUiThread(new Runnable() {
+                public void run() {
+                    // Stop "app loading" spinner if showing
+                    me.spinnerStop();
+                    me.appView.showWebPage(errorUrl, false, true, null);
+                }
+            });
+        }
+        // If not, then display error dialog
+        else {
+            final boolean exit = !(errorCode == AmazonWebViewClient.ERROR_HOST_LOOKUP);
+            me.runOnUiThread(new Runnable() {
+                public void run() {
+                    if (exit) {
+                        me.appView.setVisibility(View.GONE);
+                        me.displayError("Application Error", description + " (" + failingUrl + ")", "OK", exit);
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * Display an error dialog and optionally exit application.
+     *
+     * @param title
+     * @param message
+     * @param button
+     * @param exit
+     */
+    public void displayError(final String title, final String message, final String button, final boolean exit) {
+        final CordovaActivity me = this;
+        me.runOnUiThread(new Runnable() {
+            public void run() {
+                try {
+                    AlertDialog.Builder dlg = new AlertDialog.Builder(me);
+                    dlg.setMessage(message);
+                    dlg.setTitle(title);
+                    dlg.setCancelable(false);
+                    dlg.setPositiveButton(button,
+                            new AlertDialog.OnClickListener() {
+                                public void onClick(DialogInterface dialog, int which) {
+                                    dialog.dismiss();
+                                    if (exit) {
+                                        endActivity();
+                                    }
+                                }
+                            });
+                    dlg.create();
+                    dlg.show();
+                } catch (Exception e) {
+                    finish();
+                }
+            }
+        });
+    }
+
+    /**
+     * Determine if URL is in approved list of URLs to load.
+     *
+     * @param url
+     * @return
+     */
+    public boolean isUrlWhiteListed(String url) {
+        return Config.isUrlWhiteListed(url);
+    }
+
+    /*
+     * Hook in Cordova for menu plugins
+     *
+     */
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        this.postMessage("onCreateOptionsMenu", menu);
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        this.postMessage("onPrepareOptionsMenu", menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        this.postMessage("onOptionsItemSelected", item);
+        return true;
+    }
+
+    /**
+     * Get Activity context.
+     *
+     * @return
+     */
+    public Context getContext() {
+        LOG.d(TAG, "This will be deprecated December 2012");
+        return this;
+    }
+
+    /**
+     * Load the specified URL in the Cordova webview or a new browser instance.
+     *
+     * NOTE: If openExternal is false, only URLs listed in whitelist can be loaded.
+     *
+     * @param url           The url to load.
+     * @param openExternal  Load url in browser instead of Cordova webview.
+     * @param clearHistory  Clear the history stack, so new page becomes top of history
+     * @param params        Parameters for new app
+     */
+    public void showWebPage(String url, boolean openExternal, boolean clearHistory, HashMap<String, Object> params) {
+        if (this.appView != null) {
+            appView.showWebPage(url, openExternal, clearHistory, params);
+        }
+    }
+
+    protected Dialog splashDialog;
+
+    /**
+     * Removes the Dialog that displays the splash screen
+     */
+    public void removeSplashScreen() {
+        if (splashDialog != null && splashDialog.isShowing()) {
+            splashDialog.dismiss();
+            splashDialog = null;
+        }
+    }
+
+    /**
+     * Shows the splash screen over the full Activity
+     */
+    @SuppressWarnings("deprecation")
+    protected void showSplashScreen(final int time) {
+        final CordovaActivity that = this;
+
+        Runnable runnable = new Runnable() {
+            public void run() {
+                // Get reference to display
+                Display display = getWindowManager().getDefaultDisplay();
+
+                // Create the layout for the dialog
+                LinearLayout root = new LinearLayout(that.getActivity());
+                root.setMinimumHeight(display.getHeight());
+                root.setMinimumWidth(display.getWidth());
+                root.setOrientation(LinearLayout.VERTICAL);
+                root.setBackgroundColor(that.getIntegerProperty("backgroundColor", Color.BLACK));
+                root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT, 0.0F));
+                root.setBackgroundResource(that.splashscreen);
+                
+                // Create and show the dialog
+                splashDialog = new Dialog(that, android.R.style.Theme_Translucent_NoTitleBar);
+                // check to see if the splash screen should be full screen
+                if ((getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN)
+                        == WindowManager.LayoutParams.FLAG_FULLSCREEN) {
+                    splashDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+                            WindowManager.LayoutParams.FLAG_FULLSCREEN);
+                }
+                splashDialog.setContentView(root);
+                splashDialog.setCancelable(false);
+                splashDialog.show();
+
+                // Set Runnable to remove splash screen just in case
+                final Handler handler = new Handler();
+                handler.postDelayed(new Runnable() {
+                    public void run() {
+                        removeSplashScreen();
+                    }
+                }, time);
+            }
+        };
+        this.runOnUiThread(runnable);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event)
+    {
+        if (appView != null && (appView.isCustomViewShowing() || appView.getFocusedChild() != null ) &&
+                (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU)) {
+            return appView.onKeyUp(keyCode, event);
+        } else {
+            return super.onKeyUp(keyCode, event);
+        }
+    }
+    
+    /*
+     * Android 2.x needs to be able to check where the cursor is.  Android 4.x does not
+     * 
+     * (non-Javadoc)
+     * @see android.app.Activity#onKeyDown(int, android.view.KeyEvent)
+     */
+    
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event)
+    {
+        //Determine if the focus is on the current view or not
+        if (appView != null && appView.getFocusedChild() != null && (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU)) {
+                    return appView.onKeyDown(keyCode, event);
+        }
+        else
+            return super.onKeyDown(keyCode, event);
+    }
+    
+    
+    /**
+     * Called when a message is sent to plugin.
+     *
+     * @param id            The message id
+     * @param data          The message data
+     * @return              Object or null
+     */
+    public Object onMessage(String id, Object data) {
+        LOG.d(TAG, "onMessage(" + id + "," + data + ")");
+        if ("splashscreen".equals(id)) {
+            if ("hide".equals(data.toString())) {
+                this.removeSplashScreen();
+            }
+            else {
+                // If the splash dialog is showing don't try to show it again
+                if (this.splashDialog == null || !this.splashDialog.isShowing()) {
+                    this.splashscreen = this.getIntegerProperty("SplashScreen", 0);
+                    this.showSplashScreen(this.splashscreenTime);
+                }
+            }
+        }
+        else if ("spinner".equals(id)) {
+            if ("stop".equals(data.toString())) {
+                this.spinnerStop();
+                this.appView.setVisibility(View.VISIBLE);
+            }
+        }
+        else if ("onReceivedError".equals(id)) {
+            JSONObject d = (JSONObject) data;
+            try {
+                this.onReceivedError(d.getInt("errorCode"), d.getString("description"), d.getString("url"));
+            } catch (JSONException e) {
+                e.printStackTrace();
+            }
+        }
+        else if ("exit".equals(id)) {
+            this.endActivity();
+        }
+        return null;
+    }
+
+    public ExecutorService getThreadPool() {
+        return threadPool;
+    }
+    
+    protected void onSaveInstanceState(Bundle outState)
+    {
+        super.onSaveInstanceState(outState);
+        if(this.activityResultCallback != null)
+        {
+            String cClass = this.activityResultCallback.getClass().getName();
+            outState.putString("callbackClass", cClass);
+        }
+    }
+}
diff --git a/framework/src/org/apache/cordova/CordovaArgs.java b/framework/src/org/apache/cordova/CordovaArgs.java
new file mode 100644
index 0000000..d40d26e
--- /dev/null
+++ b/framework/src/org/apache/cordova/CordovaArgs.java
@@ -0,0 +1,113 @@
+/*
+       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 org.apache.cordova;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.util.Base64;
+
+public class CordovaArgs {
+    private JSONArray baseArgs;
+
+    public CordovaArgs(JSONArray args) {
+        this.baseArgs = args;
+    }
+
+
+    // Pass through the basics to the base args.
+    public Object get(int index) throws JSONException {
+        return baseArgs.get(index);
+    }
+
+    public boolean getBoolean(int index) throws JSONException {
+        return baseArgs.getBoolean(index);
+    }
+
+    public double getDouble(int index) throws JSONException {
+        return baseArgs.getDouble(index);
+    }
+
+    public int getInt(int index) throws JSONException {
+        return baseArgs.getInt(index);
+    }
+
+    public JSONArray getJSONArray(int index) throws JSONException {
+        return baseArgs.getJSONArray(index);
+    }
+
+    public JSONObject getJSONObject(int index) throws JSONException {
+        return baseArgs.getJSONObject(index);
+    }
+
+    public long getLong(int index) throws JSONException {
+        return baseArgs.getLong(index);
+    }
+
+    public String getString(int index) throws JSONException {
+        return baseArgs.getString(index);
+    }
+
+
+    public Object opt(int index) {
+        return baseArgs.opt(index);
+    }
+
+    public boolean optBoolean(int index) {
+        return baseArgs.optBoolean(index);
+    }
+
+    public double optDouble(int index) {
+        return baseArgs.optDouble(index);
+    }
+
+    public int optInt(int index) {
+        return baseArgs.optInt(index);
+    }
+
+    public JSONArray optJSONArray(int index) {
+        return baseArgs.optJSONArray(index);
+    }
+
+    public JSONObject optJSONObject(int index) {
+        return baseArgs.optJSONObject(index);
+    }
+
+    public long optLong(int index) {
+        return baseArgs.optLong(index);
+    }
+
+    public String optString(int index) {
+        return baseArgs.optString(index);
+    }
+
+    public boolean isNull(int index) {
+        return baseArgs.isNull(index);
+    }
+
+
+    // The interesting custom helpers.
+    public byte[] getArrayBuffer(int index) throws JSONException {
+        String encoded = baseArgs.getString(index);
+        return Base64.decode(encoded, Base64.DEFAULT);
+    }
+}
+
+
diff --git a/framework/src/org/apache/cordova/CordovaChromeClient.java b/framework/src/org/apache/cordova/CordovaChromeClient.java
new file mode 100755
index 0000000..e506d6e
--- /dev/null
+++ b/framework/src/org/apache/cordova/CordovaChromeClient.java
@@ -0,0 +1,434 @@
+/*
+       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 org.apache.cordova;
+
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.LOG;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import com.amazon.android.webkit.AmazonConsoleMessage;
+import com.amazon.android.webkit.AmazonJsPromptResult;
+import com.amazon.android.webkit.AmazonJsResult;
+import com.amazon.android.webkit.AmazonValueCallback;
+import com.amazon.android.webkit.AmazonWebChromeClient;
+import com.amazon.android.webkit.AmazonWebStorage;
+import com.amazon.android.webkit.AmazonWebView;
+import com.amazon.android.webkit.AmazonGeolocationPermissions;
+import com.amazon.android.webkit.AmazonMediaDeviceSettings;
+
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+
+/**
+ * This class is the AmazonWebChromeClient that implements callbacks for our web view.
+ */
+public class CordovaChromeClient extends AmazonWebChromeClient {
+
+    public static final int FILECHOOSER_RESULTCODE = 5173;
+    private static final String LOG_TAG = "CordovaChromeClient";
+    private String TAG = "CordovaLog";
+
+    /* Using a conservative database quota (used primarily for the stock Android back-end) */
+    private static final long DB_QUOTA = 5 * 1024 * 1024;
+    
+    protected CordovaInterface cordova;
+    protected CordovaWebView appView;
+
+    // the video progress view
+    private View mVideoProgressView;
+    
+    // File Chooser
+    public AmazonValueCallback<Uri> mUploadMessage;
+    
+    /**
+     * Constructor.
+     *
+     * @param cordova
+     */
+    public CordovaChromeClient(CordovaInterface cordova) {
+        this.cordova = cordova;
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param ctx
+     * @param app
+     */
+    public CordovaChromeClient(CordovaInterface ctx, CordovaWebView app) {
+        this.cordova = ctx;
+        this.appView = app;
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param view
+     */
+    public void setWebView(CordovaWebView view) {
+        this.appView = view;
+    }
+
+    /**
+     * Tell the client to display a javascript alert dialog.
+     *
+     * @param view
+     * @param url
+     * @param message
+     * @param result
+     */
+    @Override
+    public boolean onJsAlert(AmazonWebView view, String url, String message, final AmazonJsResult result) {
+        AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity());
+        dlg.setMessage(message);
+        dlg.setTitle("Alert");
+        //Don't let alerts break the back button
+        dlg.setCancelable(true);
+        dlg.setPositiveButton(android.R.string.ok,
+                new AlertDialog.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                        result.confirm();
+                    }
+                });
+        dlg.setOnCancelListener(
+                new DialogInterface.OnCancelListener() {
+                    public void onCancel(DialogInterface dialog) {
+                        result.cancel();
+                    }
+                });
+        dlg.setOnKeyListener(new DialogInterface.OnKeyListener() {
+            //DO NOTHING
+            public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
+                if (keyCode == KeyEvent.KEYCODE_BACK)
+                {
+                    result.confirm();
+                    return false;
+                }
+                else
+                    return true;
+            }
+        });
+        dlg.create();
+        dlg.show();
+        return true;
+    }
+
+    /**
+     * Tell the client to display a confirm dialog to the user.
+     *
+     * @param view
+     * @param url
+     * @param message
+     * @param result
+     */
+    @Override
+    public boolean onJsConfirm(AmazonWebView view, String url, String message, final AmazonJsResult result) {
+        AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity());
+        dlg.setMessage(message);
+        dlg.setTitle("Confirm");
+        dlg.setCancelable(true);
+        dlg.setPositiveButton(android.R.string.ok,
+                new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                        result.confirm();
+                    }
+                });
+        dlg.setNegativeButton(android.R.string.cancel,
+                new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                        result.cancel();
+                    }
+                });
+        dlg.setOnCancelListener(
+                new DialogInterface.OnCancelListener() {
+                    public void onCancel(DialogInterface dialog) {
+                        result.cancel();
+                    }
+                });
+        dlg.setOnKeyListener(new DialogInterface.OnKeyListener() {
+            //DO NOTHING
+            public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
+                if (keyCode == KeyEvent.KEYCODE_BACK)
+                {
+                    result.cancel();
+                    return false;
+                }
+                else
+                    return true;
+            }
+        });
+        dlg.create();
+        dlg.show();
+        return true;
+    }
+
+    /**
+     * Tell the client to display a prompt dialog to the user.
+     * If the client returns true, AmazonWebView will assume that the client will
+     * handle the prompt dialog and call the appropriate AmazonJsPromptResult method.
+     *
+     * Since we are hacking prompts for our own purposes, we should not be using them for
+     * this purpose, perhaps we should hack console.log to do this instead!
+     *
+     * @param view
+     * @param url
+     * @param message
+     * @param defaultValue
+     * @param result
+     */
+    @Override
+    public boolean onJsPrompt(AmazonWebView view, String url, String message, String defaultValue, AmazonJsPromptResult result) {
+
+        // Security check to make sure any requests are coming from the page initially
+        // loaded in webview and not another loaded in an iframe.
+        boolean reqOk = false;
+        if (url.startsWith("file://") || Config.isUrlWhiteListed(url)) {
+            reqOk = true;
+        }
+
+        // Calling PluginManager.exec() to call a native service using 
+        // prompt(this.stringify(args), "gap:"+this.stringify([service, action, callbackId, true]));
+        if (reqOk && defaultValue != null && defaultValue.length() > 3 && defaultValue.substring(0, 4).equals("gap:")) {
+            JSONArray array;
+            try {
+                array = new JSONArray(defaultValue.substring(4));
+                String service = array.getString(0);
+                String action = array.getString(1);
+                String callbackId = array.getString(2);
+                String r = this.appView.exposedJsApi.exec(service, action, callbackId, message);
+                result.confirm(r == null ? "" : r);
+            } catch (JSONException e) {
+                e.printStackTrace();
+                return false;
+            }
+        }
+
+        // Sets the native->JS bridge mode. 
+        else if (reqOk && defaultValue != null && defaultValue.equals("gap_bridge_mode:")) {
+        	try {
+                this.appView.exposedJsApi.setNativeToJsBridgeMode(Integer.parseInt(message));
+                result.confirm("");
+        	} catch (NumberFormatException e){
+                result.confirm("");
+                e.printStackTrace();
+        	}
+        }
+
+        // Polling for JavaScript messages 
+        else if (reqOk && defaultValue != null && defaultValue.equals("gap_poll:")) {
+            String r = this.appView.exposedJsApi.retrieveJsMessages("1".equals(message));
+            result.confirm(r == null ? "" : r);
+        }
+
+        // Do NO-OP so older code doesn't display dialog
+        else if (defaultValue != null && defaultValue.equals("gap_init:")) {
+            result.confirm("OK");
+        }
+
+        // Show dialog
+        else {
+            final AmazonJsPromptResult res = result;
+            AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity());
+            dlg.setMessage(message);
+            final EditText input = new EditText(this.cordova.getActivity());
+            if (defaultValue != null) {
+                input.setText(defaultValue);
+            }
+            dlg.setView(input);
+            dlg.setCancelable(false);
+            dlg.setPositiveButton(android.R.string.ok,
+                    new DialogInterface.OnClickListener() {
+                        public void onClick(DialogInterface dialog, int which) {
+                            String usertext = input.getText().toString();
+                            res.confirm(usertext);
+                        }
+                    });
+            dlg.setNegativeButton(android.R.string.cancel,
+                    new DialogInterface.OnClickListener() {
+                        public void onClick(DialogInterface dialog, int which) {
+                            res.cancel();
+                        }
+                    });
+            dlg.create();
+            dlg.show();
+        }
+        return true;
+    }
+
+    /**
+     * Handle database quota exceeded notification.
+     *
+     * @param url
+     * @param databaseIdentifier
+     * @param currentQuota
+     * @param estimatedSize
+     * @param totalUsedQuota
+     * @param quotaUpdater
+     */
+    @Override
+    public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize,
+            long totalUsedQuota, AmazonWebStorage.QuotaUpdater quotaUpdater)
+    {
+        LOG.d(TAG, "Exceeded database quota - adjusting to " + DB_QUOTA + " bytes");
+
+        // This function is only called on the stock Android back-end due to the default
+        // quota initializing to 0 bytes. When on Chromium-compatible devices or platforms,
+        // the quota is essentially "unlimited" given the sufficient disk space.
+        if (currentQuota < DB_QUOTA) {
+            quotaUpdater.updateQuota(DB_QUOTA);
+            
+        }
+        
+    }
+
+    // console.log in api level 7: http://developer.android.com/guide/developing/debug-tasks.html
+    // Expect this to not compile in a future Android release!
+    @SuppressWarnings("deprecation")
+    @Override
+    public void onConsoleMessage(String message, int lineNumber, String sourceID)
+    {
+        //This is only for Android 2.1
+        if(android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.ECLAIR_MR1)
+        {
+            LOG.d(TAG, "%s: Line %d : %s", sourceID, lineNumber, message);
+            super.onConsoleMessage(message, lineNumber, sourceID);
+        }
+    }
+
+    @TargetApi(8)
+    @Override
+    public boolean onConsoleMessage(AmazonConsoleMessage consoleMessage)
+    {
+        if (consoleMessage.message() != null)
+            LOG.d(TAG, "%s: Line %d : %s" , consoleMessage.sourceId() , consoleMessage.lineNumber(), consoleMessage.message());
+         return super.onConsoleMessage(consoleMessage);
+    }
+
+    
+    /**
+     * Instructs the client to show a prompt to ask the user to set the Geolocation permission state for the specified
+     * origin.
+     * <p>
+     * Note- This prompt is displayed when web content from the specified origin is attempting to use the Geolocation
+     * API
+     * <ul>
+     * <li>1. getCurrentPosition(PositionCallback successCallback, PositionErrorCallback errorCallback, optional
+     * PositionOptions options)</li>
+     * <li>2. watchPosition(PositionCallback successCallback, PositionErrorCallback errorCallback, optional
+     * PositionOptions options)</li>
+     * </ul>
+     * 
+     * @param origin
+     * @param callback
+     */
+    @Override
+    public void onGeolocationPermissionsShowPrompt(String origin, AmazonGeolocationPermissions.Callback callback) {
+        callback.invoke(origin, true, false);
+    }
+    
+    // API level 7 is required for this, see if we could lower this using something else
+    @Override
+    public void onShowCustomView(View view, AmazonWebChromeClient.CustomViewCallback callback) {
+        this.appView.showCustomView(view, callback);
+    }
+
+	@Override
+	public void onHideCustomView() {
+    	this.appView.hideCustomView();
+	}
+    
+    @Override
+    /**
+     * Ask the host application for a custom progress view to show while
+     * a <video> is loading.
+     * @return View The progress view.
+     */
+    public View getVideoLoadingProgressView() {
+
+	    if (mVideoProgressView == null) {	        
+	    	// Create a new Loading view programmatically.
+	    	
+	    	// create the linear layout
+	    	LinearLayout layout = new LinearLayout(this.appView.getContext());
+	        layout.setOrientation(LinearLayout.VERTICAL);
+	        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+	        layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
+	        layout.setLayoutParams(layoutParams);
+	        // the proress bar
+	        ProgressBar bar = new ProgressBar(this.appView.getContext());
+	        LinearLayout.LayoutParams barLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+	        barLayoutParams.gravity = Gravity.CENTER;
+	        bar.setLayoutParams(barLayoutParams);   
+	        layout.addView(bar);
+	        
+	        mVideoProgressView = layout;
+	    }
+    return mVideoProgressView; 
+    }
+    
+    public void openFileChooser(AmazonValueCallback<Uri> uploadMsg) {
+        this.openFileChooser(uploadMsg, "*/*");
+    }
+
+    public void openFileChooser( AmazonValueCallback<Uri> uploadMsg, String acceptType ) {
+        this.openFileChooser(uploadMsg, acceptType, null);
+    }
+    
+    public void openFileChooser(AmazonValueCallback<Uri> uploadMsg, String acceptType, String capture)
+    {
+        mUploadMessage = uploadMsg;
+        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+        i.addCategory(Intent.CATEGORY_OPENABLE);
+        i.setType("*/*");
+        this.cordova.getActivity().startActivityForResult(Intent.createChooser(i, "File Browser"),
+                FILECHOOSER_RESULTCODE);
+    }
+    
+    public AmazonValueCallback<Uri> getValueCallback() {
+        return this.mUploadMessage;
+    }
+    
+    /**
+     * Notify the host application that media access is denied.
+     * <p>
+     * Note- getUserMedia() JS API is currently not supported by AmazonWebView
+     * 
+     * @param origin
+     *            The origin of the web content attempting to use the media device request api
+     * @param callback
+     *            The callback to use to set the permission state for the origin
+     */
+    @Override
+    public void onMediaDevicePermissionsShowPrompt(String origin, AmazonMediaDeviceSettings.Callback callback) {
+        // Currently, media access should always be denied
+        callback.invoke(false, true);
+    }
+}
diff --git a/framework/src/org/apache/cordova/CordovaInterface.java b/framework/src/org/apache/cordova/CordovaInterface.java
new file mode 100755
index 0000000..ffe49bd
--- /dev/null
+++ b/framework/src/org/apache/cordova/CordovaInterface.java
@@ -0,0 +1,80 @@
+/*
+       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 org.apache.cordova;
+
+import android.app.Activity;
+import android.content.Intent;
+
+import org.apache.cordova.CordovaPlugin;
+
+import java.util.concurrent.ExecutorService;
+
+import com.amazon.android.webkit.AmazonWebKitFactory;
+
+/**
+ * The Activity interface that is implemented by CordovaActivity.
+ * It is used to isolate plugin development, and remove dependency on entire Cordova library.
+ */
+public interface CordovaInterface {
+
+    /**
+     * Launch an activity for which you would like a result when it finished. When this activity exits,
+     * your onActivityResult() method will be called.
+     *
+     * @param command     The command object
+     * @param intent      The intent to start
+     * @param requestCode   The request code that is passed to callback to identify the activity
+     */
+    abstract public void startActivityForResult(CordovaPlugin command, Intent intent, int requestCode);
+
+    /**
+     * Set the plugin to be called when a sub-activity exits.
+     *
+     * @param plugin      The plugin on which onActivityResult is to be called
+     */
+    abstract public void setActivityResultCallback(CordovaPlugin plugin);
+
+    /**
+     * Get the Android activity.
+     *
+     * @return
+     */
+    public abstract Activity getActivity();
+    
+
+    /**
+     * Called when a message is sent to plugin.
+     *
+     * @param id            The message id
+     * @param data          The message data
+     * @return              Object or null
+     */
+    public Object onMessage(String id, Object data);
+    
+    /**
+     * Returns a shared thread pool that can be used for background tasks.
+     */
+    public ExecutorService getThreadPool();
+    
+    /**
+     * Get the WebKit factory.
+     * @return
+     */
+    public abstract AmazonWebKitFactory getFactory();
+}
diff --git a/framework/src/org/apache/cordova/CordovaPlugin.java b/framework/src/org/apache/cordova/CordovaPlugin.java
new file mode 100644
index 0000000..8111e7b
--- /dev/null
+++ b/framework/src/org/apache/cordova/CordovaPlugin.java
@@ -0,0 +1,182 @@
+/*
+       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 org.apache.cordova;
+
+import org.apache.cordova.CordovaArgs;
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CallbackContext;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ * Plugins must extend this class and override one of the execute methods.
+ */
+public class CordovaPlugin {
+    public String id;
+    public CordovaWebView webView;					// WebView object
+    public CordovaInterface cordova;
+
+    /**
+     * @param cordova The context of the main Activity.
+     * @param webView The associated CordovaWebView.
+     */
+    public void initialize(CordovaInterface cordova, CordovaWebView webView) {
+        assert this.cordova == null;
+        this.cordova = cordova;
+        this.webView = webView;
+    }
+
+    /**
+     * Executes the request.
+     *
+     * This method is called from the WebView thread. To do a non-trivial amount of work, use:
+     *     cordova.getThreadPool().execute(runnable);
+     *
+     * To run on the UI thread, use:
+     *     cordova.getActivity().runOnUiThread(runnable);
+     *
+     * @param action          The action to execute.
+     * @param rawArgs         The exec() arguments in JSON form.
+     * @param callbackContext The callback context used when calling back into JavaScript.
+     * @return                Whether the action was valid.
+     */
+    public boolean execute(String action, String rawArgs, CallbackContext callbackContext) throws JSONException {
+        JSONArray args = new JSONArray(rawArgs);
+        return execute(action, args, callbackContext);
+    }
+
+    /**
+     * Executes the request.
+     *
+     * This method is called from the WebView thread. To do a non-trivial amount of work, use:
+     *     cordova.getThreadPool().execute(runnable);
+     *
+     * To run on the UI thread, use:
+     *     cordova.getActivity().runOnUiThread(runnable);
+     *
+     * @param action          The action to execute.
+     * @param args            The exec() arguments.
+     * @param callbackContext The callback context used when calling back into JavaScript.
+     * @return                Whether the action was valid.
+     */
+    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+        CordovaArgs cordovaArgs = new CordovaArgs(args);
+        return execute(action, cordovaArgs, callbackContext);
+    }
+
+    /**
+     * Executes the request.
+     *
+     * This method is called from the WebView thread. To do a non-trivial amount of work, use:
+     *     cordova.getThreadPool().execute(runnable);
+     *
+     * To run on the UI thread, use:
+     *     cordova.getActivity().runOnUiThread(runnable);
+     *
+     * @param action          The action to execute.
+     * @param args            The exec() arguments, wrapped with some Cordova helpers.
+     * @param callbackContext The callback context used when calling back into JavaScript.
+     * @return                Whether the action was valid.
+     */
+    public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) throws JSONException {
+        return false;
+    }
+
+    /**
+     * Called when the system is about to start resuming a previous activity.
+     *
+     * @param multitasking		Flag indicating if multitasking is turned on for app
+     */
+    public void onPause(boolean multitasking) {
+    }
+
+    /**
+     * Called when the activity will start interacting with the user.
+     *
+     * @param multitasking		Flag indicating if multitasking is turned on for app
+     */
+    public void onResume(boolean multitasking) {
+    }
+
+    /**
+     * Called when the activity receives a new intent.
+     */
+    public void onNewIntent(Intent intent) {
+    }
+
+    /**
+     * The final call you receive before your activity is destroyed.
+     */
+    public void onDestroy() {
+    }
+
+    /**
+     * Called when a message is sent to plugin.
+     *
+     * @param id            The message id
+     * @param data          The message data
+     * @return              Object to stop propagation or null
+     */
+    public Object onMessage(String id, Object data) {
+        return null;
+    }
+
+    /**
+     * Called when an activity you launched exits, giving you the requestCode you started it with,
+     * the resultCode it returned, and any additional data from it.
+     *
+     * @param requestCode		The request code originally supplied to startActivityForResult(),
+     * 							allowing you to identify who this result came from.
+     * @param resultCode		The integer result code returned by the child activity through its setResult().
+     * @param intent				An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
+     */
+    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+    }
+
+    /**
+     * By specifying a <url-filter> in config.xml you can map a URL (using startsWith atm) to this method.
+     *
+     * @param url				The URL that is trying to be loaded in the Cordova webview.
+     * @return					Return true to prevent the URL from loading. Default is false.
+     */
+    public boolean onOverrideUrlLoading(String url) {
+        return false;
+    }
+
+    /**
+     * Hook for redirecting requests. Applies to WebView requests as well as requests made by plugins.
+     */
+    public Uri remapUri(Uri uri) {
+        return null;
+    }
+    
+    /**
+     * Called when the WebView does a top-level navigation or refreshes.
+     *
+     * Plugins should stop any long-running processes and clean up internal state.
+     *
+     * Does nothing by default.
+     */
+    public void onReset() {
+    }
+}
diff --git a/framework/src/org/apache/cordova/CordovaResourceApi.java b/framework/src/org/apache/cordova/CordovaResourceApi.java
new file mode 100644
index 0000000..96f3d25
--- /dev/null
+++ b/framework/src/org/apache/cordova/CordovaResourceApi.java
@@ -0,0 +1,416 @@
+/*
+       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 org.apache.cordova;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.AssetManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Looper;
+import android.util.Base64;
+import android.webkit.MimeTypeMap;
+
+import com.squareup.okhttp.OkHttpClient;
+
+import org.apache.http.util.EncodingUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.channels.FileChannel;
+import java.util.Locale;
+
+public class CordovaResourceApi {
+    @SuppressWarnings("unused")
+    private static final String LOG_TAG = "CordovaResourceApi";
+
+    public static final int URI_TYPE_FILE = 0;
+    public static final int URI_TYPE_ASSET = 1;
+    public static final int URI_TYPE_CONTENT = 2;
+    public static final int URI_TYPE_RESOURCE = 3;
+    public static final int URI_TYPE_DATA = 4;
+    public static final int URI_TYPE_HTTP = 5;
+    public static final int URI_TYPE_HTTPS = 6;
+    public static final int URI_TYPE_UNKNOWN = -1;
+    
+    private static final String[] LOCAL_FILE_PROJECTION = { "_data" };
+    
+    // Creating this is light-weight.
+    private static OkHttpClient httpClient = new OkHttpClient();
+    
+    static Thread jsThread;
+
+    private final AssetManager assetManager;
+    private final ContentResolver contentResolver;
+    private final PluginManager pluginManager;
+    private boolean threadCheckingEnabled = true;
+
+
+    public CordovaResourceApi(Context context, PluginManager pluginManager) {
+        this.contentResolver = context.getContentResolver();
+        this.assetManager = context.getAssets();
+        this.pluginManager = pluginManager;
+    }
+    
+    public void setThreadCheckingEnabled(boolean value) {
+        threadCheckingEnabled = value;
+    }
+
+    public boolean isThreadCheckingEnabled() {
+        return threadCheckingEnabled;
+    }
+    
+    public static int getUriType(Uri uri) {
+        assertNonRelative(uri);
+        String scheme = uri.getScheme();
+        if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
+            return URI_TYPE_CONTENT;
+        }
+        if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
+            return URI_TYPE_RESOURCE;
+        }
+        if (ContentResolver.SCHEME_FILE.equals(scheme)) {
+            if (uri.getPath().startsWith("/android_asset/")) {
+                return URI_TYPE_ASSET;
+            }
+            return URI_TYPE_FILE;
+        }
+        if ("data".equals(scheme)) {
+            return URI_TYPE_DATA;
+        }
+        if ("http".equals(scheme)) {
+            return URI_TYPE_HTTP;
+        }
+        if ("https".equals(scheme)) {
+            return URI_TYPE_HTTPS;
+        }
+        return URI_TYPE_UNKNOWN;
+    }
+    
+    public Uri remapUri(Uri uri) {
+        assertNonRelative(uri);
+        Uri pluginUri = pluginManager.remapUri(uri);
+        return pluginUri != null ? pluginUri : uri;
+    }
+
+    public String remapPath(String path) {
+        return remapUri(Uri.fromFile(new File(path))).getPath();
+    }
+    
+    /**
+     * Returns a File that points to the resource, or null if the resource
+     * is not on the local filesystem.
+     */
+    public File mapUriToFile(Uri uri) {
+        assertBackgroundThread();
+        switch (getUriType(uri)) {
+            case URI_TYPE_FILE:
+                return new File(uri.getPath());
+            case URI_TYPE_CONTENT: {
+                Cursor cursor = contentResolver.query(uri, LOCAL_FILE_PROJECTION, null, null, null);
+                if (cursor != null) {
+                    try {
+                        int columnIndex = cursor.getColumnIndex(LOCAL_FILE_PROJECTION[0]);
+                        if (columnIndex != -1 && cursor.getCount() > 0) {
+                            cursor.moveToFirst();
+                            String realPath = cursor.getString(columnIndex);
+                            if (realPath != null) {
+                                return new File(realPath);
+                            }
+                        }
+                    } finally {
+                        cursor.close();
+                    }
+                }
+            }
+        }
+        return null;
+    }
+    
+    public String getMimeType(Uri uri) {
+        switch (getUriType(uri)) {
+            case URI_TYPE_FILE:
+            case URI_TYPE_ASSET:
+                return getMimeTypeFromPath(uri.getPath());
+            case URI_TYPE_CONTENT:
+            case URI_TYPE_RESOURCE:
+                return contentResolver.getType(uri);
+            case URI_TYPE_DATA: {
+                return getDataUriMimeType(uri);
+            }
+            case URI_TYPE_HTTP:
+            case URI_TYPE_HTTPS: {
+                try {
+                    HttpURLConnection conn = httpClient.open(new URL(uri.toString()));
+                    conn.setDoInput(false);
+                    conn.setRequestMethod("HEAD");
+                    return conn.getHeaderField("Content-Type");
+                } catch (IOException e) {
+                }
+            }
+        }
+        
+        return null;
+    }
+    
+    private String getMimeTypeFromPath(String path) {
+        String extension = path;
+        int lastDot = extension.lastIndexOf('.');
+        if (lastDot != -1) {
+            extension = extension.substring(lastDot + 1);
+        }
+        // Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185).
+        extension = extension.toLowerCase(Locale.getDefault());
+        if (extension.equals("3ga")) {
+            return "audio/3gpp";
+        }
+        return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+    }
+    
+    /**
+     * Opens a stream to the givne URI, also providing the MIME type & length.
+     * @return Never returns null.
+     * @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be
+     *     resolved before being passed into this function.
+     * @throws Throws an IOException if the URI cannot be opened.
+     * @throws Throws an IllegalStateException if called on a foreground thread.
+     */
+    public OpenForReadResult openForRead(Uri uri) throws IOException {
+        return openForRead(uri, false);
+    }
+
+    /**
+     * Opens a stream to the givne URI, also providing the MIME type & length.
+     * @return Never returns null.
+     * @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be
+     *     resolved before being passed into this function.
+     * @throws Throws an IOException if the URI cannot be opened.
+     * @throws Throws an IllegalStateException if called on a foreground thread and skipThreadCheck is false.
+     */
+    public OpenForReadResult openForRead(Uri uri, boolean skipThreadCheck) throws IOException {
+        if (!skipThreadCheck) {
+            assertBackgroundThread();
+        }
+        switch (getUriType(uri)) {
+            case URI_TYPE_FILE: {
+                FileInputStream inputStream = new FileInputStream(uri.getPath());
+                String mimeType = getMimeTypeFromPath(uri.getPath());
+                long length = inputStream.getChannel().size();
+                return new OpenForReadResult(uri, inputStream, mimeType, length, null);
+            }
+            case URI_TYPE_ASSET: {
+                String assetPath = uri.getPath().substring(15);
+                AssetFileDescriptor assetFd = null;
+                InputStream inputStream;
+                long length = -1;
+                try {
+                    assetFd = assetManager.openFd(assetPath);
+                    inputStream = assetFd.createInputStream();
+                    length = assetFd.getLength();
+                } catch (FileNotFoundException e) {
+                    // Will occur if the file is compressed.
+                    inputStream = assetManager.open(assetPath);
+                }
+                String mimeType = getMimeTypeFromPath(assetPath);
+                return new OpenForReadResult(uri, inputStream, mimeType, length, assetFd);
+            }
+            case URI_TYPE_CONTENT:
+            case URI_TYPE_RESOURCE: {
+                String mimeType = contentResolver.getType(uri);
+                AssetFileDescriptor assetFd = contentResolver.openAssetFileDescriptor(uri, "r");
+                InputStream inputStream = assetFd.createInputStream();
+                long length = assetFd.getLength();
+                return new OpenForReadResult(uri, inputStream, mimeType, length, assetFd);
+            }
+            case URI_TYPE_DATA: {
+                OpenForReadResult ret = readDataUri(uri);
+                if (ret == null) {
+                    break;
+                }
+                return ret;
+            }
+            case URI_TYPE_HTTP:
+            case URI_TYPE_HTTPS: {
+                HttpURLConnection conn = httpClient.open(new URL(uri.toString()));
+                conn.setDoInput(true);
+                String mimeType = conn.getHeaderField("Content-Type");
+                int length = conn.getContentLength();
+                InputStream inputStream = conn.getInputStream();
+                return new OpenForReadResult(uri, inputStream, mimeType, length, null);
+            }
+        }
+        throw new FileNotFoundException("URI not supported by CordovaResourceApi: " + uri);
+    }
+
+    public OutputStream openOutputStream(Uri uri) throws IOException {
+        return openOutputStream(uri, false);
+    }
+
+    /**
+     * Opens a stream to the given URI.
+     * @return Never returns null.
+     * @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be
+     *     resolved before being passed into this function.
+     * @throws Throws an IOException if the URI cannot be opened.
+     */
+    public OutputStream openOutputStream(Uri uri, boolean append) throws IOException {
+        assertBackgroundThread();
+        switch (getUriType(uri)) {
+            case URI_TYPE_FILE: {
+                File localFile = new File(uri.getPath());
+                File parent = localFile.getParentFile();
+                if (parent != null) {
+                    parent.mkdirs();
+                }
+                return new FileOutputStream(localFile, append);
+            }
+            case URI_TYPE_CONTENT:
+            case URI_TYPE_RESOURCE: {
+                AssetFileDescriptor assetFd = contentResolver.openAssetFileDescriptor(uri, append ? "wa" : "w");
+                return assetFd.createOutputStream();
+            }
+        }
+        throw new FileNotFoundException("URI not supported by CordovaResourceApi: " + uri);
+    }
+    
+    public HttpURLConnection createHttpConnection(Uri uri) throws IOException {
+        assertBackgroundThread();
+        return httpClient.open(new URL(uri.toString()));
+    }
+    
+    // Copies the input to the output in the most efficient manner possible.
+    // Closes both streams.
+    public void copyResource(OpenForReadResult input, OutputStream outputStream) throws IOException {
+        assertBackgroundThread();
+        try {
+            InputStream inputStream = input.inputStream;
+            if (inputStream instanceof FileInputStream && outputStream instanceof FileOutputStream) {
+                FileChannel inChannel = ((FileInputStream)input.inputStream).getChannel();
+                FileChannel outChannel = ((FileOutputStream)outputStream).getChannel();
+                long offset = 0;
+                long length = input.length;
+                if (input.assetFd != null) {
+                    offset = input.assetFd.getStartOffset();
+                }
+                outChannel.transferFrom(inChannel, offset, length);
+            } else {
+                final int BUFFER_SIZE = 8192;
+                byte[] buffer = new byte[BUFFER_SIZE];
+                
+                for (;;) {
+                    int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE);
+                    
+                    if (bytesRead <= 0) {
+                        break;
+                    }
+                    outputStream.write(buffer, 0, bytesRead);
+                }
+            }            
+        } finally {
+            input.inputStream.close();
+            if (outputStream != null) {
+                outputStream.close();
+            }
+        }
+    }
+
+    public void copyResource(Uri sourceUri, OutputStream outputStream) throws IOException {
+        copyResource(openForRead(sourceUri), outputStream);
+    }
+
+    
+    private void assertBackgroundThread() {
+        if (threadCheckingEnabled) {
+            Thread curThread = Thread.currentThread();
+            if (curThread == Looper.getMainLooper().getThread()) {
+                throw new IllegalStateException("Do not perform IO operations on the UI thread. Use CordovaInterface.getThreadPool() instead.");
+            }
+            if (curThread == jsThread) {
+                throw new IllegalStateException("Tried to perform an IO operation on the WebCore thread. Use CordovaInterface.getThreadPool() instead.");
+            }
+        }
+    }
+    
+    private String getDataUriMimeType(Uri uri) {
+        String uriAsString = uri.getSchemeSpecificPart();
+        int commaPos = uriAsString.indexOf(',');
+        if (commaPos == -1) {
+            return null;
+        }
+        String[] mimeParts = uriAsString.substring(0, commaPos).split(";");
+        if (mimeParts.length > 0) {
+            return mimeParts[0];
+        }
+        return null;
+    }
+
+    private OpenForReadResult readDataUri(Uri uri) {
+        String uriAsString = uri.getSchemeSpecificPart();
+        int commaPos = uriAsString.indexOf(',');
+        if (commaPos == -1) {
+            return null;
+        }
+        String[] mimeParts = uriAsString.substring(0, commaPos).split(";");
+        String contentType = null;
+        boolean base64 = false;
+        if (mimeParts.length > 0) {
+            contentType = mimeParts[0];
+        }
+        for (int i = 1; i < mimeParts.length; ++i) {
+            if ("base64".equalsIgnoreCase(mimeParts[i])) {
+                base64 = true;
+            }
+        }
+        String dataPartAsString = uriAsString.substring(commaPos + 1);
+        byte[] data = base64 ? Base64.decode(dataPartAsString, Base64.DEFAULT) : EncodingUtils.getBytes(dataPartAsString, "UTF-8");
+        InputStream inputStream = new ByteArrayInputStream(data);
+        return new OpenForReadResult(uri, inputStream, contentType, data.length, null);
+    }
+    
+    private static void assertNonRelative(Uri uri) {
+        if (!uri.isAbsolute()) {
+            throw new IllegalArgumentException("Relative URIs are not supported.");
+        }
+    }
+    
+    public static final class OpenForReadResult {
+        public final Uri uri;
+        public final InputStream inputStream;
+        public final String mimeType;
+        public final long length;
+        public final AssetFileDescriptor assetFd;
+        
+        OpenForReadResult(Uri uri, InputStream inputStream, String mimeType, long length, AssetFileDescriptor assetFd) {
+            this.uri = uri;
+            this.inputStream = inputStream;
+            this.mimeType = mimeType;
+            this.length = length;
+            this.assetFd = assetFd;
+        }
+    }
+}
diff --git a/framework/src/org/apache/cordova/CordovaWebView.java b/framework/src/org/apache/cordova/CordovaWebView.java
new file mode 100755
index 0000000..3a93ce0
--- /dev/null
+++ b/framework/src/org/apache/cordova/CordovaWebView.java
@@ -0,0 +1,1074 @@
+/*
+       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 org.apache.cordova;
+
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Locale;
+
+import org.apache.cordova.Config;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.LOG;
+import org.apache.cordova.PluginManager;
+import org.apache.cordova.PluginResult;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import com.amazon.android.webkit.AmazonWebBackForwardList;
+import com.amazon.android.webkit.AmazonWebHistoryItem;
+import com.amazon.android.webkit.AmazonWebChromeClient;
+import com.amazon.android.webkit.AmazonWebKitFactories;
+import com.amazon.android.webkit.AmazonWebSettings;
+import com.amazon.android.webkit.AmazonWebView;
+import com.amazon.android.webkit.AmazonWebKitFactory;
+
+import android.widget.FrameLayout;
+
+public class CordovaWebView extends AmazonWebView {
+
+    
+    public static final String TAG = "CordovaWebView";
+    
+    public static final String CORDOVA_VERSION = "3.2.0-dev";
+
+    private ArrayList<Integer> keyDownCodes = new ArrayList<Integer>();
+    private ArrayList<Integer> keyUpCodes = new ArrayList<Integer>();
+
+    public PluginManager pluginManager;
+    private boolean paused;
+
+    private BroadcastReceiver receiver;
+
+
+    /** Activities and other important classes **/
+    private CordovaInterface cordova;
+    CordovaWebViewClient viewClient;
+    @SuppressWarnings("unused")
+    private CordovaChromeClient chromeClient;
+
+    private String url;
+
+    // Flag to track that a loadUrl timeout occurred
+    int loadUrlTimeout = 0;
+
+    private boolean bound;
+
+    private boolean handleButton = false;
+    
+    private long lastMenuEventTime = 0;
+
+    NativeToJsMessageQueue jsMessageQueue;
+    ExposedJsApi exposedJsApi;
+
+    /** custom view created by the browser (a video player for example) */
+    private View mCustomView;
+    private AmazonWebChromeClient.CustomViewCallback mCustomViewCallback;
+
+    private ActivityResult mResult = null;
+
+    private CordovaResourceApi resourceApi;
+
+    private static final String APPCACHE_DIR = "database";
+
+    private static final String APPCACHE_DIR_EMPTY = "NONEXISTENT_PATH";
+    private static final String SAFARI_UA = "Safari";
+    private static final String MOBILE_SAFARI_UA = "Mobile Safari";
+    private static final String CORDOVA_AMAZON_FIREOS_UA = "cordova-amazon-fireos/" + CORDOVA_VERSION;
+
+    private static final String LOCAL_STORAGE_DIR = "database";
+
+    /**
+     * Arbitrary size limit for app cache resources
+     */
+    public static final long APP_CACHE_LIMIT = (1024 * 1024 * 50);
+
+    /**
+     * An enumeration to specify the desired back-end to use when constructing
+     * the WebView.
+     */
+    public enum WebViewBackend {
+
+        /** The stock Android WebView back-end */
+        ANDROID,
+
+        /** The Chromium AmazonWebView beck-end */
+        CHROMIUM,
+
+        /**
+         * Automatically select the back-end depending on the device
+         * configuration
+         */
+        AUTOMATIC;
+
+        /**
+         * @return the Android string resource ID for the name of this back-end
+         */
+        public int getNameRes() {
+            switch (this) {
+            case ANDROID:
+                return R.string.backend_name_stock_android;
+            case CHROMIUM:
+                return R.string.backend_name_amazon_chromium;
+            case AUTOMATIC:
+            default:
+                return R.string.backend_name_unknown;
+            }
+        }
+    }
+    class ActivityResult {
+        
+        int request;
+        int result;
+        Intent incoming;
+        
+        public ActivityResult(int req, int res, Intent intent) {
+            request = req;
+            result = res;
+            incoming = intent;
+        }
+
+        
+    }
+    
+    static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER =
+            new FrameLayout.LayoutParams(
+            ViewGroup.LayoutParams.MATCH_PARENT,
+            ViewGroup.LayoutParams.MATCH_PARENT,
+            Gravity.CENTER);
+    
+    /**
+     * Constructor.
+     *
+     * @param context
+     */
+    public CordovaWebView(Context context) {
+        super(context);
+
+        if (CordovaInterface.class.isInstance(context))
+        {
+            this.cordova = (CordovaInterface) context;
+            this.cordova.getFactory().initializeWebView(this, 0xFFFFFF, false, null);
+        }
+        else
+        {
+            Log.d(TAG, "Your activity must implement CordovaInterface to work");
+        }
+        this.loadConfiguration();
+        this.setup();
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param context
+     * @param attrs
+     */
+    public CordovaWebView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        if (CordovaInterface.class.isInstance(context))
+        {
+            this.cordova = (CordovaInterface) context;
+            this.cordova.getFactory().initializeWebView(this, 0xFFFFFF, false, null);
+        }
+        else
+        {
+            Log.d(TAG, "Your activity must implement CordovaInterface to work");
+        }
+        this.setWebChromeClient(new CordovaChromeClient(this.cordova, this));
+        this.initWebViewClient(this.cordova);
+        this.loadConfiguration();
+        this.setup();
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param context
+     * @param attrs
+     * @param defStyle
+     *
+     */
+    public CordovaWebView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        if (CordovaInterface.class.isInstance(context))
+        {
+            this.cordova = (CordovaInterface) context;
+            this.cordova.getFactory().initializeWebView(this, 0xFFFFFF, false, null);
+        }
+        else
+        {
+            Log.d(TAG, "Your activity must implement CordovaInterface to work");
+        }
+        this.setWebChromeClient(new CordovaChromeClient(this.cordova, this));
+        this.loadConfiguration();
+        this.setup();
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param context
+     * @param attrs
+     * @param defStyle
+     * @param privateBrowsing
+     */
+    @TargetApi(11)
+    public CordovaWebView(Context context, AttributeSet attrs, int defStyle, boolean privateBrowsing) {
+        // super(context, attrs, defStyle, privateBrowsing); // DEPRECATED
+        super(context, attrs, defStyle);
+
+        if (CordovaInterface.class.isInstance(context))
+        {
+            this.cordova = (CordovaInterface) context;
+            this.cordova.getFactory().initializeWebView(this, 0xFFFFFF, privateBrowsing, null);
+        }
+        else
+        {
+            Log.d(TAG, "Your activity must implement CordovaInterface to work");
+        }
+        this.setWebChromeClient(new CordovaChromeClient(this.cordova));
+        this.initWebViewClient(this.cordova);
+        this.loadConfiguration();
+        this.setup();
+    }
+
+
+    private void initWebViewClient(CordovaInterface cordova) {
+        if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB ||
+                android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.JELLY_BEAN_MR1)
+        {
+            this.setWebViewClient(new CordovaWebViewClient(this.cordova, this));
+        }
+        else
+        {
+            this.setWebViewClient(new IceCreamCordovaWebViewClient(this.cordova, this));
+        }
+    }
+
+    /**
+     * Initialize webview.
+     */
+    @SuppressWarnings("deprecation")
+    @SuppressLint("NewApi")
+    private void setup() {
+        this.setInitialScale(0);
+        this.setVerticalScrollBarEnabled(false);
+        if (shouldRequestFocusOnInit()) {
+			this.requestFocusFromTouch();
+		}
+		// Enable JavaScript
+        AmazonWebSettings settings = this.getSettings();
+        settings.setJavaScriptEnabled(true);
+        settings.setMediaPlaybackRequiresUserGesture(false);    
+        
+        // Set the nav dump for HTC 2.x devices (disabling for ICS, deprecated entirely for Jellybean 4.2)
+        try {
+            Method gingerbread_getMethod =  AmazonWebSettings.class.getMethod("setNavDump", new Class[] { boolean.class });
+            
+            String manufacturer = android.os.Build.MANUFACTURER;
+            Log.d(TAG, "CordovaWebView is running on device made by: " + manufacturer);
+            if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB &&
+                    android.os.Build.MANUFACTURER.contains("HTC"))
+            {
+                gingerbread_getMethod.invoke(settings, true);
+            }
+        } catch (NoSuchMethodException e) {
+            Log.d(TAG, "We are on a modern version of Android, we will deprecate HTC 2.3 devices in 2.8");
+        } catch (IllegalArgumentException e) {
+            Log.d(TAG, "Doing the NavDump failed with bad arguments");
+        } catch (IllegalAccessException e) {
+            Log.d(TAG, "This should never happen: IllegalAccessException means this isn't Android anymore");
+        } catch (InvocationTargetException e) {
+            Log.d(TAG, "This should never happen: InvocationTargetException means this isn't Android anymore.");
+        }
+
+        //We don't save any form data in the application
+        settings.setSaveFormData(false);
+        settings.setSavePassword(false);
+        
+        // Jellybean rightfully tried to lock this down. Too bad they didn't give us a whitelist
+        // while we do this
+        if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
+            Level16Apis.enableUniversalAccess(settings);
+                
+        if (getWebViewBackend(this.cordova.getFactory()) == WebViewBackend.ANDROID) {
+        	File appCacheDir = this.cordova.getActivity().getDir(APPCACHE_DIR, Context.MODE_PRIVATE);
+            if (appCacheDir.exists()) {
+                settings.setAppCachePath(appCacheDir.getPath());
+                settings.setAppCacheMaxSize(APP_CACHE_LIMIT);
+                settings.setAppCacheEnabled(true);
+            } else {
+                // shouldn't get here...
+                Log.e(TAG, "Unable to construct application cache directory, feature disabled");
+            }
+
+            File storageDir = this.cordova.getActivity().getDir(LOCAL_STORAGE_DIR, Context.MODE_PRIVATE);
+            if (storageDir.exists()) {
+                settings.setDatabasePath(storageDir.getPath());
+                settings.setDatabaseEnabled(true);
+                settings.setGeolocationDatabasePath(storageDir.getPath());
+            } else {
+                // shouldn't get here...
+                Log.e(TAG, "Unable to construct local storage directory, feature disabled");
+            }
+        } else {
+            // setting a custom path (as well as the max cache size) is not supported by Chromium,
+            // however setting the path to a non-null non-empty string is required for it to function
+            settings.setAppCachePath(APPCACHE_DIR_EMPTY);
+            settings.setAppCacheEnabled(true);
+            
+            // enable the local storage database normally with the Chromium back-end
+            settings.setDatabaseEnabled(true);
+        }
+
+        // Enable DOM storage
+        settings.setDomStorageEnabled(true);
+
+        // Enable built-in geolocation
+        settings.setGeolocationEnabled(true);
+        
+         // Fix UserAgent string
+        String userAgent = settings.getUserAgentString();
+        if ((userAgent.indexOf(MOBILE_SAFARI_UA) == -1) && (userAgent.indexOf(SAFARI_UA) != -1)) {
+            // Replace Safari with Mobile Safari
+            userAgent = userAgent.replace(SAFARI_UA, MOBILE_SAFARI_UA);
+        }
+        userAgent = userAgent.concat(" " + CORDOVA_AMAZON_FIREOS_UA); 
+        settings.setUserAgentString(userAgent);
+
+        // Fix for CB-1405
+        // Google issue 4641
+        this.updateUserAgentString();
+        
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+        if (this.receiver == null) {
+            this.receiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    updateUserAgentString();
+                }
+            };
+            this.cordova.getActivity().registerReceiver(this.receiver, intentFilter);
+        }
+        // end CB-1405
+
+        settings.setUseWideViewPort(true);
+
+        pluginManager = new PluginManager(this, this.cordova);
+        jsMessageQueue = new NativeToJsMessageQueue(this, cordova);
+        exposedJsApi = new ExposedJsApi(pluginManager, jsMessageQueue);
+        resourceApi = new CordovaResourceApi(this.getContext(), pluginManager);
+        exposeJsInterface();
+    }
+    
+    /**
+     * The actual back-end used when constructing the WebView. Note that this
+     * may differ from the requested back-end depending on the device
+     * configuration.
+     * 
+     * @return either {@link WebViewBackend#AMAZON} or
+     *         {@link WebViewBackend#ANDROID}
+     */
+    static WebViewBackend getWebViewBackend(AmazonWebKitFactory factory) {
+    	// This is to figure out if WebView is using Chromium based webapp runtime or stock AndroidWebView.
+    	// On Kindle devices default is Chromium based. There is no public API to figure out the difference. 
+    	// EmbeddedWebKitFactory is not a plublic class so only way to check is using this AmazonWebKitFactories.EMBEDDED_FACTORY class name.
+    	if (factory.getClass().getName().equals(AmazonWebKitFactories.EMBEDDED_FACTORY) ) {
+            return WebViewBackend.CHROMIUM;
+        }
+        return WebViewBackend.ANDROID;
+    }
+
+	/**
+	 * Override this method to decide wether or not you need to request the
+	 * focus when your application start
+	 * 
+	 * @return
+	 */
+    protected boolean shouldRequestFocusOnInit() {
+		return true;
+	}
+
+	private void updateUserAgentString() {
+        this.getSettings().getUserAgentString();
+    }
+
+    private void exposeJsInterface() {
+        int SDK_INT = Build.VERSION.SDK_INT;
+        boolean isHoneycomb = (SDK_INT >= Build.VERSION_CODES.HONEYCOMB && SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR2);
+        if (isHoneycomb || (SDK_INT < Build.VERSION_CODES.GINGERBREAD)) {
+            Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old.");
+            // Bug being that Java Strings do not get converted to JS strings automatically.
+            // This isn't hard to work-around on the JS side, but it's easier to just
+            // use the prompt bridge instead.
+            return;            
+        } else if (SDK_INT < Build.VERSION_CODES.HONEYCOMB && Build.MANUFACTURER.equals("unknown")) {
+            // addJavascriptInterface crashes on the 2.3 emulator.
+            Log.i(TAG, "Disabled addJavascriptInterface() bridge callback due to a bug on the 2.3 emulator");
+            return;
+        }
+        this.addJavascriptInterface(exposedJsApi, "_cordovaNative");
+    }
+
+    /**
+     * Set the WebViewClient.
+     *
+     * @param client
+     */
+    public void setWebViewClient(CordovaWebViewClient client) {
+        this.viewClient = client;
+        super.setWebViewClient(client);
+    }
+
+    /**
+     * Set the AmazonWebChromeClient.
+     *
+     * @param client
+     */
+    public void setWebChromeClient(CordovaChromeClient client) {
+        this.chromeClient = client;
+        super.setWebChromeClient(client);
+    }
+    
+    public CordovaChromeClient getWebChromeClient() {
+        return this.chromeClient;
+    }
+
+    /**
+     * Load the url into the webview.
+     *
+     * @param url
+     */
+    @Override
+    public void loadUrl(String url) {
+        if (url.equals("about:blank") || url.startsWith("javascript:")) {
+            this.loadUrlNow(url);
+        }
+        else {
+
+            String initUrl = this.getProperty("url", null);
+
+            // If first page of app, then set URL to load to be the one passed in
+            if (initUrl == null) {
+                this.loadUrlIntoView(url);
+            }
+            // Otherwise use the URL specified in the activity's extras bundle
+            else {
+                this.loadUrlIntoView(initUrl);
+            }
+        }
+    }
+
+    /**
+     * Load the url into the webview after waiting for period of time.
+     * This is used to display the splashscreen for certain amount of time.
+     *
+     * @param url
+     * @param time              The number of ms to wait before loading webview
+     */
+    public void loadUrl(final String url, int time) {
+        String initUrl = this.getProperty("url", null);
+
+        // If first page of app, then set URL to load to be the one passed in
+        if (initUrl == null) {
+            this.loadUrlIntoView(url, time);
+        }
+        // Otherwise use the URL specified in the activity's extras bundle
+        else {
+            this.loadUrlIntoView(initUrl);
+        }
+    }
+
+    /**
+     * Load the url into the webview.
+     *
+     * @param url
+     */
+    public void loadUrlIntoView(final String url) {
+        LOG.d(TAG, ">>> loadUrl(" + url + ")");
+
+        this.url = url;
+        this.pluginManager.init();
+
+
+        // Create a timeout timer for loadUrl
+        final CordovaWebView me = this;
+        final int currentLoadUrlTimeout = me.loadUrlTimeout;
+        final int loadUrlTimeoutValue = Integer.parseInt(this.getProperty("LoadUrlTimeoutValue", "20000"));
+
+        // Timeout error method
+        final Runnable loadError = new Runnable() {
+            public void run() {
+                me.stopLoading();
+                LOG.e(TAG, "CordovaWebView: TIMEOUT ERROR!");
+                if (viewClient != null) {
+                    viewClient.onReceivedError(me, -6, "The connection to the server was unsuccessful.", url);
+                }
+            }
+        };
+
+        // Timeout timer method
+        final Runnable timeoutCheck = new Runnable() {
+            public void run() {
+                try {
+                    synchronized (this) {
+                        wait(loadUrlTimeoutValue);
+                    }
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+
+                // If timeout, then stop loading and handle error
+                if (me.loadUrlTimeout == currentLoadUrlTimeout) {
+                    me.cordova.getActivity().runOnUiThread(loadError);
+                }
+            }
+        };
+
+        // Load url
+        this.cordova.getActivity().runOnUiThread(new Runnable() {
+            public void run() {
+                Thread thread = new Thread(timeoutCheck);
+                thread.start();
+                me.loadUrlNow(url);
+            }
+        });
+    }
+
+    /**
+     * Load URL in webview.
+     *
+     * @param url
+     */
+    void loadUrlNow(String url) {
+        if (LOG.isLoggable(LOG.DEBUG) && !url.startsWith("javascript:")) {
+            LOG.d(TAG, ">>> loadUrlNow()");
+        }
+        if (url.startsWith("file://") || url.startsWith("javascript:") || Config.isUrlWhiteListed(url)) {
+            super.loadUrl(url);
+        }
+    }
+
+    /**
+     * Load the url into the webview after waiting for period of time.
+     * This is used to display the splashscreen for certain amount of time.
+     *
+     * @param url
+     * @param time              The number of ms to wait before loading webview
+     */
+    public void loadUrlIntoView(final String url, final int time) {
+
+        // If not first page of app, then load immediately
+        // Add support for browser history if we use it.
+        if ((url.startsWith("javascript:")) || this.canGoBack()) {
+        }
+
+        // If first page, then show splashscreen
+        else {
+
+            LOG.d(TAG, "loadUrlIntoView(%s, %d)", url, time);
+
+            // Send message to show splashscreen now if desired
+            this.postMessage("splashscreen", "show");
+        }
+
+        // Load url
+        this.loadUrlIntoView(url);
+    }
+    
+    /**
+     * Send JavaScript statement back to JavaScript.
+     * (This is a convenience method)
+     *
+     * @param statement
+     */
+    public void sendJavascript(String statement) {
+        this.jsMessageQueue.addJavaScript(statement);
+    }
+
+    /**
+     * Send a plugin result back to JavaScript.
+     * (This is a convenience method)
+     *
+     * @param result
+     * @param callbackId
+     */
+    public void sendPluginResult(PluginResult result, String callbackId) {
+        this.jsMessageQueue.addPluginResult(result, callbackId);
+    }
+
+    /**
+     * Send a message to all plugins.
+     *
+     * @param id            The message id
+     * @param data          The message data
+     */
+    public void postMessage(String id, Object data) {
+        if (this.pluginManager != null) {
+            this.pluginManager.postMessage(id, data);
+        }
+    }
+
+
+    /**
+     * Go to previous page in history.  (We manage our own history)
+     *
+     * @return true if we went back, false if we are already at top
+     */
+    public boolean backHistory() {
+
+        // Check webview first to see if there is a history
+        // This is needed to support curPage#diffLink, since they are added to appView's history, but not our history url array (JQMobile behavior)
+        if (super.canGoBack()) {
+            printBackForwardList();
+            super.goBack();
+            
+            return true;
+        }
+        return false;
+    }
+
+
+    /**
+     * Load the specified URL in the Cordova webview or a new browser instance.
+     *
+     * NOTE: If openExternal is false, only URLs listed in whitelist can be loaded.
+     *
+     * @param url           The url to load.
+     * @param openExternal  Load url in browser instead of Cordova webview.
+     * @param clearHistory  Clear the history stack, so new page becomes top of history
+     * @param params        Parameters for new app
+     */
+    public void showWebPage(String url, boolean openExternal, boolean clearHistory, HashMap<String, Object> params) {
+        LOG.d(TAG, "showWebPage(%s, %b, %b, HashMap", url, openExternal, clearHistory);
+
+        // If clearing history
+        if (clearHistory) {
+            this.clearHistory();
+        }
+
+        // If loading into our webview
+        if (!openExternal) {
+
+            // Make sure url is in whitelist
+            if (url.startsWith("file://") || Config.isUrlWhiteListed(url)) {
+                // TODO: What about params?
+                // Load new URL
+                this.loadUrl(url);
+            }
+            // Load in default viewer if not
+            else {
+                LOG.w(TAG, "showWebPage: Cannot load URL into webview since it is not in white list.  Loading into browser instead. (URL=" + url + ")");
+                try {
+                    Intent intent = new Intent(Intent.ACTION_VIEW);
+                    intent.setData(Uri.parse(url));
+                    cordova.getActivity().startActivity(intent);
+                } catch (android.content.ActivityNotFoundException e) {
+                    LOG.e(TAG, "Error loading url " + url, e);
+                }
+            }
+        }
+
+        // Load in default view intent
+        else {
+            try {
+                Intent intent = new Intent(Intent.ACTION_VIEW);
+                intent.setData(Uri.parse(url));
+                cordova.getActivity().startActivity(intent);
+            } catch (android.content.ActivityNotFoundException e) {
+                LOG.e(TAG, "Error loading url " + url, e);
+            }
+        }
+    }
+
+    /**
+     * Check configuration parameters from Config.
+     * Approved list of URLs that can be loaded into Cordova
+     *      <access origin="http://server regexp" subdomains="true" />
+     * Log level: ERROR, WARN, INFO, DEBUG, VERBOSE (default=ERROR)
+     *      <log level="DEBUG" />
+     */
+    private void loadConfiguration() {
+ 
+        if ("true".equals(this.getProperty("Fullscreen", "false"))) {
+            this.cordova.getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
+            this.cordova.getActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
+        }
+    }
+
+    /**
+     * Get string property for activity.
+     *
+     * @param name
+     * @param defaultValue
+     * @return
+     */
+    public String getProperty(String name, String defaultValue) {
+        Bundle bundle = this.cordova.getActivity().getIntent().getExtras();
+        if (bundle == null) {
+            return defaultValue;
+        }
+        name = name.toLowerCase(Locale.getDefault());
+        Object p = bundle.get(name);
+        if (p == null) {
+            return defaultValue;
+        }
+        return p.toString();
+    }
+
+    /*
+     * onKeyDown
+     */
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event)
+    {
+        if(keyDownCodes.contains(keyCode))
+        {
+            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
+                    // only override default behavior is event bound
+                    LOG.d(TAG, "Down Key Hit");
+                    this.loadUrl("javascript:cordova.fireDocumentEvent('volumedownbutton');");
+                    return true;
+            }
+            // If volumeup key
+            else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+                    LOG.d(TAG, "Up Key Hit");
+                    this.loadUrl("javascript:cordova.fireDocumentEvent('volumeupbutton');");
+                    return true;
+            }
+            else
+            {
+                return super.onKeyDown(keyCode, event);
+            }
+        }
+        else if(keyCode == KeyEvent.KEYCODE_BACK)
+        {
+            return !(this.startOfHistory()) || this.bound;
+        }
+        else if(keyCode == KeyEvent.KEYCODE_MENU)
+        {
+            //How did we get here?  Is there a childView?
+            View childView = this.getFocusedChild();
+            if(childView != null)
+            {
+                //Make sure we close the keyboard if it's present
+                InputMethodManager imm = (InputMethodManager) cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+                imm.hideSoftInputFromWindow(childView.getWindowToken(), 0);
+                cordova.getActivity().openOptionsMenu();
+                return true;
+            } else {
+                return super.onKeyDown(keyCode, event);
+            }
+        }
+        
+        return super.onKeyDown(keyCode, event);
+    }
+    
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event)
+    {
+        // If back key
+        if (keyCode == KeyEvent.KEYCODE_BACK) {
+            // A custom view is currently displayed  (e.g. playing a video)
+            if(mCustomView != null) {
+                this.hideCustomView();
+            } else {
+                // The webview is currently displayed
+                // If back key is bound, then send event to JavaScript
+                if (this.bound) {
+                    this.loadUrl("javascript:cordova.fireDocumentEvent('backbutton');");
+                    return true;
+                } else {
+                    // If not bound
+                    // Go to previous page in webview if it is possible to go back
+                    if (this.backHistory()) {
+                        return true;
+                    }
+                    // If not, then invoke default behaviour
+                    else {
+                        //this.activityState = ACTIVITY_EXITING;
+                    	//return false;
+                    	// If they hit back button when app is initializing, app should exit instead of hang until initilazation (CB2-458)
+                    	this.cordova.getActivity().finish();
+                    }
+                }
+            }
+        }
+        // Legacy
+        else if (keyCode == KeyEvent.KEYCODE_MENU) {
+            if (this.lastMenuEventTime < event.getEventTime()) {
+                this.loadUrl("javascript:cordova.fireDocumentEvent('menubutton');");
+            }
+            this.lastMenuEventTime = event.getEventTime();
+            return super.onKeyUp(keyCode, event);
+        }
+        // If search key
+        else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
+            this.loadUrl("javascript:cordova.fireDocumentEvent('searchbutton');");
+            return true;
+        }
+        else if(keyUpCodes.contains(keyCode))
+        {
+            //What the hell should this do?
+            return super.onKeyUp(keyCode, event);
+        }
+
+        //Does webkit change this behavior?
+        return super.onKeyUp(keyCode, event);
+    }
+
+    
+    public void bindButton(boolean override)
+    {
+        this.bound = override;
+    }
+
+    public void bindButton(String button, boolean override) {
+        // TODO Auto-generated method stub
+        if (button.compareTo("volumeup")==0) {
+          keyDownCodes.add(KeyEvent.KEYCODE_VOLUME_UP);
+        }
+        else if (button.compareTo("volumedown")==0) {
+          keyDownCodes.add(KeyEvent.KEYCODE_VOLUME_DOWN);
+        }
+      }
+
+    public void bindButton(int keyCode, boolean keyDown, boolean override) {
+       if(keyDown)
+       {
+           keyDownCodes.add(keyCode);
+       }
+       else
+       {
+           keyUpCodes.add(keyCode);
+       }
+    }
+
+    public boolean isBackButtonBound()
+    {
+        return this.bound;
+    }
+    
+    public void handlePause(boolean keepRunning)
+    {
+        LOG.d(TAG, "Handle the pause");
+        // Send pause event to JavaScript
+        this.loadUrl("javascript:try{cordova.fireDocumentEvent('pause');}catch(e){console.log('exception firing pause event from native');};");
+
+        // Forward to plugins
+        if (this.pluginManager != null) {
+            this.pluginManager.onPause(keepRunning);
+        }
+
+        // If app doesn't want to run in background
+        if (!keepRunning) {
+            // Pause JavaScript timers (including setInterval)
+            this.pauseTimers();
+            this.onPause();
+        }
+        paused = true;
+   
+    }
+    
+    public void handleResume(boolean keepRunning, boolean activityResultKeepRunning)
+    {
+
+        this.loadUrl("javascript:try{cordova.fireDocumentEvent('resume');}catch(e){console.log('exception firing resume event from native');};");
+        
+        // Forward to plugins
+        if (this.pluginManager != null) {
+            this.pluginManager.onResume(keepRunning);
+        }
+
+        //resume first and then resumeTimers
+        this.onResume();
+        // Resume JavaScript timers (including setInterval)
+        this.resumeTimers();
+        paused = false;
+    }
+    
+    public void handleDestroy()
+    {
+        // Send destroy event to JavaScript
+        this.loadUrl("javascript:try{cordova.require('cordova/channel').onDestroy.fire();}catch(e){console.log('exception firing destroy event from native');};");
+
+        // Load blank page so that JavaScript onunload is called
+        this.loadUrl("about:blank");
+
+        // Forward to plugins
+        if (this.pluginManager != null) {
+            this.pluginManager.onDestroy();
+        }
+        
+        // unregister the receiver
+        if (this.receiver != null) {
+            try {
+                this.cordova.getActivity().unregisterReceiver(this.receiver);
+            } catch (Exception e) {
+                Log.e(TAG, "Error unregistering configuration receiver: " + e.getMessage(), e);
+            }
+        }
+    }
+    
+    public void onNewIntent(Intent intent)
+    {
+        //Forward to plugins
+        if (this.pluginManager != null) {
+            this.pluginManager.onNewIntent(intent);
+        }
+    }
+    
+    public boolean isPaused()
+    {
+        return paused;
+    }
+
+    public boolean hadKeyEvent() {
+        return handleButton;
+    }
+
+    // Wrapping these functions in their own class prevents warnings in adb like:
+    // VFY: unable to resolve virtual method 285: Landroid/webkit/AmazonWebSettings;.setAllowUniversalAccessFromFileURLs
+    @TargetApi(16)
+    private static class Level16Apis {
+        static void enableUniversalAccess(AmazonWebSettings settings) {
+            settings.setAllowUniversalAccessFromFileURLs(true);
+        }
+    }
+    
+    public void printBackForwardList() {
+        AmazonWebBackForwardList currentList = this.copyBackForwardList();
+        int currentSize = currentList.getSize();
+        for(int i = 0; i < currentSize; ++i)
+        {
+            AmazonWebHistoryItem item = currentList.getItemAtIndex(i);
+            String url = item.getUrl();
+            LOG.d(TAG, "The URL at index: " + Integer.toString(i) + "is " + url );
+        }
+    }
+    
+    
+    //Can Go Back is BROKEN!
+    public boolean startOfHistory()
+    {
+        AmazonWebBackForwardList currentList = this.copyBackForwardList();
+        AmazonWebHistoryItem item = currentList.getItemAtIndex(0);
+        if( item!=null){	// Null-fence in case they haven't called loadUrl yet (CB-2458)
+	        String url = item.getUrl();
+	        String currentUrl = this.getUrl();
+	        LOG.d(TAG, "The current URL is: " + currentUrl);
+	        LOG.d(TAG, "The URL at item 0 is:" + url);
+	        return currentUrl.equals(url);
+        }
+        return false;
+    }
+
+    public void showCustomView(View view, AmazonWebChromeClient.CustomViewCallback callback) {
+        // This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0
+        Log.d(TAG, "showing Custom View");
+        // if a view already exists then immediately terminate the new one
+        if (mCustomView != null) {
+            callback.onCustomViewHidden();
+            return;
+        }
+        
+        // Store the view and its callback for later (to kill it properly)
+        mCustomView = view;
+        mCustomViewCallback = callback;
+        
+        // Add the custom view to its container.
+        ViewGroup parent = (ViewGroup) this.getParent();
+        parent.addView(view, COVER_SCREEN_GRAVITY_CENTER);
+        
+        // Hide the content view.
+        this.setVisibility(View.GONE);
+        
+        // Finally show the custom view container.
+        parent.setVisibility(View.VISIBLE);
+        parent.bringToFront();
+    }
+
+    public void hideCustomView() {
+        // This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0
+        Log.d(TAG, "Hidding Custom View");
+        if (mCustomView == null) return;
+
+        // Hide the custom view.
+        mCustomView.setVisibility(View.GONE);
+        
+        // Remove the custom view from its container.
+        ViewGroup parent = (ViewGroup) this.getParent();
+        parent.removeView(mCustomView);
+        mCustomView = null;
+        mCustomViewCallback.onCustomViewHidden();
+        
+        // Show the content view.
+        this.setVisibility(View.VISIBLE);
+    }
+    
+    /**
+     * if the video overlay is showing then we need to know 
+     * as it effects back button handling
+     * 
+     * @return
+     */
+    public boolean isCustomViewShowing() {
+        return mCustomView != null;
+    }
+    
+    public AmazonWebBackForwardList restoreState(Bundle savedInstanceState)
+    {
+        AmazonWebBackForwardList myList = super.restoreState(savedInstanceState);
+        Log.d(TAG, "AmazonWebView restoration crew now restoring!");
+        //Initialize the plugin manager once more
+        this.pluginManager.init();
+        return myList;
+    }
+
+    public void storeResult(int requestCode, int resultCode, Intent intent) {
+        mResult = new ActivityResult(requestCode, resultCode, intent);
+    }
+    
+    public CordovaResourceApi getResourceApi() {
+        return resourceApi;
+    }
+}
diff --git a/framework/src/org/apache/cordova/CordovaWebViewClient.java b/framework/src/org/apache/cordova/CordovaWebViewClient.java
new file mode 100755
index 0000000..00ce660
--- /dev/null
+++ b/framework/src/org/apache/cordova/CordovaWebViewClient.java
@@ -0,0 +1,462 @@
+/*
+       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 org.apache.cordova;
+
+import java.io.ByteArrayInputStream;
+import java.util.Hashtable;
+
+import org.apache.cordova.CordovaInterface;
+
+import org.apache.cordova.LOG;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.net.http.SslError;
+import android.util.Log;
+import android.view.View;
+import com.amazon.android.webkit.AmazonHttpAuthHandler;
+import com.amazon.android.webkit.AmazonSslErrorHandler;
+import com.amazon.android.webkit.AmazonWebResourceResponse;
+import com.amazon.android.webkit.AmazonWebView;
+import com.amazon.android.webkit.AmazonWebViewClient;
+
+/**
+ * This class is the AmazonWebViewClient that implements callbacks for our web view.
+ */
+public class CordovaWebViewClient extends AmazonWebViewClient {
+
+	private static final String TAG = "CordovaWebViewClient";
+	private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/";
+    CordovaInterface cordova;
+    CordovaWebView appView;
+    private boolean doClearHistory = false;
+
+    /** The authorization tokens. */
+    private Hashtable<String, AuthenticationToken> authenticationTokens = new Hashtable<String, AuthenticationToken>();
+
+    /**
+     * Constructor.
+     *
+     * @param cordova
+     */
+    public CordovaWebViewClient(CordovaInterface cordova) {
+        this.cordova = cordova;
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param cordova
+     * @param view
+     */
+    public CordovaWebViewClient(CordovaInterface cordova, CordovaWebView view) {
+        this.cordova = cordova;
+        this.appView = view;
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param view
+     */
+    public void setWebView(CordovaWebView view) {
+        this.appView = view;
+    }
+
+
+    // Parses commands sent by setting the webView's URL to:
+    // cdvbrg:service/action/callbackId#jsonArgs
+	private void handleExecUrl(String url) {
+		int idx1 = CORDOVA_EXEC_URL_PREFIX.length();
+		int idx2 = url.indexOf('#', idx1 + 1);
+		int idx3 = url.indexOf('#', idx2 + 1);
+		int idx4 = url.indexOf('#', idx3 + 1);
+		if (idx1 == -1 || idx2 == -1 || idx3 == -1 || idx4 == -1) {
+			Log.e(TAG, "Could not decode URL command: " + url);
+			return;
+		}
+		String service    = url.substring(idx1, idx2);
+		String action     = url.substring(idx2 + 1, idx3);
+		String callbackId = url.substring(idx3 + 1, idx4);
+		String jsonArgs   = url.substring(idx4 + 1);
+        appView.pluginManager.exec(service, action, callbackId, jsonArgs);
+	}
+
+    /**
+     * Give the host application a chance to take over the control when a new url
+     * is about to be loaded in the current AmazonWebView.
+     *
+     * @param view          The AmazonWebView that is initiating the callback.
+     * @param url           The url to be loaded.
+     * @return              true to override, false for default behavior
+     */
+	@Override
+    public boolean shouldOverrideUrlLoading(AmazonWebView view, String url) {
+    	// Check if it's an exec() bridge command message.
+    	if (NativeToJsMessageQueue.ENABLE_LOCATION_CHANGE_EXEC_MODE && url.startsWith(CORDOVA_EXEC_URL_PREFIX)) {
+    		handleExecUrl(url);
+    	}
+
+        // Give plugins the chance to handle the url
+    	else if ((this.appView.pluginManager != null) && this.appView.pluginManager.onOverrideUrlLoading(url)) {
+        }
+
+        // If dialing phone (tel:5551212)
+        else if (url.startsWith(AmazonWebView.SCHEME_TEL)) {
+            try {
+                Intent intent = new Intent(Intent.ACTION_DIAL);
+                intent.setData(Uri.parse(url));
+                this.cordova.getActivity().startActivity(intent);
+            } catch (android.content.ActivityNotFoundException e) {
+                LOG.e(TAG, "Error dialing " + url + ": " + e.toString());
+            }
+        }
+
+        // If displaying map (geo:0,0?q=address)
+        else if (url.startsWith("geo:")) {
+            try {
+                Intent intent = new Intent(Intent.ACTION_VIEW);
+                intent.setData(Uri.parse(url));
+                this.cordova.getActivity().startActivity(intent);
+            } catch (android.content.ActivityNotFoundException e) {
+                LOG.e(TAG, "Error showing map " + url + ": " + e.toString());
+            }
+        }
+
+        // If sending email (mailto:abc@corp.com)
+        else if (url.startsWith(AmazonWebView.SCHEME_MAILTO)) {
+            try {
+                Intent intent = new Intent(Intent.ACTION_VIEW);
+                intent.setData(Uri.parse(url));
+                this.cordova.getActivity().startActivity(intent);
+            } catch (android.content.ActivityNotFoundException e) {
+                LOG.e(TAG, "Error sending email " + url + ": " + e.toString());
+            }
+        }
+
+        // If sms:5551212?body=This is the message
+        else if (url.startsWith("sms:")) {
+            try {
+                Intent intent = new Intent(Intent.ACTION_VIEW);
+
+                // Get address
+                String address = null;
+                int parmIndex = url.indexOf('?');
+                if (parmIndex == -1) {
+                    address = url.substring(4);
+                }
+                else {
+                    address = url.substring(4, parmIndex);
+
+                    // If body, then set sms body
+                    Uri uri = Uri.parse(url);
+                    String query = uri.getQuery();
+                    if (query != null) {
+                        if (query.startsWith("body=")) {
+                            intent.putExtra("sms_body", query.substring(5));
+                        }
+                    }
+                }
+                intent.setData(Uri.parse("sms:" + address));
+                intent.putExtra("address", address);
+                intent.setType("vnd.android-dir/mms-sms");
+                this.cordova.getActivity().startActivity(intent);
+            } catch (android.content.ActivityNotFoundException e) {
+                LOG.e(TAG, "Error sending sms " + url + ":" + e.toString());
+            }
+        }
+        
+        //Android Market
+        else if(url.startsWith("market:")) {
+            try {
+                Intent intent = new Intent(Intent.ACTION_VIEW);
+                intent.setData(Uri.parse(url));
+                this.cordova.getActivity().startActivity(intent);
+            } catch (android.content.ActivityNotFoundException e) {
+                LOG.e(TAG, "Error loading Google Play Store: " + url, e);
+            }
+        }
+
+        // All else
+        else {
+
+            // If our app or file:, then load into a new Cordova webview container by starting a new instance of our activity.
+            // Our app continues to run.  When BACK is pressed, our app is redisplayed.
+            if (url.startsWith("file://") || url.startsWith("data:")  || Config.isUrlWhiteListed(url)) {
+                return false;
+            }
+
+            // If not our application, let default viewer handle
+            else {
+                try {
+                    Intent intent = new Intent(Intent.ACTION_VIEW);
+                    intent.setData(Uri.parse(url));
+                    this.cordova.getActivity().startActivity(intent);
+                } catch (android.content.ActivityNotFoundException e) {
+                    LOG.e(TAG, "Error loading url " + url, e);
+                }
+            }
+        }
+        return true;
+    }
+    
+    /**
+     * On received http auth request.
+     * The method reacts on all registered authentication tokens. There is one and only one authentication token for any host + realm combination
+     *
+     * @param view
+     * @param handler
+     * @param host
+     * @param realm
+     */
+    @Override
+    public void onReceivedHttpAuthRequest(AmazonWebView view, AmazonHttpAuthHandler handler, String host, String realm) {
+
+        // Get the authentication token
+        AuthenticationToken token = this.getAuthenticationToken(host, realm);
+        if (token != null) {
+            handler.proceed(token.getUserName(), token.getPassword());
+        }
+        else {
+            // Handle 401 like we'd normally do!
+            super.onReceivedHttpAuthRequest(view, handler, host, realm);
+        }
+    }
+
+    /**
+     * Notify the host application that a page has started loading.
+     * This method is called once for each main frame load so a page with iframes or framesets will call onPageStarted
+     * one time for the main frame. This also means that onPageStarted will not be called when the contents of an
+     * embedded frame changes, i.e. clicking a link whose target is an iframe.
+     *
+     * @param view          The webview initiating the callback.
+     * @param url           The url of the page.
+     */
+    @Override
+    public void onPageStarted(AmazonWebView view, String url, Bitmap favicon) {
+
+        // Flush stale messages.
+        this.appView.jsMessageQueue.reset();
+
+        // Broadcast message that page has loaded
+        this.appView.postMessage("onPageStarted", url);
+
+        // Notify all plugins of the navigation, so they can clean up if necessary.
+        if (this.appView.pluginManager != null) {
+            this.appView.pluginManager.onReset();
+        }
+    }
+
+    /**
+     * Notify the host application that a page has finished loading.
+     * This method is called only for main frame. When onPageFinished() is called, the rendering picture may not be updated yet.
+     *
+     *
+     * @param view          The webview initiating the callback.
+     * @param url           The url of the page.
+     */
+    @Override
+    public void onPageFinished(AmazonWebView view, String url) {
+        super.onPageFinished(view, url);
+        LOG.d(TAG, "onPageFinished(" + url + ")");
+
+        /**
+         * Because of a timing issue we need to clear this history in onPageFinished as well as
+         * onPageStarted. However we only want to do this if the doClearHistory boolean is set to
+         * true. You see when you load a url with a # in it which is common in jQuery applications
+         * onPageStared is not called. Clearing the history at that point would break jQuery apps.
+         */
+        if (this.doClearHistory) {
+            view.clearHistory();
+            this.doClearHistory = false;
+        }
+
+        // Clear timeout flag
+        this.appView.loadUrlTimeout++;
+
+        // Broadcast message that page has loaded
+        this.appView.postMessage("onPageFinished", url);
+
+        // Make app visible after 2 sec in case there was a JS error and Cordova JS never initialized correctly
+        if (this.appView.getVisibility() == View.INVISIBLE) {
+            Thread t = new Thread(new Runnable() {
+                public void run() {
+                    try {
+                        Thread.sleep(2000);
+                        cordova.getActivity().runOnUiThread(new Runnable() {
+                            public void run() {
+                                appView.postMessage("spinner", "stop");
+                            }
+                        });
+                    } catch (InterruptedException e) {
+                    }
+                }
+            });
+            t.start();
+        }
+
+        // Shutdown if blank loaded
+        if (url.equals("about:blank")) {
+            appView.postMessage("exit", null);
+        }
+    }
+
+    /**
+     * Report an error to the host application. These errors are unrecoverable (i.e. the main resource is unavailable).
+     * The errorCode parameter corresponds to one of the ERROR_* constants.
+     *
+     * @param view          The AmazonWebView that is initiating the callback.
+     * @param errorCode     The error code corresponding to an ERROR_* value.
+     * @param description   A String describing the error.
+     * @param failingUrl    The url that failed to load.
+     */
+    @Override
+    public void onReceivedError(AmazonWebView view, int errorCode, String description, String failingUrl) {
+        LOG.d(TAG, "CordovaWebViewClient.onReceivedError: Error code=%s Description=%s URL=%s", errorCode, description, failingUrl);
+
+        // Clear timeout flag
+        this.appView.loadUrlTimeout++;
+
+        // Handle error
+        JSONObject data = new JSONObject();
+        try {
+            data.put("errorCode", errorCode);
+            data.put("description", description);
+            data.put("url", failingUrl);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+        this.appView.postMessage("onReceivedError", data);
+    }
+
+    /**
+     * Notify the host application that an SSL error occurred while loading a resource.
+     * The host application must call either handler.cancel() or handler.proceed().
+     * Note that the decision may be retained for use in response to future SSL errors.
+     * The default behavior is to cancel the load.
+     *
+     * @param view          The AmazonWebView that is initiating the callback.
+     * @param handler       An AmazonSslErrorHandler object that will handle the user's response.
+     * @param error         The SSL error object.
+     */
+    @TargetApi(8)
+    @Override
+    public void onReceivedSslError(AmazonWebView view, AmazonSslErrorHandler handler, SslError error) {
+
+        final String packageName = this.cordova.getActivity().getPackageName();
+        final PackageManager pm = this.cordova.getActivity().getPackageManager();
+
+        ApplicationInfo appInfo;
+        try {
+            appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
+            if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
+                // debug = true
+                handler.proceed();
+                return;
+            } else {
+                // debug = false
+                super.onReceivedSslError(view, handler, error);
+            }
+        } catch (NameNotFoundException e) {
+            // When it doubt, lock it out!
+            super.onReceivedSslError(view, handler, error);
+        }
+    }
+
+
+    /**
+     * Sets the authentication token.
+     *
+     * @param authenticationToken
+     * @param host
+     * @param realm
+     */
+    public void setAuthenticationToken(AuthenticationToken authenticationToken, String host, String realm) {
+        if (host == null) {
+            host = "";
+        }
+        if (realm == null) {
+            realm = "";
+        }
+        this.authenticationTokens.put(host.concat(realm), authenticationToken);
+    }
+
+    /**
+     * Removes the authentication token.
+     *
+     * @param host
+     * @param realm
+     *
+     * @return the authentication token or null if did not exist
+     */
+    public AuthenticationToken removeAuthenticationToken(String host, String realm) {
+        return this.authenticationTokens.remove(host.concat(realm));
+    }
+
+    /**
+     * Gets the authentication token.
+     *
+     * In order it tries:
+     * 1- host + realm
+     * 2- host
+     * 3- realm
+     * 4- no host, no realm
+     *
+     * @param host
+     * @param realm
+     *
+     * @return the authentication token
+     */
+    public AuthenticationToken getAuthenticationToken(String host, String realm) {
+        AuthenticationToken token = null;
+        token = this.authenticationTokens.get(host.concat(realm));
+
+        if (token == null) {
+            // try with just the host
+            token = this.authenticationTokens.get(host);
+
+            // Try the realm
+            if (token == null) {
+                token = this.authenticationTokens.get(realm);
+            }
+
+            // if no host found, just query for default
+            if (token == null) {
+                token = this.authenticationTokens.get("");
+            }
+        }
+
+        return token;
+    }
+
+    /**
+     * Clear all authentication tokens.
+     */
+    public void clearAuthenticationTokens() {
+        this.authenticationTokens.clear();
+    }
+
+}
diff --git a/framework/src/org/apache/cordova/DirectoryManager.java b/framework/src/org/apache/cordova/DirectoryManager.java
new file mode 100644
index 0000000..a6cf3f3
--- /dev/null
+++ b/framework/src/org/apache/cordova/DirectoryManager.java
@@ -0,0 +1,162 @@
+/*
+       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 org.apache.cordova;
+
+import java.io.File;
+
+import android.content.Context;
+import android.os.Environment;
+import android.os.StatFs;
+
+/**
+ * This class provides file directory utilities.
+ * All file operations are performed on the SD card.
+ *
+ * It is used by the FileUtils class.
+ */
+@Deprecated // Deprecated in 3.1. To be removed in 4.0.
+public class DirectoryManager {
+
+    @SuppressWarnings("unused")
+    private static final String LOG_TAG = "DirectoryManager";
+
+    /**
+     * Determine if a file or directory exists.
+     * @param name				The name of the file to check.
+     * @return					T=exists, F=not found
+     */
+    public static boolean testFileExists(String name) {
+        boolean status;
+
+        // If SD card exists
+        if ((testSaveLocationExists()) && (!name.equals(""))) {
+            File path = Environment.getExternalStorageDirectory();
+            File newPath = constructFilePaths(path.toString(), name);
+            status = newPath.exists();
+        }
+        // If no SD card
+        else {
+            status = false;
+        }
+        return status;
+    }
+
+    /**
+     * Get the free disk space
+     * 
+     * @return 		Size in KB or -1 if not available
+     */
+    public static long getFreeDiskSpace(boolean checkInternal) {
+        String status = Environment.getExternalStorageState();
+        long freeSpace = 0;
+
+        // If SD card exists
+        if (status.equals(Environment.MEDIA_MOUNTED)) {
+            freeSpace = freeSpaceCalculation(Environment.getExternalStorageDirectory().getPath());
+        }
+        else if (checkInternal) {
+            freeSpace = freeSpaceCalculation("/");
+        }
+        // If no SD card and we haven't been asked to check the internal directory then return -1
+        else {
+            return -1;
+        }
+
+        return freeSpace;
+    }
+
+    /**
+     * Given a path return the number of free KB
+     * 
+     * @param path to the file system
+     * @return free space in KB
+     */
+    private static long freeSpaceCalculation(String path) {
+        StatFs stat = new StatFs(path);
+        long blockSize = stat.getBlockSize();
+        long availableBlocks = stat.getAvailableBlocks();
+        return availableBlocks * blockSize / 1024;
+    }
+
+    /**
+     * Determine if SD card exists.
+     * 
+     * @return				T=exists, F=not found
+     */
+    public static boolean testSaveLocationExists() {
+        String sDCardStatus = Environment.getExternalStorageState();
+        boolean status;
+
+        // If SD card is mounted
+        if (sDCardStatus.equals(Environment.MEDIA_MOUNTED)) {
+            status = true;
+        }
+
+        // If no SD card
+        else {
+            status = false;
+        }
+        return status;
+    }
+
+    /**
+     * Create a new file object from two file paths.
+     *
+     * @param file1			Base file path
+     * @param file2			Remaining file path
+     * @return				File object
+     */
+    private static File constructFilePaths (String file1, String file2) {
+        File newPath;
+        if (file2.startsWith(file1)) {
+            newPath = new File(file2);
+        }
+        else {
+            newPath = new File(file1 + "/" + file2);
+        }
+        return newPath;
+    }
+
+    /**
+     * Determine if we can use the SD Card to store the temporary file.  If not then use
+     * the internal cache directory.
+     *
+     * @return the absolute path of where to store the file
+     */
+    public static String getTempDirectoryPath(Context ctx) {
+        File cache = null;
+
+        // SD Card Mounted
+        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+            cache = new File(Environment.getExternalStorageDirectory().getAbsolutePath() +
+                    "/Android/data/" + ctx.getPackageName() + "/cache/");
+        }
+        // Use internal storage
+        else {
+            cache = ctx.getCacheDir();
+        }
+
+        // Create the cache directory if it doesn't exist
+        if (!cache.exists()) {
+            cache.mkdirs();
+        }
+
+        return cache.getAbsolutePath();
+    }
+}
diff --git a/framework/src/org/apache/cordova/DroidGap.java b/framework/src/org/apache/cordova/DroidGap.java
new file mode 100644
index 0000000..fbaf6c4
--- /dev/null
+++ b/framework/src/org/apache/cordova/DroidGap.java
@@ -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.
+*/
+
+// We moved everything to CordovaActivity
+
+package org.apache.cordova;
+
+public class DroidGap extends CordovaActivity {
+
+}
diff --git a/framework/src/org/apache/cordova/ExifHelper.java b/framework/src/org/apache/cordova/ExifHelper.java
new file mode 100644
index 0000000..5c42610
--- /dev/null
+++ b/framework/src/org/apache/cordova/ExifHelper.java
@@ -0,0 +1,186 @@
+/*
+       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 org.apache.cordova;
+
+import java.io.IOException;
+
+import android.media.ExifInterface;
+
+@Deprecated // Deprecated in 3.1. To be removed in 4.0.
+public class ExifHelper {
+    private String aperture = null;
+    private String datetime = null;
+    private String exposureTime = null;
+    private String flash = null;
+    private String focalLength = null;
+    private String gpsAltitude = null;
+    private String gpsAltitudeRef = null;
+    private String gpsDateStamp = null;
+    private String gpsLatitude = null;
+    private String gpsLatitudeRef = null;
+    private String gpsLongitude = null;
+    private String gpsLongitudeRef = null;
+    private String gpsProcessingMethod = null;
+    private String gpsTimestamp = null;
+    private String iso = null;
+    private String make = null;
+    private String model = null;
+    private String orientation = null;
+    private String whiteBalance = null;
+
+    private ExifInterface inFile = null;
+    private ExifInterface outFile = null;
+
+    /**
+     * The file before it is compressed
+     *
+     * @param filePath
+     * @throws IOException
+     */
+    public void createInFile(String filePath) throws IOException {
+        this.inFile = new ExifInterface(filePath);
+    }
+
+    /**
+     * The file after it has been compressed
+     *
+     * @param filePath
+     * @throws IOException
+     */
+    public void createOutFile(String filePath) throws IOException {
+        this.outFile = new ExifInterface(filePath);
+    }
+
+    /**
+     * Reads all the EXIF data from the input file.
+     */
+    public void readExifData() {
+        this.aperture = inFile.getAttribute(ExifInterface.TAG_APERTURE);
+        this.datetime = inFile.getAttribute(ExifInterface.TAG_DATETIME);
+        this.exposureTime = inFile.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
+        this.flash = inFile.getAttribute(ExifInterface.TAG_FLASH);
+        this.focalLength = inFile.getAttribute(ExifInterface.TAG_FOCAL_LENGTH);
+        this.gpsAltitude = inFile.getAttribute(ExifInterface.TAG_GPS_ALTITUDE);
+        this.gpsAltitudeRef = inFile.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF);
+        this.gpsDateStamp = inFile.getAttribute(ExifInterface.TAG_GPS_DATESTAMP);
+        this.gpsLatitude = inFile.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
+        this.gpsLatitudeRef = inFile.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
+        this.gpsLongitude = inFile.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
+        this.gpsLongitudeRef = inFile.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
+        this.gpsProcessingMethod = inFile.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD);
+        this.gpsTimestamp = inFile.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP);
+        this.iso = inFile.getAttribute(ExifInterface.TAG_ISO);
+        this.make = inFile.getAttribute(ExifInterface.TAG_MAKE);
+        this.model = inFile.getAttribute(ExifInterface.TAG_MODEL);
+        this.orientation = inFile.getAttribute(ExifInterface.TAG_ORIENTATION);
+        this.whiteBalance = inFile.getAttribute(ExifInterface.TAG_WHITE_BALANCE);
+    }
+
+    /**
+     * Writes the previously stored EXIF data to the output file.
+     *
+     * @throws IOException
+     */
+    public void writeExifData() throws IOException {
+        // Don't try to write to a null file
+        if (this.outFile == null) {
+            return;
+        }
+
+        if (this.aperture != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_APERTURE, this.aperture);
+        }
+        if (this.datetime != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_DATETIME, this.datetime);
+        }
+        if (this.exposureTime != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, this.exposureTime);
+        }
+        if (this.flash != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_FLASH, this.flash);
+        }
+        if (this.focalLength != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_FOCAL_LENGTH, this.focalLength);
+        }
+        if (this.gpsAltitude != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, this.gpsAltitude);
+        }
+        if (this.gpsAltitudeRef != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, this.gpsAltitudeRef);
+        }
+        if (this.gpsDateStamp != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, this.gpsDateStamp);
+        }
+        if (this.gpsLatitude != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_GPS_LATITUDE, this.gpsLatitude);
+        }
+        if (this.gpsLatitudeRef != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, this.gpsLatitudeRef);
+        }
+        if (this.gpsLongitude != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, this.gpsLongitude);
+        }
+        if (this.gpsLongitudeRef != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, this.gpsLongitudeRef);
+        }
+        if (this.gpsProcessingMethod != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, this.gpsProcessingMethod);
+        }
+        if (this.gpsTimestamp != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, this.gpsTimestamp);
+        }
+        if (this.iso != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_ISO, this.iso);
+        }
+        if (this.make != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_MAKE, this.make);
+        }
+        if (this.model != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_MODEL, this.model);
+        }
+        if (this.orientation != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_ORIENTATION, this.orientation);
+        }
+        if (this.whiteBalance != null) {
+            this.outFile.setAttribute(ExifInterface.TAG_WHITE_BALANCE, this.whiteBalance);
+        }
+
+        this.outFile.saveAttributes();
+    }
+
+    public int getOrientation() {
+        int o = Integer.parseInt(this.orientation);
+
+        if (o == ExifInterface.ORIENTATION_NORMAL) {
+            return 0;
+        } else if (o == ExifInterface.ORIENTATION_ROTATE_90) {
+            return 90;
+        } else if (o == ExifInterface.ORIENTATION_ROTATE_180) {
+            return 180;
+        } else if (o == ExifInterface.ORIENTATION_ROTATE_270) {
+            return 270;
+        } else {
+            return 0;
+        }
+    }
+
+    public void resetOrientation() {
+        this.orientation = "" + ExifInterface.ORIENTATION_NORMAL;
+    }
+}
diff --git a/framework/src/org/apache/cordova/ExposedJsApi.java b/framework/src/org/apache/cordova/ExposedJsApi.java
new file mode 100755
index 0000000..fde5722
--- /dev/null
+++ b/framework/src/org/apache/cordova/ExposedJsApi.java
@@ -0,0 +1,76 @@
+/*
+       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 org.apache.cordova;
+
+import android.webkit.JavascriptInterface;
+import org.apache.cordova.PluginManager;
+import org.json.JSONException;
+
+/**
+ * Contains APIs that the JS can call. All functions in here should also have
+ * an equivalent entry in CordovaChromeClient.java, and be added to
+ * cordova-js/lib/android/plugin/android/promptbasednativeapi.js
+ */
+/* package */ class ExposedJsApi {
+    
+    private PluginManager pluginManager;
+    private NativeToJsMessageQueue jsMessageQueue;
+    
+    public ExposedJsApi(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue) {
+        this.pluginManager = pluginManager;
+        this.jsMessageQueue = jsMessageQueue;
+    }
+
+    @JavascriptInterface
+    public String exec(String service, String action, String callbackId, String arguments) throws JSONException {
+        // If the arguments weren't received, send a message back to JS.  It will switch bridge modes and try again.  See CB-2666.
+        // We send a message meant specifically for this case.  It starts with "@" so no other message can be encoded into the same string.
+        if (arguments == null) {
+            return "@Null arguments.";
+        }
+
+        jsMessageQueue.setPaused(true);
+        try {
+            // Tell the resourceApi what thread the JS is running on.
+            CordovaResourceApi.jsThread = Thread.currentThread();
+            
+            pluginManager.exec(service, action, callbackId, arguments);
+            String ret = "";
+            if (!NativeToJsMessageQueue.DISABLE_EXEC_CHAINING) {
+                ret = jsMessageQueue.popAndEncode(false);
+            }
+            return ret;
+        } catch (Throwable e) {
+            e.printStackTrace();
+            return "";
+        } finally {
+            jsMessageQueue.setPaused(false);
+        }
+    }
+    
+    @JavascriptInterface
+    public void setNativeToJsBridgeMode(int value) {
+        jsMessageQueue.setBridgeMode(value);
+    }
+    
+    @JavascriptInterface
+    public String retrieveJsMessages(boolean fromOnlineEvent) {
+        return jsMessageQueue.popAndEncode(fromOnlineEvent);
+    }
+}
diff --git a/framework/src/org/apache/cordova/FileHelper.java b/framework/src/org/apache/cordova/FileHelper.java
new file mode 100644
index 0000000..eac5098
--- /dev/null
+++ b/framework/src/org/apache/cordova/FileHelper.java
@@ -0,0 +1,163 @@
+/*
+       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 org.apache.cordova;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.webkit.MimeTypeMap;
+
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.LOG;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.Locale;
+
+@Deprecated // Deprecated in 3.1. To be removed in 4.0.
+public class FileHelper {
+    private static final String LOG_TAG = "FileUtils";
+    private static final String _DATA = "_data";
+
+    /**
+     * Returns the real path of the given URI string.
+     * If the given URI string represents a content:// URI, the real path is retrieved from the media store.
+     *
+     * @param uriString the URI string of the audio/image/video
+     * @param cordova the current application context
+     * @return the full path to the file
+     */
+    @SuppressWarnings("deprecation")
+    public static String getRealPath(String uriString, CordovaInterface cordova) {
+        String realPath = null;
+
+        if (uriString.startsWith("content://")) {
+            String[] proj = { _DATA };
+            Cursor cursor = cordova.getActivity().managedQuery(Uri.parse(uriString), proj, null, null, null);
+            int column_index = cursor.getColumnIndexOrThrow(_DATA);
+            cursor.moveToFirst();
+            realPath = cursor.getString(column_index);
+            if (realPath == null) {
+                LOG.e(LOG_TAG, "Could get real path for URI string %s", uriString);
+            }
+        } else if (uriString.startsWith("file://")) {
+            realPath = uriString.substring(7);
+            if (realPath.startsWith("/android_asset/")) {
+                LOG.e(LOG_TAG, "Cannot get real path for URI string %s because it is a file:///android_asset/ URI.", uriString);
+                realPath = null;
+            }
+        } else {
+            realPath = uriString;
+        }
+
+        return realPath;
+    }
+
+    /**
+     * Returns the real path of the given URI.
+     * If the given URI is a content:// URI, the real path is retrieved from the media store.
+     *
+     * @param uri the URI of the audio/image/video
+     * @param cordova the current application context
+     * @return the full path to the file
+     */
+    public static String getRealPath(Uri uri, CordovaInterface cordova) {
+        return FileHelper.getRealPath(uri.toString(), cordova);
+    }
+
+    /**
+     * Returns an input stream based on given URI string.
+     *
+     * @param uriString the URI string from which to obtain the input stream
+     * @param cordova the current application context
+     * @return an input stream into the data at the given URI or null if given an invalid URI string
+     * @throws IOException
+     */
+    public static InputStream getInputStreamFromUriString(String uriString, CordovaInterface cordova) throws IOException {
+        if (uriString.startsWith("content")) {
+            Uri uri = Uri.parse(uriString);
+            return cordova.getActivity().getContentResolver().openInputStream(uri);
+        } else if (uriString.startsWith("file://")) {
+            int question = uriString.indexOf("?");
+            if (question > -1) {
+            	uriString = uriString.substring(0,question);
+            }
+            if (uriString.startsWith("file:///android_asset/")) {
+                Uri uri = Uri.parse(uriString);
+                String relativePath = uri.getPath().substring(15);
+                return cordova.getActivity().getAssets().open(relativePath);
+            } else {
+                return new FileInputStream(getRealPath(uriString, cordova));
+            }
+        } else {
+            return new FileInputStream(getRealPath(uriString, cordova));
+        }
+    }
+
+    /**
+     * Removes the "file://" prefix from the given URI string, if applicable.
+     * If the given URI string doesn't have a "file://" prefix, it is returned unchanged.
+     *
+     * @param uriString the URI string to operate on
+     * @return a path without the "file://" prefix
+     */
+    public static String stripFileProtocol(String uriString) {
+        if (uriString.startsWith("file://")) {
+            uriString = uriString.substring(7);
+        }
+        return uriString;
+    }
+
+    public static String getMimeTypeForExtension(String path) {
+        String extension = path;
+        int lastDot = extension.lastIndexOf('.');
+        if (lastDot != -1) {
+            extension = extension.substring(lastDot + 1);
+        }
+        // Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185).
+        extension = extension.toLowerCase(Locale.getDefault());
+        if (extension.equals("3ga")) {
+            return "audio/3gpp";
+        }
+        return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+    }
+    
+    /**
+     * Returns the mime type of the data specified by the given URI string.
+     *
+     * @param uriString the URI string of the data
+     * @return the mime type of the specified data
+     */
+    public static String getMimeType(String uriString, CordovaInterface cordova) {
+        String mimeType = null;
+
+        Uri uri = Uri.parse(uriString);
+        if (uriString.startsWith("content://")) {
+            mimeType = cordova.getActivity().getContentResolver().getType(uri);
+        } else {
+            mimeType = getMimeTypeForExtension(uri.getPath());
+        }
+
+        return mimeType;
+    }
+}
diff --git a/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java
new file mode 100644
index 0000000..aba4f17
--- /dev/null
+++ b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java
@@ -0,0 +1,96 @@
+/*
+       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 org.apache.cordova;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaResourceApi.OpenForReadResult;
+import org.apache.cordova.LOG;
+
+import android.annotation.TargetApi;
+import android.net.Uri;
+import android.os.Build;
+import com.amazon.android.webkit.AmazonWebResourceResponse;
+import com.amazon.android.webkit.AmazonWebView;
+
+@TargetApi(Build.VERSION_CODES.HONEYCOMB)
+public class IceCreamCordovaWebViewClient extends CordovaWebViewClient {
+
+    private static final String TAG = "IceCreamCordovaWebViewClient";
+
+    public IceCreamCordovaWebViewClient(CordovaInterface cordova) {
+        super(cordova);
+    }
+    
+    public IceCreamCordovaWebViewClient(CordovaInterface cordova, CordovaWebView view) {
+        super(cordova, view);
+    }
+
+    @Override
+    public AmazonWebResourceResponse shouldInterceptRequest(AmazonWebView view, String url) {
+        try {
+            // Check the against the white-list.
+            if ((url.startsWith("http:") || url.startsWith("https:")) && !Config.isUrlWhiteListed(url)) {
+                LOG.w(TAG, "URL blocked by whitelist: " + url);
+                // Results in a 404.
+                return new AmazonWebResourceResponse("text/plain", "UTF-8", null);
+            }
+
+            CordovaResourceApi resourceApi = appView.getResourceApi();
+            Uri origUri = Uri.parse(url);
+            // Allow plugins to intercept AmazonWebView requests.
+            Uri remappedUri = resourceApi.remapUri(origUri);
+            
+            if (!origUri.equals(remappedUri) || needsSpecialsInAssetUrlFix(origUri)) {
+                OpenForReadResult result = resourceApi.openForRead(remappedUri, true);
+                return new AmazonWebResourceResponse(result.mimeType, "UTF-8", result.inputStream);
+            }
+            // If we don't need to special-case the request, let the browser load it.
+            return null;
+        } catch (IOException e) {
+            if (!(e instanceof FileNotFoundException)) {
+                LOG.e("IceCreamCordovaWebViewClient", "Error occurred while loading a file (returning a 404).", e);
+            }
+            // Results in a 404.
+            return new AmazonWebResourceResponse("text/plain", "UTF-8", null);
+        }
+    }
+
+    private static boolean needsSpecialsInAssetUrlFix(Uri uri) {
+        if (CordovaResourceApi.getUriType(uri) != CordovaResourceApi.URI_TYPE_ASSET) {
+            return false;
+        }
+        if (uri.getQuery() != null || uri.getFragment() != null) {
+            return true;
+        }
+        
+        if (!uri.toString().contains("%")) {
+            return false;
+        }
+
+        switch(android.os.Build.VERSION.SDK_INT){
+            case android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH:
+            case android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1:
+                return true;
+        }
+        return false;
+    }
+}
diff --git a/framework/src/org/apache/cordova/JSONUtils.java b/framework/src/org/apache/cordova/JSONUtils.java
new file mode 100644
index 0000000..86038bb
--- /dev/null
+++ b/framework/src/org/apache/cordova/JSONUtils.java
@@ -0,0 +1,43 @@
+/*
+       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 org.apache.cordova;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+@Deprecated // Deprecated in 3.1. To be removed in 4.0.
+public class JSONUtils {
+	public static List<String> toStringList(JSONArray array) throws JSONException {
+        if(array == null) {
+            return null;
+        }
+        else {
+            List<String> list = new ArrayList<String>();
+
+            for (int i = 0; i < array.length(); i++) {
+                list.add(array.get(i).toString());
+            }
+
+            return list;
+        }
+    }
+}
diff --git a/framework/src/org/apache/cordova/LOG.java b/framework/src/org/apache/cordova/LOG.java
new file mode 100755
index 0000000..d5fdfdd
--- /dev/null
+++ b/framework/src/org/apache/cordova/LOG.java
@@ -0,0 +1,234 @@
+/*
+       Licensed to the Apache Software Foundation (ASF) under one
+       or more contributor license agreements.  See the NOTICE file
+       distributed with this work for additional information
+       regarding copyright ownership.  The ASF licenses this file
+       to you under the Apache License, Version 2.0 (the
+       "License"); you may not use this file except in compliance
+       with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing,
+       software distributed under the License is distributed on an
+       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+       KIND, either express or implied.  See the License for the
+       specific language governing permissions and limitations
+       under the License.
+*/
+package org.apache.cordova;
+
+import android.util.Log;
+
+/**
+ * Log to Android logging system.
+ *
+ * Log message can be a string or a printf formatted string with arguments.
+ * See http://developer.android.com/reference/java/util/Formatter.html
+ */
+public class LOG {
+
+    public static final int VERBOSE = Log.VERBOSE;
+    public static final int DEBUG = Log.DEBUG;
+    public static final int INFO = Log.INFO;
+    public static final int WARN = Log.WARN;
+    public static final int ERROR = Log.ERROR;
+
+    // Current log level
+    public static int LOGLEVEL = Log.ERROR;
+
+    /**
+     * Set the current log level.
+     *
+     * @param logLevel
+     */
+    public static void setLogLevel(int logLevel) {
+        LOGLEVEL = logLevel;
+        Log.i("CordovaLog", "Changing log level to " + logLevel);
+    }
+
+    /**
+     * Set the current log level.
+     *
+     * @param logLevel
+     */
+    public static void setLogLevel(String logLevel) {
+        if ("VERBOSE".equals(logLevel)) LOGLEVEL = VERBOSE;
+        else if ("DEBUG".equals(logLevel)) LOGLEVEL = DEBUG;
+        else if ("INFO".equals(logLevel)) LOGLEVEL = INFO;
+        else if ("WARN".equals(logLevel)) LOGLEVEL = WARN;
+        else if ("ERROR".equals(logLevel)) LOGLEVEL = ERROR;
+        Log.i("CordovaLog", "Changing log level to " + logLevel + "(" + LOGLEVEL + ")");
+    }
+
+    /**
+     * Determine if log level will be logged
+     *
+     * @param logLevel
+     * @return
+     */
+    public static boolean isLoggable(int logLevel) {
+        return (logLevel >= LOGLEVEL);
+    }
+
+    /**
+     * Verbose log message.
+     *
+     * @param tag
+     * @param s
+     */
+    public static void v(String tag, String s) {
+        if (LOG.VERBOSE >= LOGLEVEL) Log.v(tag, s);
+    }
+
+    /**
+     * Debug log message.
+     *
+     * @param tag
+     * @param s
+     */
+    public static void d(String tag, String s) {
+        if (LOG.DEBUG >= LOGLEVEL) Log.d(tag, s);
+    }
+
+    /**
+     * Info log message.
+     *
+     * @param tag
+     * @param s
+     */
+    public static void i(String tag, String s) {
+        if (LOG.INFO >= LOGLEVEL) Log.i(tag, s);
+    }
+
+    /**
+     * Warning log message.
+     *
+     * @param tag
+     * @param s
+     */
+    public static void w(String tag, String s) {
+        if (LOG.WARN >= LOGLEVEL) Log.w(tag, s);
+    }
+
+    /**
+     * Error log message.
+     *
+     * @param tag
+     * @param s
+     */
+    public static void e(String tag, String s) {
+        if (LOG.ERROR >= LOGLEVEL) Log.e(tag, s);
+    }
+
+    /**
+     * Verbose log message.
+     *
+     * @param tag
+     * @param s
+     * @param e
+     */
+    public static void v(String tag, String s, Throwable e) {
+        if (LOG.VERBOSE >= LOGLEVEL) Log.v(tag, s, e);
+    }
+
+    /**
+     * Debug log message.
+     *
+     * @param tag
+     * @param s
+     * @param e
+     */
+    public static void d(String tag, String s, Throwable e) {
+        if (LOG.DEBUG >= LOGLEVEL) Log.d(tag, s, e);
+    }
+
+    /**
+     * Info log message.
+     *
+     * @param tag
+     * @param s
+     * @param e
+     */
+    public static void i(String tag, String s, Throwable e) {
+        if (LOG.INFO >= LOGLEVEL) Log.i(tag, s, e);
+    }
+
+    /**
+     * Warning log message.
+     *
+     * @param tag
+     * @param s
+     * @param e
+     */
+    public static void w(String tag, String s, Throwable e) {
+        if (LOG.WARN >= LOGLEVEL) Log.w(tag, s, e);
+    }
+
+    /**
+     * Error log message.
+     *
+     * @param tag
+     * @param s
+     * @param e
+     */
+    public static void e(String tag, String s, Throwable e) {
+        if (LOG.ERROR >= LOGLEVEL) Log.e(tag, s, e);
+    }
+
+    /**
+     * Verbose log message with printf formatting.
+     *
+     * @param tag
+     * @param s
+     * @param args
+     */
+    public static void v(String tag, String s, Object... args) {
+        if (LOG.VERBOSE >= LOGLEVEL) Log.v(tag, String.format(s, args));
+    }
+
+    /**
+     * Debug log message with printf formatting.
+     *
+     * @param tag
+     * @param s
+     * @param args
+     */
+    public static void d(String tag, String s, Object... args) {
+        if (LOG.DEBUG >= LOGLEVEL) Log.d(tag, String.format(s, args));
+    }
+
+    /**
+     * Info log message with printf formatting.
+     *
+     * @param tag
+     * @param s
+     * @param args
+     */
+    public static void i(String tag, String s, Object... args) {
+        if (LOG.INFO >= LOGLEVEL) Log.i(tag, String.format(s, args));
+    }
+
+    /**
+     * Warning log message with printf formatting.
+     *
+     * @param tag
+     * @param s
+     * @param args
+     */
+    public static void w(String tag, String s, Object... args) {
+        if (LOG.WARN >= LOGLEVEL) Log.w(tag, String.format(s, args));
+    }
+
+    /**
+     * Error log message with printf formatting.
+     *
+     * @param tag
+     * @param s
+     * @param args
+     */
+    public static void e(String tag, String s, Object... args) {
+        if (LOG.ERROR >= LOGLEVEL) Log.e(tag, String.format(s, args));
+    }
+
+}
diff --git a/framework/src/org/apache/cordova/LinearLayoutSoftKeyboardDetect.java b/framework/src/org/apache/cordova/LinearLayoutSoftKeyboardDetect.java
new file mode 100755
index 0000000..6d8bb48
--- /dev/null
+++ b/framework/src/org/apache/cordova/LinearLayoutSoftKeyboardDetect.java
@@ -0,0 +1,168 @@
+/*
+       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 org.apache.cordova;
+
+import org.apache.cordova.LOG;
+
+import android.content.Context;
+//import android.view.View.MeasureSpec;
+import android.widget.LinearLayout;
+
+/**
+ * This class is used to detect when the soft keyboard is shown and hidden in the web view.
+ */
+public class LinearLayoutSoftKeyboardDetect extends LinearLayout {
+
+    private static final String TAG = "SoftKeyboardDetect";
+
+    // Kindle Fire devices have a Soft Key Bar with Home, Back and Search buttons that is displayed by default if the
+    // application is not set to full screen mode. As a result,
+    // In portrait mode - Contanier's Height = Screen Height - Status Bar Height - Soft Key Bar Height, Container's
+    // Width = Screen Width
+    // In landscape mode - Contanier's Height = Screen Height - Status Bar Height, Contanier's Width = Screen Width -
+    // Soft Key Bar Width
+    //
+    // Screen width and height for 3rd generation Kindle Fire devices are listed below
+    // Kindle Fire HDX 8.9" Screen Height = 2560 Screen Width = 1600
+    // [In portrait mode] SoftKeyBar Height = 122 (4.76% of Screen Height)
+    // [In landscape mode] SoftKeyBar Width = 122 (4.76% of Screen Width)
+    // Kindle Fire HDX 7" Screen Height = 1920 Screen Width = 1200
+    // [In portrait mode] SoftKeyBar Height = 117 (6.3% of Screen Height)
+    // [In landscape mode] SoftKeyBar Width = 117 (6.3% of Screen Width)
+    // Kindle Fire HD 7" Screen Height = 1280 Screen Width = 800
+    // [In portrait mode] SoftKeyBar Height = 78 (6.09% of Screen Height)
+    // [In landscape mode] SoftKeyBar Width = 78 (6.09% of Screen Width)
+    //
+    // Show/Hide keyboard events
+    // Should be fired when the height of the container changes by a value equal to the height of the keyboard.
+    // Our implementation - These events will be fired when the change in the height of the container is more than 7% of
+    // the screen height. This threshold value was chosen based on the height of the Soft Key Bar in the 3rd generation
+    // devices. This change has been added so that the show/hide keyboard events are not wrongly triggered when the soft
+    // key bar shows up.
+    //
+    // Orientation change
+    // [Landscape to Portrait] Screen Height should be equal to Container Width
+    // [Portrait to Landscape] Screen Height should be equal to Container Width + Soft Key Bar Width
+    // Our Implementation - Orientation change occurs when the difference between screen height and container width is
+    // less than 7% of the screen height. This threshold value was chosen based on the height of the Soft Key Bar in the
+    // 3rd generation devices.
+   
+    private static final int PERCENTAGE_CHANGE_THRESHOLD = 7; 
+    
+    private int oldHeight = 0;  // Need to save the old height as not to send redundant events
+    private int oldWidth = 0; // Need to save old width for orientation change
+    private int screenWidth = 0;
+    private int screenHeight = 0;
+    private CordovaActivity app = null;
+
+    public LinearLayoutSoftKeyboardDetect(Context context, int width, int height) {
+        super(context);
+        screenWidth = width;
+        screenHeight = height;
+        app = (CordovaActivity) context;
+    }
+
+    @Override
+    /**
+     * Start listening to new measurement events.  Fire events when the height
+     * gets smaller fire a show keyboard event and when height gets bigger fire
+     * a hide keyboard event.
+     *
+     * Note: We are using app.postMessage so that this is more compatible with the API
+     *
+     * @param widthMeasureSpec
+     * @param heightMeasureSpec
+     */
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        LOG.v(TAG, "We are in our onMeasure method");
+
+        // Get the current height of the visible part of the screen.
+        // This height will not included the status bar.\
+        int width, height;
+
+        height = MeasureSpec.getSize(heightMeasureSpec);
+        width = MeasureSpec.getSize(widthMeasureSpec);
+        LOG.v(TAG, "Old Height = %d", oldHeight);
+        LOG.v(TAG, "Height = %d", height);
+        LOG.v(TAG, "Old Width = %d", oldWidth);
+        LOG.v(TAG, "Width = %d", width);
+        
+        // If the oldHeight = 0 then this is the first measure event as the app starts up.
+        // If oldHeight == height then we got a measurement change that doesn't affect us.
+        if (oldHeight == 0 || oldHeight == height) {
+            LOG.d(TAG, "Ignore this event");
+        }
+        // Account for orientation change and ignore this event/Fire orientation change
+        else if (hasOrientationChanged(width)) {
+            int tmp_var = screenHeight;
+            screenHeight = screenWidth;
+            screenWidth = tmp_var;
+            LOG.v(TAG, "Orientation Change");
+            
+        }
+        // If the height has gotten bigger then we will assume the soft keyboard has
+        // gone away.
+        else if (height > oldHeight) {
+            if (app != null && hasKeyboardEventOccurred(height) ) {   
+                LOG.v(TAG, "Fired Hide Keyboard Event") ;
+                app.appView.sendJavascript("cordova.fireDocumentEvent('hidekeyboard');");
+            }
+        }
+        // If the height has gotten smaller then we will assume the soft keyboard has 
+        // been displayed.
+        else if (height < oldHeight) {
+            if (app != null && hasKeyboardEventOccurred(height)  ) {  
+                LOG.v(TAG, "Fired Show Keyboard Event") ;
+                app.appView.sendJavascript("cordova.fireDocumentEvent('showkeyboard');");
+            }
+        }
+
+        // Update the old height for the next event
+        oldHeight = height;
+        oldWidth = width;
+    }
+
+    /**
+     * Function that returns true if orientation has changed
+     * 
+     * @param width
+     *            The width of the container
+     * @return
+     */
+    private boolean hasOrientationChanged(int width) {
+        //Calculate difference between screen height and container as a percentage of screen height
+        double percentageChange = (double) (Math.abs(screenHeight - width) * 100) / screenHeight;
+        return percentageChange < PERCENTAGE_CHANGE_THRESHOLD;
+    }
+
+    /**
+     * Function that returns true if the keyboard show/hide event has occurred.
+     * 
+     * @param height
+     *            The height of the container
+     * @return
+     */
+    private boolean hasKeyboardEventOccurred(int height) {
+        //Calculate percentage change in container height as a percentage of screen height
+        double percentageChange = (double) (Math.abs(oldHeight - height) * 100) / screenHeight; 
+        return percentageChange > PERCENTAGE_CHANGE_THRESHOLD; 
+    }
+}
diff --git a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java
new file mode 100755
index 0000000..5d10c33
--- /dev/null
+++ b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java
@@ -0,0 +1,495 @@
+/*
+       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 org.apache.cordova;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.LinkedList;
+
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.PluginResult;
+
+import android.os.Message;
+import android.util.Log;
+import com.amazon.android.webkit.AmazonWebView;
+
+/**
+ * Holds the list of messages to be sent to the AmazonWebView.
+ */
+public class NativeToJsMessageQueue {
+    private static final String LOG_TAG = "JsMessageQueue";
+
+    // This must match the default value in incubator-cordova-js/lib/android/exec.js
+    private static final int DEFAULT_BRIDGE_MODE = 2;
+    
+    // Set this to true to force plugin results to be encoding as
+    // JS instead of the custom format (useful for benchmarking).
+    private static final boolean FORCE_ENCODE_USING_EVAL = false;
+
+    // Disable URL-based exec() bridge by default since it's a bit of a
+    // security concern.
+    static final boolean ENABLE_LOCATION_CHANGE_EXEC_MODE = false;
+        
+    // Disable sending back native->JS messages during an exec() when the active
+    // exec() is asynchronous. Set this to true when running bridge benchmarks.
+    static final boolean DISABLE_EXEC_CHAINING = false;
+    
+    // Arbitrarily chosen upper limit for how much data to send to JS in one shot.
+    // This currently only chops up on message boundaries. It may be useful
+    // to allow it to break up messages.
+    private static int MAX_PAYLOAD_SIZE = 50 * 1024 * 10240;
+    
+    /**
+     * The index into registeredListeners to treat as active. 
+     */
+    private int activeListenerIndex;
+    
+    /**
+     * When true, the active listener is not fired upon enqueue. When set to false,
+     * the active listener will be fired if the queue is non-empty. 
+     */
+    private boolean paused;
+    
+    /**
+     * The list of JavaScript statements to be sent to JavaScript.
+     */
+    private final LinkedList<JsMessage> queue = new LinkedList<JsMessage>();
+
+    /**
+     * The array of listeners that can be used to send messages to JS.
+     */
+    private final BridgeMode[] registeredListeners;    
+    
+    private final CordovaInterface cordova;
+    private final CordovaWebView webView;
+
+    public NativeToJsMessageQueue(CordovaWebView webView, CordovaInterface cordova) {
+        this.cordova = cordova;
+        this.webView = webView;
+        registeredListeners = new BridgeMode[4];
+        registeredListeners[0] = null;  // Polling. Requires no logic.
+        registeredListeners[1] = new LoadUrlBridgeMode();
+        registeredListeners[2] = new OnlineEventsBridgeMode();
+        registeredListeners[3] = new PrivateApiBridgeMode();
+        reset();
+    }
+    
+    /**
+     * Changes the bridge mode.
+     */
+    public void setBridgeMode(int value) {
+        if (value < 0 || value >= registeredListeners.length) {
+            Log.d(LOG_TAG, "Invalid NativeToJsBridgeMode: " + value);
+        } else {
+            if (value != activeListenerIndex) {
+                Log.d(LOG_TAG, "Set native->JS mode to " + value);
+                synchronized (this) {
+                    activeListenerIndex = value;
+                    BridgeMode activeListener = registeredListeners[value];
+                    if (!paused && !queue.isEmpty() && activeListener != null) {
+                        activeListener.onNativeToJsMessageAvailable();
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     * Clears all messages and resets to the default bridge mode.
+     */
+    public void reset() {
+        synchronized (this) {
+            queue.clear();
+            setBridgeMode(DEFAULT_BRIDGE_MODE);
+        }
+    }
+
+    private int calculatePackedMessageLength(JsMessage message) {
+        int messageLen = message.calculateEncodedLength();
+        String messageLenStr = String.valueOf(messageLen);
+        return messageLenStr.length() + messageLen + 1;        
+    }
+    
+    private void packMessage(JsMessage message, StringBuilder sb) {
+        int len = message.calculateEncodedLength();
+        sb.append(len)
+          .append(' ');
+        message.encodeAsMessage(sb);
+    }
+    
+    /**
+     * Combines and returns queued messages combined into a single string.
+     * Combines as many messages as possible, while staying under MAX_PAYLOAD_SIZE.
+     * Returns null if the queue is empty.
+     */
+    public String popAndEncode(boolean fromOnlineEvent) {
+        synchronized (this) {
+            registeredListeners[activeListenerIndex].notifyOfFlush(fromOnlineEvent);
+            if (queue.isEmpty()) {
+                return null;
+            }
+            int totalPayloadLen = 0;
+            int numMessagesToSend = 0;
+            for (JsMessage message : queue) {
+                int messageSize = calculatePackedMessageLength(message);
+                if (numMessagesToSend > 0 && totalPayloadLen + messageSize > MAX_PAYLOAD_SIZE && MAX_PAYLOAD_SIZE > 0) {
+                    break;
+                }
+                totalPayloadLen += messageSize;
+                numMessagesToSend += 1;
+            }
+
+            StringBuilder sb = new StringBuilder(totalPayloadLen);
+            for (int i = 0; i < numMessagesToSend; ++i) {
+                JsMessage message = queue.removeFirst();
+                packMessage(message, sb);
+            }
+            
+            if (!queue.isEmpty()) {
+                // Attach a char to indicate that there are more messages pending.
+                sb.append('*');
+            }
+            String ret = sb.toString();
+            return ret;
+        }
+    }
+    
+    /**
+     * Same as popAndEncode(), except encodes in a form that can be executed as JS.
+     */
+    private String popAndEncodeAsJs() {
+        synchronized (this) {
+            int length = queue.size();
+            if (length == 0) {
+                return null;
+            }
+            int totalPayloadLen = 0;
+            int numMessagesToSend = 0;
+            for (JsMessage message : queue) {
+                int messageSize = message.calculateEncodedLength() + 50; // overestimate.
+                if (numMessagesToSend > 0 && totalPayloadLen + messageSize > MAX_PAYLOAD_SIZE && MAX_PAYLOAD_SIZE > 0) {
+                    break;
+                }
+                totalPayloadLen += messageSize;
+                numMessagesToSend += 1;
+            }
+            boolean willSendAllMessages = numMessagesToSend == queue.size();
+            StringBuilder sb = new StringBuilder(totalPayloadLen + (willSendAllMessages ? 0 : 100));
+            // Wrap each statement in a try/finally so that if one throws it does 
+            // not affect the next.
+            for (int i = 0; i < numMessagesToSend; ++i) {
+                JsMessage message = queue.removeFirst();
+                if (willSendAllMessages && (i + 1 == numMessagesToSend)) {
+                    message.encodeAsJsMessage(sb);
+                } else {
+                    sb.append("try{");
+                    message.encodeAsJsMessage(sb);
+                    sb.append("}finally{");
+                }
+            }
+            if (!willSendAllMessages) {
+                sb.append("window.setTimeout(function(){cordova.require('cordova/plugin/android/polling').pollOnce();},0);");
+            }
+            for (int i = willSendAllMessages ? 1 : 0; i < numMessagesToSend; ++i) {
+                sb.append('}');
+            }
+            String ret = sb.toString();
+            return ret;
+        }
+    }   
+
+    /**
+     * Add a JavaScript statement to the list.
+     */
+    public void addJavaScript(String statement) {
+        enqueueMessage(new JsMessage(statement));
+    }
+
+    /**
+     * Add a JavaScript statement to the list.
+     */
+    public void addPluginResult(PluginResult result, String callbackId) {
+        if (callbackId == null) {
+            Log.e(LOG_TAG, "Got plugin result with no callbackId", new Throwable());
+            return;
+        }
+        // Don't send anything if there is no result and there is no need to
+        // clear the callbacks.
+        boolean noResult = result.getStatus() == PluginResult.Status.NO_RESULT.ordinal();
+        boolean keepCallback = result.getKeepCallback();
+        if (noResult && keepCallback) {
+            return;
+        }
+        JsMessage message = new JsMessage(result, callbackId);
+        if (FORCE_ENCODE_USING_EVAL) {
+            StringBuilder sb = new StringBuilder(message.calculateEncodedLength() + 50);
+            message.encodeAsJsMessage(sb);
+            message = new JsMessage(sb.toString());
+        }
+
+        enqueueMessage(message);
+    }
+    
+    private void enqueueMessage(JsMessage message) {
+        synchronized (this) {
+            queue.add(message);
+            if (!paused && registeredListeners[activeListenerIndex] != null) {
+                registeredListeners[activeListenerIndex].onNativeToJsMessageAvailable();
+            }
+        }        
+    }
+    
+    public void setPaused(boolean value) {
+        if (paused && value) {
+            // This should never happen. If a use-case for it comes up, we should
+            // change pause to be a counter.
+            Log.e(LOG_TAG, "nested call to setPaused detected.", new Throwable());
+        }
+        paused = value;
+        if (!value) {
+            synchronized (this) {
+                if (!queue.isEmpty() && registeredListeners[activeListenerIndex] != null) {
+                    registeredListeners[activeListenerIndex].onNativeToJsMessageAvailable();
+                }
+            }   
+        }
+    }
+    
+    public boolean getPaused() {
+        return paused;
+    }
+
+    private abstract class BridgeMode {
+        abstract void onNativeToJsMessageAvailable();
+        void notifyOfFlush(boolean fromOnlineEvent) {}
+    }
+    
+    /** Uses webView.loadUrl("javascript:") to execute messages. */
+    private class LoadUrlBridgeMode extends BridgeMode {
+        final Runnable runnable = new Runnable() {
+            public void run() {
+                String js = popAndEncodeAsJs();
+                if (js != null) {
+                    webView.loadUrlNow("javascript:" + js);
+                }
+            }
+        };
+        
+        @Override void onNativeToJsMessageAvailable() {
+            cordova.getActivity().runOnUiThread(runnable);
+        }
+    }
+
+    /** Uses online/offline events to tell the JS when to poll for messages. */
+    private class OnlineEventsBridgeMode extends BridgeMode {
+        boolean online = false;
+        final Runnable runnable = new Runnable() {
+            public void run() {
+                if (!queue.isEmpty()) {
+                    webView.setNetworkAvailable(online);
+                }
+            }                
+        };
+        OnlineEventsBridgeMode() {
+            webView.setNetworkAvailable(true);
+        }
+        @Override void onNativeToJsMessageAvailable() {
+            cordova.getActivity().runOnUiThread(runnable);
+        }
+        // Track when online/offline events are fired so that we don't fire excess events.
+        @Override void notifyOfFlush(boolean fromOnlineEvent) {
+            if (fromOnlineEvent) {
+                online = !online;
+            }
+        }
+    }
+    
+    /**
+     * Uses Java reflection to access an API that lets us eval JS.
+     * Requires Android 3.2.4 or above. 
+     */
+    private class PrivateApiBridgeMode extends BridgeMode {
+    	// Message added in commit:
+    	// http://omapzoom.org/?p=platform/frameworks/base.git;a=commitdiff;h=9497c5f8c4bc7c47789e5ccde01179abc31ffeb2
+    	// Which first appeared in 3.2.4ish.
+    	private static final int EXECUTE_JS = 194;
+    	
+    	Method sendMessageMethod;
+    	Object webViewCore;
+    	boolean initFailed;
+
+    	@SuppressWarnings("rawtypes")
+    	private void initReflection() {
+        	Object webViewObject = webView;
+    		Class webViewClass = AmazonWebView.class;
+        	try {
+    			Field f = webViewClass.getDeclaredField("mProvider");
+    			f.setAccessible(true);
+    			webViewObject = f.get(webView);
+    			webViewClass = webViewObject.getClass();
+        	} catch (Throwable e) {
+        		// mProvider is only required on newer Android releases.
+    		}
+        	
+        	try {
+    			Field f = webViewClass.getDeclaredField("mWebViewCore");
+                f.setAccessible(true);
+    			webViewCore = f.get(webViewObject);
+    			
+    			if (webViewCore != null) {
+    				sendMessageMethod = webViewCore.getClass().getDeclaredMethod("sendMessage", Message.class);
+	    			sendMessageMethod.setAccessible(true);	    			
+    			}
+    		} catch (Throwable e) {
+    			initFailed = true;
+				Log.e(LOG_TAG, "PrivateApiBridgeMode failed to find the expected APIs.", e);
+    		}
+    	}
+    	
+        @Override void onNativeToJsMessageAvailable() {
+        	if (sendMessageMethod == null && !initFailed) {
+        		initReflection();
+        	}
+        	// webViewCore is lazily initialized, and so may not be available right away.
+        	if (sendMessageMethod != null) {
+	        	String js = popAndEncodeAsJs();
+	        	Message execJsMessage = Message.obtain(null, EXECUTE_JS, js);
+				try {
+				    sendMessageMethod.invoke(webViewCore, execJsMessage);
+				} catch (Throwable e) {
+					Log.e(LOG_TAG, "Reflection message bridge failed.", e);
+				}
+        	}
+        }
+    }    
+    private static class JsMessage {
+        final String jsPayloadOrCallbackId;
+        final PluginResult pluginResult;
+        JsMessage(String js) {
+            if (js == null) {
+                throw new NullPointerException();
+            }
+            jsPayloadOrCallbackId = js;
+            pluginResult = null;
+        }
+        JsMessage(PluginResult pluginResult, String callbackId) {
+            if (callbackId == null || pluginResult == null) {
+                throw new NullPointerException();
+            }
+            jsPayloadOrCallbackId = callbackId;
+            this.pluginResult = pluginResult;
+        }
+        
+        int calculateEncodedLength() {
+            if (pluginResult == null) {
+                return jsPayloadOrCallbackId.length() + 1;
+            }
+            int statusLen = String.valueOf(pluginResult.getStatus()).length();
+            int ret = 2 + statusLen + 1 + jsPayloadOrCallbackId.length() + 1;
+            switch (pluginResult.getMessageType()) {
+                case PluginResult.MESSAGE_TYPE_BOOLEAN: // f or t
+                case PluginResult.MESSAGE_TYPE_NULL: // N
+                    ret += 1;
+                    break;
+                case PluginResult.MESSAGE_TYPE_NUMBER: // n
+                    ret += 1 + pluginResult.getMessage().length();
+                    break;
+                case PluginResult.MESSAGE_TYPE_STRING: // s
+                    ret += 1 + pluginResult.getStrMessage().length();
+                    break;
+                case PluginResult.MESSAGE_TYPE_BINARYSTRING:
+                    ret += 1 + pluginResult.getMessage().length();
+                    break;
+                case PluginResult.MESSAGE_TYPE_ARRAYBUFFER:
+                    ret += 1 + pluginResult.getMessage().length();
+                    break;
+                case PluginResult.MESSAGE_TYPE_JSON:
+                default:
+                    ret += pluginResult.getMessage().length();
+            }
+            return ret;
+        }
+        
+        void encodeAsMessage(StringBuilder sb) {
+            if (pluginResult == null) {
+                sb.append('J')
+                  .append(jsPayloadOrCallbackId);
+                return;
+            }
+            int status = pluginResult.getStatus();
+            boolean noResult = status == PluginResult.Status.NO_RESULT.ordinal();
+            boolean resultOk = status == PluginResult.Status.OK.ordinal();
+            boolean keepCallback = pluginResult.getKeepCallback();
+
+            sb.append((noResult || resultOk) ? 'S' : 'F')
+              .append(keepCallback ? '1' : '0')
+              .append(status)
+              .append(' ')
+              .append(jsPayloadOrCallbackId)
+              .append(' ');
+            switch (pluginResult.getMessageType()) {
+                case PluginResult.MESSAGE_TYPE_BOOLEAN:
+                    sb.append(pluginResult.getMessage().charAt(0)); // t or f.
+                    break;
+                case PluginResult.MESSAGE_TYPE_NULL: // N
+                    sb.append('N');
+                    break;
+                case PluginResult.MESSAGE_TYPE_NUMBER: // n
+                    sb.append('n')
+                      .append(pluginResult.getMessage());
+                    break;
+                case PluginResult.MESSAGE_TYPE_STRING: // s
+                    sb.append('s');
+                    sb.append(pluginResult.getStrMessage());
+                    break;
+                case PluginResult.MESSAGE_TYPE_BINARYSTRING: // S
+                    sb.append('S');
+                    sb.append(pluginResult.getMessage());
+                    break;                    
+                case PluginResult.MESSAGE_TYPE_ARRAYBUFFER: // A
+                    sb.append('A');
+                    sb.append(pluginResult.getMessage());
+                    break;
+                case PluginResult.MESSAGE_TYPE_JSON:
+                default:
+                    sb.append(pluginResult.getMessage()); // [ or {
+            }
+        }
+        
+        void encodeAsJsMessage(StringBuilder sb) {
+            if (pluginResult == null) {
+                sb.append(jsPayloadOrCallbackId);
+            } else {
+                int status = pluginResult.getStatus();
+                boolean success = (status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal());
+                sb.append("cordova.callbackFromNative('")
+                  .append(jsPayloadOrCallbackId)
+                  .append("',")
+                  .append(success)
+                  .append(",")
+                  .append(status)
+                  .append(",[")
+                  .append(pluginResult.getMessage())
+                  .append("],")
+                  .append(pluginResult.getKeepCallback())
+                  .append(");");
+            }
+        }
+    }
+}
diff --git a/framework/src/org/apache/cordova/PluginEntry.java b/framework/src/org/apache/cordova/PluginEntry.java
new file mode 100755
index 0000000..e668d89
--- /dev/null
+++ b/framework/src/org/apache/cordova/PluginEntry.java
@@ -0,0 +1,132 @@
+/*
+       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 org.apache.cordova;
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+
+//import android.content.Context;
+//import com.amazon.android.webkit.AmazonWebView;
+
+/**
+ * This class represents a service entry object.
+ */
+public class PluginEntry {
+
+    /**
+     * The name of the service that this plugin implements
+     */
+    public String service = "";
+
+    /**
+     * The plugin class name that implements the service.
+     */
+    public String pluginClass = "";
+
+    /**
+     * The plugin object.
+     * Plugin objects are only created when they are called from JavaScript.  (see PluginManager.exec)
+     * The exception is if the onload flag is set, then they are created when PluginManager is initialized.
+     */
+    public CordovaPlugin plugin = null;
+
+    /**
+     * Flag that indicates the plugin object should be created when PluginManager is initialized.
+     */
+    public boolean onload = false;
+
+    /**
+     * Constructor
+     *
+     * @param service               The name of the service
+     * @param pluginClass           The plugin class name
+     * @param onload                Create plugin object when HTML page is loaded
+     */
+    public PluginEntry(String service, String pluginClass, boolean onload) {
+        this.service = service;
+        this.pluginClass = pluginClass;
+        this.onload = onload;
+    }
+
+    /**
+     * Alternate constructor
+     *
+     * @param service               The name of the service
+     * @param plugin                The plugin associated with this entry
+     */
+    public PluginEntry(String service, CordovaPlugin plugin) {
+        this.service = service;
+        this.plugin = plugin;
+        this.pluginClass = plugin.getClass().getName();
+        this.onload = false;
+    }
+
+    /**
+     * Create plugin object.
+     * If plugin is already created, then just return it.
+     *
+     * @return                      The plugin object
+     */
+    public CordovaPlugin createPlugin(CordovaWebView webView, CordovaInterface ctx) {
+        if (this.plugin != null) {
+            return this.plugin;
+        }
+        try {
+            @SuppressWarnings("rawtypes")
+            Class c = getClassByName(this.pluginClass);
+            if (isCordovaPlugin(c)) {
+                this.plugin = (CordovaPlugin) c.newInstance();
+                this.plugin.initialize(ctx, webView);
+                return plugin;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.out.println("Error adding plugin " + this.pluginClass + ".");
+        }
+        return null;
+    }
+
+    /**
+     * Get the class.
+     *
+     * @param clazz
+     * @return
+     * @throws ClassNotFoundException
+     */
+    @SuppressWarnings("rawtypes")
+    private Class getClassByName(final String clazz) throws ClassNotFoundException {
+        Class c = null;
+        if (clazz != null) {
+            c = Class.forName(clazz);
+        }
+        return c;
+    }
+
+    /**
+     * Returns whether the given class extends CordovaPlugin.
+     */
+    @SuppressWarnings("rawtypes")
+    private boolean isCordovaPlugin(Class c) {
+        if (c != null) {
+            return org.apache.cordova.CordovaPlugin.class.isAssignableFrom(c);
+        }
+        return false;
+    }
+}
diff --git a/framework/src/org/apache/cordova/PluginManager.java b/framework/src/org/apache/cordova/PluginManager.java
new file mode 100755
index 0000000..33a6f22
--- /dev/null
+++ b/framework/src/org/apache/cordova/PluginManager.java
@@ -0,0 +1,436 @@
+/*
+       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 org.apache.cordova;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.cordova.CordovaArgs;
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.PluginEntry;
+import org.apache.cordova.PluginResult;
+import org.json.JSONException;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.Intent;
+import android.content.res.XmlResourceParser;
+
+import android.net.Uri;
+import android.os.Debug;
+import android.util.Log;
+
+/**
+ * PluginManager is exposed to JavaScript in the Cordova WebView.
+ *
+ * Calling native plugin code can be done by calling PluginManager.exec(...)
+ * from JavaScript.
+ */
+public class PluginManager {
+    private static String TAG = "PluginManager";
+    private static final int SLOW_EXEC_WARNING_THRESHOLD = Debug.isDebuggerConnected() ? 60 : 16;
+
+    // List of service entries
+    private final HashMap<String, PluginEntry> entries = new HashMap<String, PluginEntry>();
+
+    private final CordovaInterface ctx;
+    private final CordovaWebView app;
+
+    // Flag to track first time through
+    private boolean firstRun;
+
+    // Map URL schemes like foo: to plugins that want to handle those schemes
+    // This would allow how all URLs are handled to be offloaded to a plugin
+    protected HashMap<String, String> urlMap = new HashMap<String, String>();
+
+    private AtomicInteger numPendingUiExecs;
+
+    /**
+     * Constructor.
+     *
+     * @param app
+     * @param ctx
+     */
+    public PluginManager(CordovaWebView app, CordovaInterface ctx) {
+        this.ctx = ctx;
+        this.app = app;
+        this.firstRun = true;
+        this.numPendingUiExecs = new AtomicInteger(0);
+    }
+
+    /**
+     * Init when loading a new HTML page into webview.
+     */
+    public void init() {
+        LOG.d(TAG, "init()");
+
+        // If first time, then load plugins from config.xml file
+        if (this.firstRun) {
+            this.loadPlugins();
+            this.firstRun = false;
+        }
+
+        // Stop plugins on current HTML page and discard plugin objects
+        else {
+            this.onPause(false);
+            this.onDestroy();
+            this.clearPluginObjects();
+        }
+
+        // Insert PluginManager service
+        this.addService(new PluginEntry("PluginManager", new PluginManagerService()));
+
+        // Start up all plugins that have onload specified
+        this.startupPlugins();
+    }
+
+    /**
+     * Load plugins from res/xml/config.xml
+     */
+    public void loadPlugins() {
+        int id = this.ctx.getActivity().getResources().getIdentifier("config", "xml", this.ctx.getActivity().getPackageName());
+        if (id == 0) {
+            this.pluginConfigurationMissing();
+            //We have the error, we need to exit without crashing!
+            return;
+        }
+        XmlResourceParser xml = this.ctx.getActivity().getResources().getXml(id);
+        int eventType = -1;
+        String service = "", pluginClass = "", paramType = "";
+        boolean onload = false;
+        boolean insideFeature = false;
+        while (eventType != XmlResourceParser.END_DOCUMENT) {
+            if (eventType == XmlResourceParser.START_TAG) {
+                String strNode = xml.getName();
+                if (strNode.equals("url-filter")) {
+                    this.urlMap.put(xml.getAttributeValue(null, "value"), service);
+                }
+                else if (strNode.equals("feature")) {
+                    //Check for supported feature sets  aka. plugins (Accelerometer, Geolocation, etc)
+                    //Set the bit for reading params
+                    insideFeature = true;
+                    service = xml.getAttributeValue(null, "name");
+                }
+                else if (insideFeature && strNode.equals("param")) {
+                    paramType = xml.getAttributeValue(null, "name");
+                    if (paramType.equals("service")) // check if it is using the older service param
+                        service = xml.getAttributeValue(null, "value");
+                    else if (paramType.equals("package") || paramType.equals("android-package"))
+                        pluginClass = xml.getAttributeValue(null,"value");
+                    else if (paramType.equals("onload"))
+                        onload = "true".equals(xml.getAttributeValue(null, "value"));
+                }
+            }
+            else if (eventType == XmlResourceParser.END_TAG)
+            {
+                String strNode = xml.getName();
+                if (strNode.equals("feature") || strNode.equals("plugin"))
+                {
+                    PluginEntry entry = new PluginEntry(service, pluginClass, onload);
+                    this.addService(entry);
+
+                    //Empty the strings to prevent plugin loading bugs
+                    service = "";
+                    pluginClass = "";
+                    insideFeature = false;
+                }
+            }
+            try {
+                eventType = xml.next();
+            } catch (XmlPullParserException e) {
+                e.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Delete all plugin objects.
+     */
+    public void clearPluginObjects() {
+        for (PluginEntry entry : this.entries.values()) {
+            entry.plugin = null;
+        }
+    }
+
+    /**
+     * Create plugins objects that have onload set.
+     */
+    public void startupPlugins() {
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.onload) {
+                entry.createPlugin(this.app, this.ctx);
+            }
+        }
+    }
+
+    /**
+     * Receives a request for execution and fulfills it by finding the appropriate
+     * Java class and calling it's execute method.
+     *
+     * PluginManager.exec can be used either synchronously or async. In either case, a JSON encoded
+     * string is returned that will indicate if any errors have occurred when trying to find
+     * or execute the class denoted by the clazz argument.
+     *
+     * @param service       String containing the service to run
+     * @param action        String containing the action that the class is supposed to perform. This is
+     *                      passed to the plugin execute method and it is up to the plugin developer
+     *                      how to deal with it.
+     * @param callbackId    String containing the id of the callback that is execute in JavaScript if
+     *                      this is an async plugin call.
+     * @param rawArgs       An Array literal string containing any arguments needed in the
+     *                      plugin execute method.
+     */
+    public void exec(final String service, final String action, final String callbackId, final String rawArgs) {
+        if (numPendingUiExecs.get() > 0) {
+            numPendingUiExecs.getAndIncrement();
+            this.ctx.getActivity().runOnUiThread(new Runnable() {
+                public void run() {
+                    execHelper(service, action, callbackId, rawArgs);
+                    numPendingUiExecs.getAndDecrement();
+                }
+            });
+        } else {
+            execHelper(service, action, callbackId, rawArgs);
+        }
+    }
+
+    private void execHelper(final String service, final String action, final String callbackId, final String rawArgs) {
+        CordovaPlugin plugin = getPlugin(service);
+        if (plugin == null) {
+            Log.d(TAG, "exec() call to unknown plugin: " + service);
+            PluginResult cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);
+            app.sendPluginResult(cr, callbackId);
+            return;
+        }
+        try {
+            CallbackContext callbackContext = new CallbackContext(callbackId, app);
+            long pluginStartTime = System.currentTimeMillis();
+            boolean wasValidAction = plugin.execute(action, rawArgs, callbackContext);
+            long duration = System.currentTimeMillis() - pluginStartTime;
+            
+            if (duration > SLOW_EXEC_WARNING_THRESHOLD) {
+                Log.w(TAG, "THREAD WARNING: exec() call to " + service + "." + action + " blocked the main thread for " + duration + "ms. Plugin should use CordovaInterface.getThreadPool().");
+            }
+            if (!wasValidAction) {
+                PluginResult cr = new PluginResult(PluginResult.Status.INVALID_ACTION);
+                app.sendPluginResult(cr, callbackId);
+            }
+        } catch (JSONException e) {
+            PluginResult cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
+            app.sendPluginResult(cr, callbackId);
+        }
+    }
+
+    @Deprecated
+    public void exec(String service, String action, String callbackId, String jsonArgs, boolean async) {
+        exec(service, action, callbackId, jsonArgs);
+    }
+
+    /**
+     * Get the plugin object that implements the service.
+     * If the plugin object does not already exist, then create it.
+     * If the service doesn't exist, then return null.
+     *
+     * @param service       The name of the service.
+     * @return              CordovaPlugin or null
+     */
+    public CordovaPlugin getPlugin(String service) {
+        PluginEntry entry = this.entries.get(service);
+        if (entry == null) {
+            return null;
+        }
+        CordovaPlugin plugin = entry.plugin;
+        if (plugin == null) {
+            plugin = entry.createPlugin(this.app, this.ctx);
+        }
+        return plugin;
+    }
+
+    /**
+     * Add a plugin class that implements a service to the service entry table.
+     * This does not create the plugin object instance.
+     *
+     * @param service           The service name
+     * @param className         The plugin class name
+     */
+    public void addService(String service, String className) {
+        PluginEntry entry = new PluginEntry(service, className, false);
+        this.addService(entry);
+    }
+
+    /**
+     * Add a plugin class that implements a service to the service entry table.
+     * This does not create the plugin object instance.
+     *
+     * @param entry             The plugin entry
+     */
+    public void addService(PluginEntry entry) {
+        this.entries.put(entry.service, entry);
+    }
+
+    /**
+     * Called when the system is about to start resuming a previous activity.
+     *
+     * @param multitasking      Flag indicating if multitasking is turned on for app
+     */
+    public void onPause(boolean multitasking) {
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.plugin != null) {
+                entry.plugin.onPause(multitasking);
+            }
+        }
+    }
+
+    /**
+     * Called when the activity will start interacting with the user.
+     *
+     * @param multitasking      Flag indicating if multitasking is turned on for app
+     */
+    public void onResume(boolean multitasking) {
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.plugin != null) {
+                entry.plugin.onResume(multitasking);
+            }
+        }
+    }
+
+    /**
+     * The final call you receive before your activity is destroyed.
+     */
+    public void onDestroy() {
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.plugin != null) {
+                entry.plugin.onDestroy();
+            }
+        }
+    }
+
+    /**
+     * Send a message to all plugins.
+     *
+     * @param id                The message id
+     * @param data              The message data
+     * @return
+     */
+    public Object postMessage(String id, Object data) {
+        Object obj = this.ctx.onMessage(id, data);
+        if (obj != null) {
+            return obj;
+        }
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.plugin != null) {
+                obj = entry.plugin.onMessage(id, data);
+                if (obj != null) {
+                    return obj;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Called when the activity receives a new intent.
+     */
+    public void onNewIntent(Intent intent) {
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.plugin != null) {
+                entry.plugin.onNewIntent(intent);
+            }
+        }
+    }
+
+    /**
+     * Called when the URL of the webview changes.
+     *
+     * @param url               The URL that is being changed to.
+     * @return                  Return false to allow the URL to load, return true to prevent the URL from loading.
+     */
+    public boolean onOverrideUrlLoading(String url) {
+        Iterator<Entry<String, String>> it = this.urlMap.entrySet().iterator();
+        while (it.hasNext()) {
+            HashMap.Entry<String, String> pairs = it.next();
+            if (url.startsWith(pairs.getKey())) {
+                return this.getPlugin(pairs.getValue()).onOverrideUrlLoading(url);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Called when the app navigates or refreshes.
+     */
+    public void onReset() {
+        Iterator<PluginEntry> it = this.entries.values().iterator();
+        while (it.hasNext()) {
+            CordovaPlugin plugin = it.next().plugin;
+            if (plugin != null) {
+                plugin.onReset();
+            }
+        }
+    }
+
+
+    private void pluginConfigurationMissing() {
+        LOG.e(TAG, "=====================================================================================");
+        LOG.e(TAG, "ERROR: config.xml is missing.  Add res/xml/config.xml to your project.");
+        LOG.e(TAG, "https://git-wip-us.apache.org/repos/asf?p=incubator-cordova-android.git;a=blob;f=framework/res/xml/plugins.xml");
+        LOG.e(TAG, "=====================================================================================");
+    }
+
+    Uri remapUri(Uri uri) {
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.plugin != null) {
+                Uri ret = entry.plugin.remapUri(uri);
+                if (ret != null) {
+                    return ret;
+                }
+            }
+        }
+        return null;
+    }
+
+    private class PluginManagerService extends CordovaPlugin {
+        @Override
+        public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
+            if ("startup".equals(action)) {
+                // The onPageStarted event of CordovaWebViewClient resets the queue of messages to be returned to javascript in response
+                // to exec calls. Since this event occurs on the UI thread and exec calls happen on the WebCore thread it is possible
+                // that onPageStarted occurs after exec calls have started happening on a new page, which can cause the message queue
+                // to be reset between the queuing of a new message and its retrieval by javascript. To avoid this from happening,
+                // javascript always sends a "startup" exec upon loading a new page which causes all future exec calls to happen on the UI
+                // thread (and hence after onPageStarted) until there are no more pending exec calls remaining.
+                numPendingUiExecs.getAndIncrement();
+                ctx.getActivity().runOnUiThread(new Runnable() {
+                    public void run() {
+                        numPendingUiExecs.getAndDecrement();
+                    }
+                });
+                return true;
+            }
+            return false;
+        }
+    }
+}
diff --git a/framework/src/org/apache/cordova/PluginResult.java b/framework/src/org/apache/cordova/PluginResult.java
new file mode 100755
index 0000000..920cbc2
--- /dev/null
+++ b/framework/src/org/apache/cordova/PluginResult.java
@@ -0,0 +1,179 @@
+/*
+       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 org.apache.cordova;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import android.util.Base64;
+
+public class PluginResult {
+    private final int status;
+    private final int messageType;
+    private boolean keepCallback = false;
+    private String strMessage;
+    private String encodedMessage;
+
+    public PluginResult(Status status) {
+        this(status, PluginResult.StatusMessages[status.ordinal()]);
+    }
+
+    public PluginResult(Status status, String message) {
+        this.status = status.ordinal();
+        this.messageType = message == null ? MESSAGE_TYPE_NULL : MESSAGE_TYPE_STRING;
+        this.strMessage = message;
+    }
+
+    public PluginResult(Status status, JSONArray message) {
+        this.status = status.ordinal();
+        this.messageType = MESSAGE_TYPE_JSON;
+        encodedMessage = message.toString();
+    }
+
+    public PluginResult(Status status, JSONObject message) {
+        this.status = status.ordinal();
+        this.messageType = MESSAGE_TYPE_JSON;
+        encodedMessage = message.toString();
+    }
+
+    public PluginResult(Status status, int i) {
+        this.status = status.ordinal();
+        this.messageType = MESSAGE_TYPE_NUMBER;
+        this.encodedMessage = ""+i;
+    }
+
+    public PluginResult(Status status, float f) {
+        this.status = status.ordinal();
+        this.messageType = MESSAGE_TYPE_NUMBER;
+        this.encodedMessage = ""+f;
+    }
+
+    public PluginResult(Status status, boolean b) {
+        this.status = status.ordinal();
+        this.messageType = MESSAGE_TYPE_BOOLEAN;
+        this.encodedMessage = Boolean.toString(b);
+    }
+
+    public PluginResult(Status status, byte[] data) {
+        this(status, data, false);
+    }
+
+    public PluginResult(Status status, byte[] data, boolean binaryString) {
+        this.status = status.ordinal();
+        this.messageType = binaryString ? MESSAGE_TYPE_BINARYSTRING : MESSAGE_TYPE_ARRAYBUFFER;
+        this.encodedMessage = Base64.encodeToString(data, Base64.NO_WRAP);
+    }
+    
+    public void setKeepCallback(boolean b) {
+        this.keepCallback = b;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public int getMessageType() {
+        return messageType;
+    }
+
+    public String getMessage() {
+        if (encodedMessage == null) {
+            encodedMessage = JSONObject.quote(strMessage);
+        }
+        return encodedMessage;
+    }
+
+    /**
+     * If messageType == MESSAGE_TYPE_STRING, then returns the message string.
+     * Otherwise, returns null.
+     */
+    public String getStrMessage() {
+        return strMessage;
+    }
+
+    public boolean getKeepCallback() {
+        return this.keepCallback;
+    }
+
+    @Deprecated // Use sendPluginResult instead of sendJavascript.
+    public String getJSONString() {
+        return "{\"status\":" + this.status + ",\"message\":" + this.getMessage() + ",\"keepCallback\":" + this.keepCallback + "}";
+    }
+
+    @Deprecated // Use sendPluginResult instead of sendJavascript.
+    public String toCallbackString(String callbackId) {
+        // If no result to be sent and keeping callback, then no need to sent back to JavaScript
+        if ((status == PluginResult.Status.NO_RESULT.ordinal()) && keepCallback) {
+        	return null;
+        }
+
+        // Check the success (OK, NO_RESULT & !KEEP_CALLBACK)
+        if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {
+            return toSuccessCallbackString(callbackId);
+        }
+
+        return toErrorCallbackString(callbackId);
+    }
+
+    @Deprecated // Use sendPluginResult instead of sendJavascript.
+    public String toSuccessCallbackString(String callbackId) {
+        return "cordova.callbackSuccess('"+callbackId+"',"+this.getJSONString()+");";
+    }
+
+    @Deprecated // Use sendPluginResult instead of sendJavascript.
+    public String toErrorCallbackString(String callbackId) {
+        return "cordova.callbackError('"+callbackId+"', " + this.getJSONString()+ ");";
+    }
+
+    public static final int MESSAGE_TYPE_STRING = 1;
+    public static final int MESSAGE_TYPE_JSON = 2;
+    public static final int MESSAGE_TYPE_NUMBER = 3;
+    public static final int MESSAGE_TYPE_BOOLEAN = 4;
+    public static final int MESSAGE_TYPE_NULL = 5;
+    public static final int MESSAGE_TYPE_ARRAYBUFFER = 6;
+    // Use BINARYSTRING when your string may contain null characters.
+    // This is required to work around a bug in the platform :(.
+    public static final int MESSAGE_TYPE_BINARYSTRING = 7;
+
+    public static String[] StatusMessages = new String[] {
+        "No result",
+        "OK",
+        "Class not found",
+        "Illegal access",
+        "Instantiation error",
+        "Malformed url",
+        "IO error",
+        "Invalid action",
+        "JSON error",
+        "Error"
+    };
+
+    public enum Status {
+        NO_RESULT,
+        OK,
+        CLASS_NOT_FOUND_EXCEPTION,
+        ILLEGAL_ACCESS_EXCEPTION,
+        INSTANTIATION_EXCEPTION,
+        MALFORMED_URL_EXCEPTION,
+        IO_EXCEPTION,
+        INVALID_ACTION,
+        JSON_EXCEPTION,
+        ERROR
+    }
+}
diff --git a/framework/src/org/apache/cordova/Whitelist.java b/framework/src/org/apache/cordova/Whitelist.java
new file mode 100644
index 0000000..f23ac3d
--- /dev/null
+++ b/framework/src/org/apache/cordova/Whitelist.java
@@ -0,0 +1,170 @@
+/*
+       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 org.apache.cordova;
+
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.cordova.LOG;
+
+import android.net.Uri;
+
+public class Whitelist {
+    private static class URLPattern {
+        public Pattern scheme;
+        public Pattern host;
+        public Integer port;
+        public Pattern path;
+
+        private String regexFromPattern(String pattern, boolean allowWildcards) {
+            final String toReplace = "\\.[]{}()^$?+|";
+            StringBuilder regex = new StringBuilder();
+            for (int i=0; i < pattern.length(); i++) {
+                char c = pattern.charAt(i);
+                if (c == '*' && allowWildcards) {
+                    regex.append(".");
+                } else if (toReplace.indexOf(c) > -1) {
+                    regex.append('\\');
+                }
+                regex.append(c);
+            }
+            return regex.toString();
+        }
+
+        public URLPattern(String scheme, String host, String port, String path) throws MalformedURLException {
+            try {
+                if (scheme == null || "*".equals(scheme)) {
+                    this.scheme = null;
+                } else {
+                    this.scheme = Pattern.compile(regexFromPattern(scheme, false));
+                }
+                if ("*".equals(host)) {
+                    this.host = null;
+                } else if (host.startsWith("*.")) {
+                    this.host = Pattern.compile("([a-z0-9.-]*\\.)?" + regexFromPattern(host.substring(2), false));
+                } else {
+                    this.host = Pattern.compile(regexFromPattern(host, false));
+                }
+                if (port == null || "*".equals(port)) {
+                    this.port = null;
+                } else {
+                    this.port = Integer.parseInt(port,10);
+                }
+                if (path == null || "/*".equals(path)) {
+                    this.path = null;
+                } else {
+                    this.path = Pattern.compile(regexFromPattern(path, true));
+                }
+            } catch (NumberFormatException e) {
+                throw new MalformedURLException("Port must be a number");
+            }
+        }
+
+        public boolean matches(Uri uri) {
+            try {
+                return ((scheme == null || scheme.matcher(uri.getScheme()).matches()) &&
+                        (host == null || host.matcher(uri.getHost()).matches()) &&
+                        (port == null || port.equals(uri.getPort())) &&
+                        (path == null || path.matcher(uri.getPath()).matches()));
+            } catch (Exception e) {
+                LOG.d(TAG, e.toString());
+                return false;
+            }
+        }
+    }
+
+    private ArrayList<URLPattern> whiteList;
+
+    public static final String TAG = "Whitelist";
+
+    public Whitelist() {
+        this.whiteList = new ArrayList<URLPattern>();
+    }
+
+    /* Match patterns (from http://developer.chrome.com/extensions/match_patterns.html)
+     *
+     * <url-pattern> := <scheme>://<host><path>
+     * <scheme> := '*' | 'http' | 'https' | 'file' | 'ftp' | 'chrome-extension'
+     * <host> := '*' | '*.' <any char except '/' and '*'>+
+     * <path> := '/' <any chars>
+     *
+     * We extend this to explicitly allow a port attached to the host, and we allow
+     * the scheme to be omitted for backwards compatibility. (Also host is not required
+     * to begin with a "*" or "*.".)
+     */
+    public void addWhiteListEntry(String origin, boolean subdomains) {
+        if (whiteList != null) {
+            try {
+                // Unlimited access to network resources
+                if (origin.compareTo("*") == 0) {
+                    LOG.d(TAG, "Unlimited access to network resources");
+                    whiteList = null;
+                }
+                else { // specific access
+                    Pattern parts = Pattern.compile("^((\\*|[a-z-]+)://)?(\\*|((\\*\\.)?[^*/:]+))?(:(\\d+))?(/.*)?");
+                    Matcher m = parts.matcher(origin);
+                    if (m.matches()) {
+                        String scheme = m.group(2);
+                        String host = m.group(3);
+                        // Special case for two urls which are allowed to have empty hosts
+                        if (("file".equals(scheme) || "content".equals(scheme)) && host == null) host = "*";
+                        String port = m.group(7);
+                        String path = m.group(8);
+                        if (scheme == null) {
+                            // XXX making it stupid friendly for people who forget to include protocol/SSL
+                            whiteList.add(new URLPattern("http", host, port, path));
+                            whiteList.add(new URLPattern("https", host, port, path));
+                        } else {
+                            whiteList.add(new URLPattern(scheme, host, port, path));
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                LOG.d(TAG, "Failed to add origin %s", origin);
+            }
+        }
+    }
+
+
+    /**
+     * Determine if URL is in approved list of URLs to load.
+     *
+     * @param uri
+     * @return
+     */
+    public boolean isUrlWhiteListed(String uri) {
+        // If there is no whitelist, then it's wide open
+        if (whiteList == null) return true;
+
+        Uri parsedUri = Uri.parse(uri);
+        // Look for match in white list
+        Iterator<URLPattern> pit = whiteList.iterator();
+        while (pit.hasNext()) {
+            URLPattern p = pit.next();
+            if (p.matches(parsedUri)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/framework/test/org/apache/cordova/PreferenceNodeTest.java b/framework/test/org/apache/cordova/PreferenceNodeTest.java
new file mode 100644
index 0000000..0dea62a
--- /dev/null
+++ b/framework/test/org/apache/cordova/PreferenceNodeTest.java
@@ -0,0 +1,53 @@
+/*
+       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.
+*/
+import org.junit.*;
+import static org.junit.Assert.*;
+
+import org.apache.cordova.PreferenceNode;
+
+public class PreferenceNodeTest {
+    @Test
+        public void testConstructor() {
+            PreferenceNode foo = new org.apache.cordova.PreferenceNode("fullscreen", "false", false);
+            assertEquals("fullscreen", foo.name);
+            assertEquals("false", foo.value);
+            assertEquals(false, foo.readonly);
+        }
+
+    @Test
+        public void testNameAssignment() {
+            PreferenceNode foo = new org.apache.cordova.PreferenceNode("fullscreen", "false", false);
+            foo.name = "widescreen";
+            assertEquals("widescreen", foo.name);
+        }
+
+    @Test
+        public void testValueAssignment() {
+            PreferenceNode foo = new org.apache.cordova.PreferenceNode("fullscreen", "false", false);
+            foo.value = "maybe";
+            assertEquals("maybe", foo.value);
+        }
+
+    @Test
+        public void testReadonlyAssignment() {
+            PreferenceNode foo = new org.apache.cordova.PreferenceNode("fullscreen", "false", false);
+            foo.readonly = true;
+            assertEquals(true, foo.readonly);
+        }
+}
diff --git a/framework/test/org/apache/cordova/PreferenceSetTest.java b/framework/test/org/apache/cordova/PreferenceSetTest.java
new file mode 100644
index 0000000..ea915f9
--- /dev/null
+++ b/framework/test/org/apache/cordova/PreferenceSetTest.java
@@ -0,0 +1,73 @@
+/*
+       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.
+*/
+import org.junit.*;
+import static org.junit.Assert.*;
+
+import org.apache.cordova.PreferenceNode;
+import org.apache.cordova.PreferenceSet;
+
+public class PreferenceSetTest {
+    private PreferenceSet preferences;
+    private PreferenceNode screen;
+
+    @Before
+        public void setUp() {
+            preferences = new PreferenceSet();
+            screen = new PreferenceNode("fullscreen", "true", false);
+        }
+
+    @Test
+        public void testAddition() {
+            preferences.add(screen);
+            assertEquals(1, preferences.size());
+        }
+
+    @Test
+        public void testClear() {
+            preferences.add(screen);
+            preferences.clear();
+            assertEquals(0, preferences.size());
+        }
+
+    @Test
+        public void testPreferenceRetrieval() {
+            preferences.add(screen);
+            assertEquals("true", preferences.pref("fullscreen"));
+        }
+
+    @Test
+        public void testNoPreferenceRetrieval() {
+            // return null if the preference is not defined
+            assertEquals(null, preferences.pref("antigravity"));
+        }
+
+    @Test
+        public void testUnsetPreferenceChecking() {
+            PreferenceSet emptySet = new PreferenceSet();
+            boolean value = emptySet.prefMatches("fullscreen", "true");
+            assertEquals(false, value);
+        }
+
+    @Test
+        public void testSetPreferenceChecking() {
+            preferences.add(screen);
+            boolean value = preferences.prefMatches("fullscreen", "true");
+            assertEquals(true, value);
+        }
+}
diff --git a/nothing.txt b/nothing.txt
deleted file mode 100644
index e69de29..0000000
--- a/nothing.txt
+++ /dev/null
diff --git a/test/.classpath b/test/.classpath
new file mode 100644
index 0000000..a4763d1
--- /dev/null
+++ b/test/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="src" path="gen"/>
+	<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+	<classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+	<classpathentry kind="output" path="bin/classes"/>
+</classpath>
diff --git a/test/.project b/test/.project
new file mode 100644
index 0000000..7bacb6f
--- /dev/null
+++ b/test/.project
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>CordovaViewTestActivity</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/test/AndroidManifest.xml b/test/AndroidManifest.xml
new file mode 100755
index 0000000..ee778ae
--- /dev/null
+++ b/test/AndroidManifest.xml
@@ -0,0 +1,303 @@
+<?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" 
+    xmlns:amazon="http://schemas.amazon.com/apk/res/android" android:windowSoftInputMode="adjustPan"
+      package="org.apache.cordova.test" android:versionName="1.0" android:versionCode="1">
+    <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.VIBRATE" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <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.WRITE_EXTERNAL_STORAGE" />   
+    <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-sdk android:minSdkVersion="8" />
+
+    <instrumentation
+        android:name="android.test.InstrumentationTestRunner"
+        android:targetPackage="org.apache.cordova.test" />
+
+    <application
+        android:icon="@drawable/icon"
+        android:label="@string/app_name" >
+        <uses-library android:name="android.test.runner" />
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.CordovaWebViewTestActivity" >
+            <intent-filter >
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.background" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.backgroundcolor" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.basicauth" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.CordovaActivity" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.CordovaDriverAction" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.errorurl" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.fullscreen" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.htmlnotfound" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.iframe" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.lifecycle" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.loading" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.menus" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.splashscreen" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.tests" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.timeout" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.userwebview" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.whitelist" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.xhr" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:windowSoftInputMode="adjustPan"
+            android:label="@string/app_name" 
+            android:configChanges="orientation|keyboardHidden"
+            android:name=".actions.backbuttonmultipage" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+                <!-- This is required for the Tests to work with AmazonWebView -->
+        <amazon:enable-feature 
+            android:name="com.amazon.webview" 
+            amazon:required="false" />
+
+        <!--
+             The following service entries exist to allow Chromium to start sandboxed
+             renderer processes. These service entries are necessary because there is no way
+             to declare a service dynamically, and our implementation of Chromium uses the
+             Android service API to communicate with the renderer processes. The number of
+             service entries, and their process names, must match the constants which have
+             been compiled into Chromium.
+        -->
+        <service
+            android:name="org.chromium.content.app.SandboxedProcessService0"
+            android:exported="false"
+            android:permission="org.chromium.content_shell.permission.SANDBOX"
+            android:process=":sandboxed_process0" />
+        <service
+            android:name="org.chromium.content.app.SandboxedProcessService1"
+            android:exported="false"
+            android:permission="org.chromium.content_shell.permission.SANDBOX"
+            android:process=":sandboxed_process1" />
+        <service
+            android:name="org.chromium.content.app.SandboxedProcessService2"
+            android:exported="false"
+            android:permission="org.chromium.content_shell.permission.SANDBOX"
+            android:process=":sandboxed_process2" />
+        <service
+            android:name="org.chromium.content.app.SandboxedProcessService3"
+            android:exported="false"
+            android:permission="org.chromium.content_shell.permission.SANDBOX"
+            android:process=":sandboxed_process3" />
+        <service
+            android:name="org.chromium.content.app.SandboxedProcessService4"
+            android:exported="false"
+            android:permission="org.chromium.content_shell.permission.SANDBOX"
+            android:process=":sandboxed_process4" />
+        <service
+            android:name="org.chromium.content.app.SandboxedProcessService5"
+            android:exported="false"
+            android:permission="org.chromium.content_shell.permission.SANDBOX"
+            android:process=":sandboxed_process5" />
+        </application>
+</manifest> 
diff --git a/test/README.md b/test/README.md
new file mode 100755
index 0000000..5d86720
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,50 @@
+<!--
+#
+# 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.
+#
+-->
+# Android Native Tests #
+
+These tests are designed to verify Android native features and other Android specific features.
+
+## Initial Setup ##
+
+Before running the tests, they need to be set up.
+
+0. Copy cordova-x.y.z.jar into libs directory
+
+To run from command line:
+
+0. Build by entering `ant debug install`
+0. Run tests by clicking on "CordovaTest" icon on device
+
+To run from Eclipse:
+
+0. Import Android project into Eclipse
+0. Ensure Project properties "Java Build Path" includes the lib/cordova-x.y.z.jar
+0. Create run configuration if not already created
+0. Run tests 
+
+## Automatic Runs ##
+
+Once you have installed the test, you can launch and run the tests
+automatically with the below command:
+
+    adb shell am instrument -w org.apache.cordova.test/android.test.InstrumentationTestRunner
+
+(Optionally, you can also run in Eclipse)
diff --git a/test/ant.properties b/test/ant.properties
new file mode 100755
index 0000000..ee52d86
--- /dev/null
+++ b/test/ant.properties
@@ -0,0 +1,17 @@
+# 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/test/assets/www/backbuttonmultipage/index.html b/test/assets/www/backbuttonmultipage/index.html
new file mode 100755
index 0000000..afab731
--- /dev/null
+++ b/test/assets/www/backbuttonmultipage/index.html
@@ -0,0 +1,41 @@
+<!--
+         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 http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
+<title>Backbutton</title> 
+<link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+<script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+<script type="text/javascript" charset="utf-8" src="../main.js"></script>
+
+<body onload="init();" id="stage" class="theme">
+    <h1>Cordova Android Tests</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;</span>, Color Depth: <span id="colorDepth"></span></h4>
+    </div>
+    <div id="info">
+        <h4>Page 1</h4>
+        Go to next page.<br>
+        If returning from previous page, press "backbutton".  You should exit this app.
+    </div>
+    <a href="sample2.html" class="btn large">Next page</a>
+</body> 
+</html>
diff --git a/test/assets/www/backbuttonmultipage/sample2.html b/test/assets/www/backbuttonmultipage/sample2.html
new file mode 100755
index 0000000..397ac70
--- /dev/null
+++ b/test/assets/www/backbuttonmultipage/sample2.html
@@ -0,0 +1,41 @@
+<!--
+         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 http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
+<title>Backbutton</title> 
+<link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+<script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+<script type="text/javascript" charset="utf-8" src="../main.js"></script>
+
+<body onload="init();" id="stage" class="theme">
+    <h1>Cordova Android Tests</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;</span>, Color Depth: <span id="colorDepth"></span></h4>
+    </div>
+    <div id="info">
+        <h4>Page 2</h4>
+        Go to next page.<br>
+        If returning from previous page, press "backbutton".  You should go to Page 1.
+    </div>
+    <a href="sample3.html" class="btn large">Next page</a>
+</body> 
+</html>
diff --git a/test/assets/www/backbuttonmultipage/sample3.html b/test/assets/www/backbuttonmultipage/sample3.html
new file mode 100755
index 0000000..f4b1f8a
--- /dev/null
+++ b/test/assets/www/backbuttonmultipage/sample3.html
@@ -0,0 +1,43 @@
+<!--
+         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 http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
+<title>Backbutton</title> 
+<link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+<script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+<script type="text/javascript" charset="utf-8" src="../main.js"></script>
+
+<body onload="init();" id="stage" class="theme">
+    <h1>Cordova Android Tests</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;</span>, Color Depth: <span id="colorDepth"></span></h4>
+    </div>
+    <div id="info">
+        <h4>Page 3</h4>
+        Press the 3 buttons below.  You should stay on same page.<br>
+        Press "backbutton" 4 times.  This will go back to #test3, #test2, #test1, then return to previous Page 2.<br>
+    </div>
+    <a href="sample3.html#test1" class="btn large">page3#test1</a>
+    <a href="sample3.html#test2" class="btn large">page3#test2</a>
+    <a href="sample3.html#test3" class="btn large">page3#test3</a>
+</body> 
+</html>
diff --git a/test/assets/www/background/index.html b/test/assets/www/background/index.html
new file mode 100755
index 0000000..2a073a6
--- /dev/null
+++ b/test/assets/www/background/index.html
@@ -0,0 +1,117 @@
+<!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>
+<head>
+<meta http-equiv="Content-type" content="text/html; charset=utf-8">
+<title>Background Page 1</title>
+<link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+<script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+
+<script type="text/javascript" charset="utf-8">
+
+	function onLoad() {
+		console.log("Page1: onload");
+		log("Page1: onload @ " + new Date().toLocaleTimeString());
+		document.addEventListener("deviceready", onDeviceReady, false);
+	}
+
+	function onUnLoaded() {
+		console.log("Page1: onunload");
+		log("Page1: onunload @ " + new Date().toLocaleTimeString());
+	}
+
+	function onDeviceReady() {
+		// Register the event listener
+		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;
+
+		document.addEventListener("pause", onPause, false);
+		document.addEventListener("resume", onResume, false);
+		
+		window.setInterval(function() {
+			log("Page1: Running");
+		}, 2000);
+	}
+
+	function onPause() {
+		console.log("Page1: onpause");
+		log("Page1: onpause @ " + new Date().toLocaleTimeString());
+	}
+
+	function onResume() {
+		console.log("Page1: onresume");
+		log("Page1: onresume @ " + new Date().toLocaleTimeString());
+	}
+
+	function log(s) {
+		var el = document.getElementById('status');
+		var status = el.innerHTML + s + "<br>";
+		el.innerHTML = status;
+		localStorage.backgroundstatus = status;
+	}
+	
+	function clearStatus() {
+		console.log("clear()");
+		localStorage.backgroundstatus = "";
+		document.getElementById('status').innerHTML = "";
+	}
+		
+</script>
+</head>
+<body onload="onLoad()" onunload="onUnLoaded()"  id="stage" class="theme">
+	<h1>Events</h1>
+	<div id="info">
+		<h4>
+			Platform: <span id="platform"> &nbsp;</span>, Version: <span
+				id="version">&nbsp;</span>
+		</h4>
+		<h4>
+			UUID: <span id="uuid"> &nbsp;</span>, Name: <span id="name">&nbsp;</span>
+		</h4>
+		<h4>
+			Width: <span id="width"> &nbsp;</span>, Height: <span id="height">&nbsp;
+			</span>, Color Depth: <span id="colorDepth"></span>
+		</h4>
+	</div>
+	<div id="info">
+	   Press "Home" button, then return to this app to see pause/resume.<br>
+	   There shouldn't be any "Running" entries between pause and resume.<br>
+	</div>
+	<div id="info">
+	   <h4>Info for event testing:</h4>
+	   <div id="status"></div>
+	</div>
+    
+    <!--  a href="index2.html" class="btn large" >Load new page</a -->
+    <a href="javascript:" class="btn large" onclick="clearStatus();">Clear status</a>
+    
+    <script>
+    document.getElementById('status').innerHTML = localStorage.backgroundstatus;
+    </script>
+</body>
+</html>
+
+
diff --git a/test/assets/www/background/index2.html b/test/assets/www/background/index2.html
new file mode 100755
index 0000000..addf6eb
--- /dev/null
+++ b/test/assets/www/background/index2.html
@@ -0,0 +1,116 @@
+<!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>
+<head>
+<meta http-equiv="Content-type" content="text/html; charset=utf-8">
+<title>Background Page 2</title>
+<link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+<script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+
+<script type="text/javascript" charset="utf-8">
+
+    function onLoad() {
+        console.log("Page2: onload");
+        log("Page2: onload @ " + new Date().toLocaleTimeString());
+        document.addEventListener("deviceready", onDeviceReady, false);
+    }
+
+    function onUnLoaded() {
+        console.log("Page2: onunload");
+        log("Page2: onunload @ " + new Date().toLocaleTimeString());
+    }
+
+    function onDeviceReady() {
+        // Register the event listener
+        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;
+
+        document.addEventListener("pause", onPause, false);
+        document.addEventListener("resume", onResume, false);
+
+        window.setInterval(function() {
+            log("Page2: Running");
+        }, 2000);
+}
+
+    function onPause() {
+        console.log("Page2: onpause");
+        log("Page2: onpause @ " + new Date().toLocaleTimeString());
+    }
+
+    function onResume() {
+        console.log("Page2: onresume");
+        log("Page2: onresume @ " + new Date().toLocaleTimeString());
+    }
+
+    function log(s) {
+        var el = document.getElementById('status');
+        var status = el.innerHTML + s + "<br>";
+        el.innerHTML = status;
+        localStorage.backgroundstatus = status;
+    }
+    
+    function clearStatus() {
+        console.log("clear()");
+        localStorage.backgroundstatus = "";
+        document.getElementById('status').innerHTML = "";
+    }
+        
+</script>
+</head>
+<body onload="onLoad()" onunload="onUnLoaded()"  id="stage" class="theme">
+    <h1>Events</h1>
+    <div id="info">
+        <h4>
+            Platform: <span id="platform"> &nbsp;</span>, Version: <span
+                id="version">&nbsp;</span>
+        </h4>
+        <h4>
+            UUID: <span id="uuid"> &nbsp;</span>, Name: <span id="name">&nbsp;</span>
+        </h4>
+        <h4>
+            Width: <span id="width"> &nbsp;</span>, Height: <span id="height">&nbsp;
+            </span>, Color Depth: <span id="colorDepth"></span>
+        </h4>
+    </div>
+    <div id="info">
+       <h4>Press "Back" button to return to Page 1.</h4>
+    </div>
+    <div id="info">
+       <h4>Info for event testing:</h4>
+       <div id="status"></div>
+    </div>
+    
+    <a href="index.html" class="btn large" >Load new page</a>
+    <a href="javascript:" class="btn large" onclick="clearStatus();">Clear status</a>
+    
+    <script>
+    document.getElementById('status').innerHTML = localStorage.backgroundstatus;
+    </script>
+</body>
+</html>
+
+
diff --git a/test/assets/www/backgroundcolor/index.html b/test/assets/www/backgroundcolor/index.html
new file mode 100755
index 0000000..0746dcf
--- /dev/null
+++ b/test/assets/www/backgroundcolor/index.html
@@ -0,0 +1,41 @@
+<!--
+         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.
+-->
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta name="viewport" content="width=320; user-scalable=no" />
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>Cordova Tests</title>
+      <link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+      <script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+      <script type="text/javascript" charset="utf-8" src="../main.js"></script>
+  </head>
+  <body onload="init();" id="stage" class="theme">
+    <h1>Background Color Test</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;
+                   </span>, Color Depth: <span id="colorDepth"></span></h4>
+     </div>
+     <div id="info">
+     Before this page was show, you should have seen the background flash green.</br>
+     </div>
+  </body>
+</html>
diff --git a/test/assets/www/basicauth/index.html b/test/assets/www/basicauth/index.html
new file mode 100755
index 0000000..02ff0b2
--- /dev/null
+++ b/test/assets/www/basicauth/index.html
@@ -0,0 +1,42 @@
+<!--
+         Licensed to the Apache Software Foundation (ASF) under one
+         or more contributor license agreements.  See the NOTICE file
+         distributed with this work for additional information
+         regarding copyright ownership.  The ASF licenses this file
+         to you under the Apache License, Version 2.0 (the
+         "License"); you may not use this file except in compliance
+         with the License.  You may obtain a copy of the License at
+
+           http://www.apache.org/licenses/LICENSE-2.0
+
+         Unless required by applicable law or agreed to in writing,
+         software distributed under the License is distributed on an
+         "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+         KIND, either express or implied.  See the License for the
+         specific language governing permissions and limitations
+         under the License.
+-->
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta name="viewport" content="width=320; user-scalable=no" />
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>Cordova Tests</title>
+      <link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+      <script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+      <script type="text/javascript" charset="utf-8" src="../main.js"></script>
+  </head>
+  <body onload="init();" id="stage" class="theme">
+    <h1>Basic Auth</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;
+                   </span>, Color Depth: <span id="colorDepth"></span></h4>
+     </div>
+     <div id="info">
+     Loading link below should be successful and show page indicating username=test & password=test. <br>
+     </div>
+    <a href="http://browserspy.dk/password-ok.php" class="btn large">Test password</a>
+  </body>
+</html>
diff --git a/test/assets/www/cordova.android.js b/test/assets/www/cordova.android.js
new file mode 100644
index 0000000..5e460e4
--- /dev/null
+++ b/test/assets/www/cordova.android.js
@@ -0,0 +1,6878 @@
+// Platform: android
+// 2.8.0-0-g6208c95
+/*
+ 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 = '2.8.0-0-g6208c95';
+// file: lib/scripts/require.js
+
+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');
+
+/**
+ * Listen for DOMContentLoaded and notify our channel subscribers.
+ */
+document.addEventListener('DOMContentLoaded', function() {
+    channel.onDOMContentLoaded.fire();
+}, false);
+if (document.readyState == 'complete' || document.readyState == 'interactive') {
+    channel.onDOMContentLoaded.fire();
+}
+
+/**
+ * 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;
+}
+
+if(typeof window.console === "undefined") {
+    window.console = {
+        log:function(){}
+    };
+}
+
+var cordova = {
+    define:define,
+    require:require,
+    /**
+     * 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);
+            }
+        });
+    }
+};
+
+// 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');
+
+module.exports = cordova;
+
+});
+
+// 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 jake test.
+        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/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.
+ * onCordovaInfoReady*         Internal event fired when device properties are available.
+ * onCordovaConnectionReady*   Internal event fired when the connection property has been set.
+ * 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 device properties are available
+channel.createSticky('onCordovaInfoReady');
+
+// Event to indicate that the connection property has been set.
+channel.createSticky('onCordovaConnectionReady');
+
+// 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('onCordovaConnectionReady');
+channel.waitForInitialization('onDOMContentLoaded');
+
+module.exports = channel;
+
+});
+
+// file: lib/common/commandProxy.js
+define("cordova/commandProxy", function(require, exports, module) {
+
+
+// internal map of proxy function
+var CommandProxyMap = {};
+
+module.exports = {
+
+    // example: cordova.commandProxy.add("Accelerometer",{getCurrentAcceleration: function(successCallback, errorCallback, options) {...},...);
+    add:function(id,proxyObj) {
+        console.log("adding proxy for " + id);
+        CommandProxyMap[id] = proxyObj;
+        return proxyObj;
+    },
+
+    // cordova.commandProxy.remove("Accelerometer");
+    remove:function(id) {
+        var proxy = CommandProxyMap[id];
+        delete CommandProxyMap[id];
+        CommandProxyMap[id] = null;
+        return proxy;
+    },
+
+    get:function(service,action) {
+        return ( CommandProxyMap[service] ? CommandProxyMap[service][action] : null );
+    }
+};
+});
+
+// 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/plugin/android/nativeapiprovider'),
+    utils = require('cordova/utils'),
+    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] = window.btoa(String.fromCharCode.apply(null, new Uint8Array(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 pollOnce() {
+    var msg = nativeApiProvider.get().retrieveJsMessages();
+    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', pollOnce, false);
+    window.addEventListener('offline', pollOnce, 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/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);
+};
+
+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 symbolPath = symbolList[i + 2];
+        var lastDot = symbolPath.lastIndexOf('.');
+        var namespace = symbolPath.substr(0, lastDot);
+        var lastName = symbolPath.substr(lastDot + 1);
+
+        var module = require(moduleName);
+        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.loadMatchingModules = function(matchingRegExp) {
+    for (var k in moduleMap) {
+        if (matchingRegExp.exec(k)) {
+            require(k);
+        }
+    }
+};
+
+exports.reset();
+
+
+});
+
+// file: lib/android/platform.js
+define("cordova/platform", function(require, exports, module) {
+
+module.exports = {
+    id: "android",
+    initialize:function() {
+        var channel = require("cordova/channel"),
+            cordova = require('cordova'),
+            exec = require('cordova/exec'),
+            modulemapper = require('cordova/modulemapper');
+
+        modulemapper.loadMatchingModules(/cordova.*\/symbols$/);
+        modulemapper.clobbers('cordova/plugin/android/app', 'navigator.app');
+
+        modulemapper.mapModules(window);
+
+        // 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.join(function() {
+            exec(null, null, "App", "show", []);
+        }, [channel.onCordovaReady]);
+    }
+};
+
+});
+
+// file: lib/common/plugin/Acceleration.js
+define("cordova/plugin/Acceleration", function(require, exports, module) {
+
+var Acceleration = function(x, y, z, timestamp) {
+    this.x = x;
+    this.y = y;
+    this.z = z;
+    this.timestamp = timestamp || (new Date()).getTime();
+};
+
+module.exports = Acceleration;
+
+});
+
+// file: lib/common/plugin/Camera.js
+define("cordova/plugin/Camera", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    exec = require('cordova/exec'),
+    Camera = require('cordova/plugin/CameraConstants'),
+    CameraPopoverHandle = require('cordova/plugin/CameraPopoverHandle');
+
+var cameraExport = {};
+
+// Tack on the Camera Constants to the base camera plugin.
+for (var key in Camera) {
+    cameraExport[key] = Camera[key];
+}
+
+/**
+ * Gets a picture from source defined by "options.sourceType", and returns the
+ * image as defined by the "options.destinationType" option.
+
+ * The defaults are sourceType=CAMERA and destinationType=FILE_URI.
+ *
+ * @param {Function} successCallback
+ * @param {Function} errorCallback
+ * @param {Object} options
+ */
+cameraExport.getPicture = function(successCallback, errorCallback, options) {
+    argscheck.checkArgs('fFO', 'Camera.getPicture', arguments);
+    options = options || {};
+    var getValue = argscheck.getValue;
+
+    var quality = getValue(options.quality, 50);
+    var destinationType = getValue(options.destinationType, Camera.DestinationType.FILE_URI);
+    var sourceType = getValue(options.sourceType, Camera.PictureSourceType.CAMERA);
+    var targetWidth = getValue(options.targetWidth, -1);
+    var targetHeight = getValue(options.targetHeight, -1);
+    var encodingType = getValue(options.encodingType, Camera.EncodingType.JPEG);
+    var mediaType = getValue(options.mediaType, Camera.MediaType.PICTURE);
+    var allowEdit = !!options.allowEdit;
+    var correctOrientation = !!options.correctOrientation;
+    var saveToPhotoAlbum = !!options.saveToPhotoAlbum;
+    var popoverOptions = getValue(options.popoverOptions, null);
+    var cameraDirection = getValue(options.cameraDirection, Camera.Direction.BACK);
+
+    var args = [quality, destinationType, sourceType, targetWidth, targetHeight, encodingType,
+                mediaType, allowEdit, correctOrientation, saveToPhotoAlbum, popoverOptions, cameraDirection];
+
+    exec(successCallback, errorCallback, "Camera", "takePicture", args);
+    return new CameraPopoverHandle();
+};
+
+cameraExport.cleanup = function(successCallback, errorCallback) {
+    exec(successCallback, errorCallback, "Camera", "cleanup", []);
+};
+
+module.exports = cameraExport;
+
+});
+
+// file: lib/common/plugin/CameraConstants.js
+define("cordova/plugin/CameraConstants", function(require, exports, module) {
+
+module.exports = {
+  DestinationType:{
+    DATA_URL: 0,         // Return base64 encoded string
+    FILE_URI: 1,         // Return file uri (content://media/external/images/media/2 for Android)
+    NATIVE_URI: 2        // Return native uri (eg. asset-library://... for iOS)
+  },
+  EncodingType:{
+    JPEG: 0,             // Return JPEG encoded image
+    PNG: 1               // Return PNG encoded image
+  },
+  MediaType:{
+    PICTURE: 0,          // allow selection of still pictures only. DEFAULT. Will return format specified via DestinationType
+    VIDEO: 1,            // allow selection of video only, ONLY RETURNS URL
+    ALLMEDIA : 2         // allow selection from all media types
+  },
+  PictureSourceType:{
+    PHOTOLIBRARY : 0,    // Choose image from picture library (same as SAVEDPHOTOALBUM for Android)
+    CAMERA : 1,          // Take picture from camera
+    SAVEDPHOTOALBUM : 2  // Choose image from picture library (same as PHOTOLIBRARY for Android)
+  },
+  PopoverArrowDirection:{
+      ARROW_UP : 1,        // matches iOS UIPopoverArrowDirection constants to specify arrow location on popover
+      ARROW_DOWN : 2,
+      ARROW_LEFT : 4,
+      ARROW_RIGHT : 8,
+      ARROW_ANY : 15
+  },
+  Direction:{
+      BACK: 0,
+      FRONT: 1
+  }
+};
+
+});
+
+// file: lib/common/plugin/CameraPopoverHandle.js
+define("cordova/plugin/CameraPopoverHandle", function(require, exports, module) {
+
+var exec = require('cordova/exec');
+
+/**
+ * A handle to an image picker popover.
+ */
+var CameraPopoverHandle = function() {
+    this.setPosition = function(popoverOptions) {
+        console.log('CameraPopoverHandle.setPosition is only supported on iOS.');
+    };
+};
+
+module.exports = CameraPopoverHandle;
+
+});
+
+// file: lib/common/plugin/CameraPopoverOptions.js
+define("cordova/plugin/CameraPopoverOptions", function(require, exports, module) {
+
+var Camera = require('cordova/plugin/CameraConstants');
+
+/**
+ * Encapsulates options for iOS Popover image picker
+ */
+var CameraPopoverOptions = function(x,y,width,height,arrowDir){
+    // information of rectangle that popover should be anchored to
+    this.x = x || 0;
+    this.y = y || 32;
+    this.width = width || 320;
+    this.height = height || 480;
+    // The direction of the popover arrow
+    this.arrowDir = arrowDir || Camera.PopoverArrowDirection.ARROW_ANY;
+};
+
+module.exports = CameraPopoverOptions;
+
+});
+
+// file: lib/common/plugin/CaptureAudioOptions.js
+define("cordova/plugin/CaptureAudioOptions", function(require, exports, module) {
+
+/**
+ * Encapsulates all audio capture operation configuration options.
+ */
+var CaptureAudioOptions = function(){
+    // Upper limit of sound clips user can record. Value must be equal or greater than 1.
+    this.limit = 1;
+    // Maximum duration of a single sound clip in seconds.
+    this.duration = 0;
+};
+
+module.exports = CaptureAudioOptions;
+
+});
+
+// file: lib/common/plugin/CaptureError.js
+define("cordova/plugin/CaptureError", function(require, exports, module) {
+
+/**
+ * The CaptureError interface encapsulates all errors in the Capture API.
+ */
+var CaptureError = function(c) {
+   this.code = c || null;
+};
+
+// Camera or microphone failed to capture image or sound.
+CaptureError.CAPTURE_INTERNAL_ERR = 0;
+// Camera application or audio capture application is currently serving other capture request.
+CaptureError.CAPTURE_APPLICATION_BUSY = 1;
+// Invalid use of the API (e.g. limit parameter has value less than one).
+CaptureError.CAPTURE_INVALID_ARGUMENT = 2;
+// User exited camera application or audio capture application before capturing anything.
+CaptureError.CAPTURE_NO_MEDIA_FILES = 3;
+// The requested capture operation is not supported.
+CaptureError.CAPTURE_NOT_SUPPORTED = 20;
+
+module.exports = CaptureError;
+
+});
+
+// file: lib/common/plugin/CaptureImageOptions.js
+define("cordova/plugin/CaptureImageOptions", function(require, exports, module) {
+
+/**
+ * Encapsulates all image capture operation configuration options.
+ */
+var CaptureImageOptions = function(){
+    // Upper limit of images user can take. Value must be equal or greater than 1.
+    this.limit = 1;
+};
+
+module.exports = CaptureImageOptions;
+
+});
+
+// file: lib/common/plugin/CaptureVideoOptions.js
+define("cordova/plugin/CaptureVideoOptions", function(require, exports, module) {
+
+/**
+ * Encapsulates all video capture operation configuration options.
+ */
+var CaptureVideoOptions = function(){
+    // Upper limit of videos user can record. Value must be equal or greater than 1.
+    this.limit = 1;
+    // Maximum duration of a single video clip in seconds.
+    this.duration = 0;
+};
+
+module.exports = CaptureVideoOptions;
+
+});
+
+// file: lib/common/plugin/CompassError.js
+define("cordova/plugin/CompassError", function(require, exports, module) {
+
+/**
+ *  CompassError.
+ *  An error code assigned by an implementation when an error has occurred
+ * @constructor
+ */
+var CompassError = function(err) {
+    this.code = (err !== undefined ? err : null);
+};
+
+CompassError.COMPASS_INTERNAL_ERR = 0;
+CompassError.COMPASS_NOT_SUPPORTED = 20;
+
+module.exports = CompassError;
+
+});
+
+// file: lib/common/plugin/CompassHeading.js
+define("cordova/plugin/CompassHeading", function(require, exports, module) {
+
+var CompassHeading = function(magneticHeading, trueHeading, headingAccuracy, timestamp) {
+  this.magneticHeading = magneticHeading;
+  this.trueHeading = trueHeading;
+  this.headingAccuracy = headingAccuracy;
+  this.timestamp = timestamp || new Date().getTime();
+};
+
+module.exports = CompassHeading;
+
+});
+
+// file: lib/common/plugin/ConfigurationData.js
+define("cordova/plugin/ConfigurationData", function(require, exports, module) {
+
+/**
+ * Encapsulates a set of parameters that the capture device supports.
+ */
+function ConfigurationData() {
+    // The ASCII-encoded string in lower case representing the media type.
+    this.type = null;
+    // The height attribute represents height of the image or video in pixels.
+    // In the case of a sound clip this attribute has value 0.
+    this.height = 0;
+    // The width attribute represents width of the image or video in pixels.
+    // In the case of a sound clip this attribute has value 0
+    this.width = 0;
+}
+
+module.exports = ConfigurationData;
+
+});
+
+// file: lib/common/plugin/Connection.js
+define("cordova/plugin/Connection", function(require, exports, module) {
+
+/**
+ * Network status
+ */
+module.exports = {
+        UNKNOWN: "unknown",
+        ETHERNET: "ethernet",
+        WIFI: "wifi",
+        CELL_2G: "2g",
+        CELL_3G: "3g",
+        CELL_4G: "4g",
+        CELL:"cellular",
+        NONE: "none"
+};
+
+});
+
+// file: lib/common/plugin/Contact.js
+define("cordova/plugin/Contact", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    exec = require('cordova/exec'),
+    ContactError = require('cordova/plugin/ContactError'),
+    utils = require('cordova/utils');
+
+/**
+* Converts primitives into Complex Object
+* Currently only used for Date fields
+*/
+function convertIn(contact) {
+    var value = contact.birthday;
+    try {
+      contact.birthday = new Date(parseFloat(value));
+    } catch (exception){
+      console.log("Cordova Contact convertIn error: exception creating date.");
+    }
+    return contact;
+}
+
+/**
+* Converts Complex objects into primitives
+* Only conversion at present is for Dates.
+**/
+
+function convertOut(contact) {
+    var value = contact.birthday;
+    if (value !== null) {
+        // try to make it a Date object if it is not already
+        if (!utils.isDate(value)){
+            try {
+                value = new Date(value);
+            } catch(exception){
+                value = null;
+            }
+        }
+        if (utils.isDate(value)){
+            value = value.valueOf(); // convert to milliseconds
+        }
+        contact.birthday = value;
+    }
+    return contact;
+}
+
+/**
+* Contains information about a single contact.
+* @constructor
+* @param {DOMString} id unique identifier
+* @param {DOMString} displayName
+* @param {ContactName} name
+* @param {DOMString} nickname
+* @param {Array.<ContactField>} phoneNumbers array of phone numbers
+* @param {Array.<ContactField>} emails array of email addresses
+* @param {Array.<ContactAddress>} addresses array of addresses
+* @param {Array.<ContactField>} ims instant messaging user ids
+* @param {Array.<ContactOrganization>} organizations
+* @param {DOMString} birthday contact's birthday
+* @param {DOMString} note user notes about contact
+* @param {Array.<ContactField>} photos
+* @param {Array.<ContactField>} categories
+* @param {Array.<ContactField>} urls contact's web sites
+*/
+var Contact = function (id, displayName, name, nickname, phoneNumbers, emails, addresses,
+    ims, organizations, birthday, note, photos, categories, urls) {
+    this.id = id || null;
+    this.rawId = null;
+    this.displayName = displayName || null;
+    this.name = name || null; // ContactName
+    this.nickname = nickname || null;
+    this.phoneNumbers = phoneNumbers || null; // ContactField[]
+    this.emails = emails || null; // ContactField[]
+    this.addresses = addresses || null; // ContactAddress[]
+    this.ims = ims || null; // ContactField[]
+    this.organizations = organizations || null; // ContactOrganization[]
+    this.birthday = birthday || null;
+    this.note = note || null;
+    this.photos = photos || null; // ContactField[]
+    this.categories = categories || null; // ContactField[]
+    this.urls = urls || null; // ContactField[]
+};
+
+/**
+* Removes contact from device storage.
+* @param successCB success callback
+* @param errorCB error callback
+*/
+Contact.prototype.remove = function(successCB, errorCB) {
+    argscheck.checkArgs('FF', 'Contact.remove', arguments);
+    var fail = errorCB && function(code) {
+        errorCB(new ContactError(code));
+    };
+    if (this.id === null) {
+        fail(ContactError.UNKNOWN_ERROR);
+    }
+    else {
+        exec(successCB, fail, "Contacts", "remove", [this.id]);
+    }
+};
+
+/**
+* Creates a deep copy of this Contact.
+* With the contact ID set to null.
+* @return copy of this Contact
+*/
+Contact.prototype.clone = function() {
+    var clonedContact = utils.clone(this);
+    clonedContact.id = null;
+    clonedContact.rawId = null;
+
+    function nullIds(arr) {
+        if (arr) {
+            for (var i = 0; i < arr.length; ++i) {
+                arr[i].id = null;
+            }
+        }
+    }
+
+    // Loop through and clear out any id's in phones, emails, etc.
+    nullIds(clonedContact.phoneNumbers);
+    nullIds(clonedContact.emails);
+    nullIds(clonedContact.addresses);
+    nullIds(clonedContact.ims);
+    nullIds(clonedContact.organizations);
+    nullIds(clonedContact.categories);
+    nullIds(clonedContact.photos);
+    nullIds(clonedContact.urls);
+    return clonedContact;
+};
+
+/**
+* Persists contact to device storage.
+* @param successCB success callback
+* @param errorCB error callback
+*/
+Contact.prototype.save = function(successCB, errorCB) {
+    argscheck.checkArgs('FFO', 'Contact.save', arguments);
+    var fail = errorCB && function(code) {
+        errorCB(new ContactError(code));
+    };
+    var success = function(result) {
+        if (result) {
+            if (successCB) {
+                var fullContact = require('cordova/plugin/contacts').create(result);
+                successCB(convertIn(fullContact));
+            }
+        }
+        else {
+            // no Entry object returned
+            fail(ContactError.UNKNOWN_ERROR);
+        }
+    };
+    var dupContact = convertOut(utils.clone(this));
+    exec(success, fail, "Contacts", "save", [dupContact]);
+};
+
+
+module.exports = Contact;
+
+});
+
+// file: lib/common/plugin/ContactAddress.js
+define("cordova/plugin/ContactAddress", function(require, exports, module) {
+
+/**
+* Contact address.
+* @constructor
+* @param {DOMString} id unique identifier, should only be set by native code
+* @param formatted // NOTE: not a W3C standard
+* @param streetAddress
+* @param locality
+* @param region
+* @param postalCode
+* @param country
+*/
+
+var ContactAddress = function(pref, type, formatted, streetAddress, locality, region, postalCode, country) {
+    this.id = null;
+    this.pref = (typeof pref != 'undefined' ? pref : false);
+    this.type = type || null;
+    this.formatted = formatted || null;
+    this.streetAddress = streetAddress || null;
+    this.locality = locality || null;
+    this.region = region || null;
+    this.postalCode = postalCode || null;
+    this.country = country || null;
+};
+
+module.exports = ContactAddress;
+
+});
+
+// file: lib/common/plugin/ContactError.js
+define("cordova/plugin/ContactError", function(require, exports, module) {
+
+/**
+ *  ContactError.
+ *  An error code assigned by an implementation when an error has occurred
+ * @constructor
+ */
+var ContactError = function(err) {
+    this.code = (typeof err != 'undefined' ? err : null);
+};
+
+/**
+ * Error codes
+ */
+ContactError.UNKNOWN_ERROR = 0;
+ContactError.INVALID_ARGUMENT_ERROR = 1;
+ContactError.TIMEOUT_ERROR = 2;
+ContactError.PENDING_OPERATION_ERROR = 3;
+ContactError.IO_ERROR = 4;
+ContactError.NOT_SUPPORTED_ERROR = 5;
+ContactError.PERMISSION_DENIED_ERROR = 20;
+
+module.exports = ContactError;
+
+});
+
+// file: lib/common/plugin/ContactField.js
+define("cordova/plugin/ContactField", function(require, exports, module) {
+
+/**
+* Generic contact field.
+* @constructor
+* @param {DOMString} id unique identifier, should only be set by native code // NOTE: not a W3C standard
+* @param type
+* @param value
+* @param pref
+*/
+var ContactField = function(type, value, pref) {
+    this.id = null;
+    this.type = (type && type.toString()) || null;
+    this.value = (value && value.toString()) || null;
+    this.pref = (typeof pref != 'undefined' ? pref : false);
+};
+
+module.exports = ContactField;
+
+});
+
+// file: lib/common/plugin/ContactFindOptions.js
+define("cordova/plugin/ContactFindOptions", function(require, exports, module) {
+
+/**
+ * ContactFindOptions.
+ * @constructor
+ * @param filter used to match contacts against
+ * @param multiple boolean used to determine if more than one contact should be returned
+ */
+
+var ContactFindOptions = function(filter, multiple) {
+    this.filter = filter || '';
+    this.multiple = (typeof multiple != 'undefined' ? multiple : false);
+};
+
+module.exports = ContactFindOptions;
+
+});
+
+// file: lib/common/plugin/ContactName.js
+define("cordova/plugin/ContactName", function(require, exports, module) {
+
+/**
+* Contact name.
+* @constructor
+* @param formatted // NOTE: not part of W3C standard
+* @param familyName
+* @param givenName
+* @param middle
+* @param prefix
+* @param suffix
+*/
+var ContactName = function(formatted, familyName, givenName, middle, prefix, suffix) {
+    this.formatted = formatted || null;
+    this.familyName = familyName || null;
+    this.givenName = givenName || null;
+    this.middleName = middle || null;
+    this.honorificPrefix = prefix || null;
+    this.honorificSuffix = suffix || null;
+};
+
+module.exports = ContactName;
+
+});
+
+// file: lib/common/plugin/ContactOrganization.js
+define("cordova/plugin/ContactOrganization", function(require, exports, module) {
+
+/**
+* Contact organization.
+* @constructor
+* @param {DOMString} id unique identifier, should only be set by native code // NOTE: not a W3C standard
+* @param name
+* @param dept
+* @param title
+* @param startDate
+* @param endDate
+* @param location
+* @param desc
+*/
+
+var ContactOrganization = function(pref, type, name, dept, title) {
+    this.id = null;
+    this.pref = (typeof pref != 'undefined' ? pref : false);
+    this.type = type || null;
+    this.name = name || null;
+    this.department = dept || null;
+    this.title = title || null;
+};
+
+module.exports = ContactOrganization;
+
+});
+
+// file: lib/common/plugin/Coordinates.js
+define("cordova/plugin/Coordinates", function(require, exports, module) {
+
+/**
+ * This class contains position information.
+ * @param {Object} lat
+ * @param {Object} lng
+ * @param {Object} alt
+ * @param {Object} acc
+ * @param {Object} head
+ * @param {Object} vel
+ * @param {Object} altacc
+ * @constructor
+ */
+var Coordinates = function(lat, lng, alt, acc, head, vel, altacc) {
+    /**
+     * The latitude of the position.
+     */
+    this.latitude = lat;
+    /**
+     * The longitude of the position,
+     */
+    this.longitude = lng;
+    /**
+     * The accuracy of the position.
+     */
+    this.accuracy = acc;
+    /**
+     * The altitude of the position.
+     */
+    this.altitude = (alt !== undefined ? alt : null);
+    /**
+     * The direction the device is moving at the position.
+     */
+    this.heading = (head !== undefined ? head : null);
+    /**
+     * The velocity with which the device is moving at the position.
+     */
+    this.speed = (vel !== undefined ? vel : null);
+
+    if (this.speed === 0 || this.speed === null) {
+        this.heading = NaN;
+    }
+
+    /**
+     * The altitude accuracy of the position.
+     */
+    this.altitudeAccuracy = (altacc !== undefined) ? altacc : null;
+};
+
+module.exports = Coordinates;
+
+});
+
+// file: lib/common/plugin/DirectoryEntry.js
+define("cordova/plugin/DirectoryEntry", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    utils = require('cordova/utils'),
+    exec = require('cordova/exec'),
+    Entry = require('cordova/plugin/Entry'),
+    FileError = require('cordova/plugin/FileError'),
+    DirectoryReader = require('cordova/plugin/DirectoryReader');
+
+/**
+ * An interface representing a directory on the file system.
+ *
+ * {boolean} isFile always false (readonly)
+ * {boolean} isDirectory always true (readonly)
+ * {DOMString} name of the directory, excluding the path leading to it (readonly)
+ * {DOMString} fullPath the absolute full path to the directory (readonly)
+ * TODO: implement this!!! {FileSystem} filesystem on which the directory resides (readonly)
+ */
+var DirectoryEntry = function(name, fullPath) {
+     DirectoryEntry.__super__.constructor.call(this, false, true, name, fullPath);
+};
+
+utils.extend(DirectoryEntry, Entry);
+
+/**
+ * Creates a new DirectoryReader to read entries from this directory
+ */
+DirectoryEntry.prototype.createReader = function() {
+    return new DirectoryReader(this.fullPath);
+};
+
+/**
+ * Creates or looks up a directory
+ *
+ * @param {DOMString} path either a relative or absolute path from this directory in which to look up or create a directory
+ * @param {Flags} options to create or exclusively create the directory
+ * @param {Function} successCallback is called with the new entry
+ * @param {Function} errorCallback is called with a FileError
+ */
+DirectoryEntry.prototype.getDirectory = function(path, options, successCallback, errorCallback) {
+    argscheck.checkArgs('sOFF', 'DirectoryEntry.getDirectory', arguments);
+    var win = successCallback && function(result) {
+        var entry = new DirectoryEntry(result.name, result.fullPath);
+        successCallback(entry);
+    };
+    var fail = errorCallback && function(code) {
+        errorCallback(new FileError(code));
+    };
+    exec(win, fail, "File", "getDirectory", [this.fullPath, path, options]);
+};
+
+/**
+ * Deletes a directory and all of it's contents
+ *
+ * @param {Function} successCallback is called with no parameters
+ * @param {Function} errorCallback is called with a FileError
+ */
+DirectoryEntry.prototype.removeRecursively = function(successCallback, errorCallback) {
+    argscheck.checkArgs('FF', 'DirectoryEntry.removeRecursively', arguments);
+    var fail = errorCallback && function(code) {
+        errorCallback(new FileError(code));
+    };
+    exec(successCallback, fail, "File", "removeRecursively", [this.fullPath]);
+};
+
+/**
+ * Creates or looks up a file
+ *
+ * @param {DOMString} path either a relative or absolute path from this directory in which to look up or create a file
+ * @param {Flags} options to create or exclusively create the file
+ * @param {Function} successCallback is called with the new entry
+ * @param {Function} errorCallback is called with a FileError
+ */
+DirectoryEntry.prototype.getFile = function(path, options, successCallback, errorCallback) {
+    argscheck.checkArgs('sOFF', 'DirectoryEntry.getFile', arguments);
+    var win = successCallback && function(result) {
+        var FileEntry = require('cordova/plugin/FileEntry');
+        var entry = new FileEntry(result.name, result.fullPath);
+        successCallback(entry);
+    };
+    var fail = errorCallback && function(code) {
+        errorCallback(new FileError(code));
+    };
+    exec(win, fail, "File", "getFile", [this.fullPath, path, options]);
+};
+
+module.exports = DirectoryEntry;
+
+});
+
+// file: lib/common/plugin/DirectoryReader.js
+define("cordova/plugin/DirectoryReader", function(require, exports, module) {
+
+var exec = require('cordova/exec'),
+    FileError = require('cordova/plugin/FileError') ;
+
+/**
+ * An interface that lists the files and directories in a directory.
+ */
+function DirectoryReader(path) {
+    this.path = path || null;
+}
+
+/**
+ * Returns a list of entries from a directory.
+ *
+ * @param {Function} successCallback is called with a list of entries
+ * @param {Function} errorCallback is called with a FileError
+ */
+DirectoryReader.prototype.readEntries = function(successCallback, errorCallback) {
+    var win = typeof successCallback !== 'function' ? null : function(result) {
+        var retVal = [];
+        for (var i=0; i<result.length; i++) {
+            var entry = null;
+            if (result[i].isDirectory) {
+                entry = new (require('cordova/plugin/DirectoryEntry'))();
+            }
+            else if (result[i].isFile) {
+                entry = new (require('cordova/plugin/FileEntry'))();
+            }
+            entry.isDirectory = result[i].isDirectory;
+            entry.isFile = result[i].isFile;
+            entry.name = result[i].name;
+            entry.fullPath = result[i].fullPath;
+            retVal.push(entry);
+        }
+        successCallback(retVal);
+    };
+    var fail = typeof errorCallback !== 'function' ? null : function(code) {
+        errorCallback(new FileError(code));
+    };
+    exec(win, fail, "File", "readEntries", [this.path]);
+};
+
+module.exports = DirectoryReader;
+
+});
+
+// file: lib/common/plugin/Entry.js
+define("cordova/plugin/Entry", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    exec = require('cordova/exec'),
+    FileError = require('cordova/plugin/FileError'),
+    Metadata = require('cordova/plugin/Metadata');
+
+/**
+ * Represents a file or directory on the local file system.
+ *
+ * @param isFile
+ *            {boolean} true if Entry is a file (readonly)
+ * @param isDirectory
+ *            {boolean} true if Entry is a directory (readonly)
+ * @param name
+ *            {DOMString} name of the file or directory, excluding the path
+ *            leading to it (readonly)
+ * @param fullPath
+ *            {DOMString} the absolute full path to the file or directory
+ *            (readonly)
+ */
+function Entry(isFile, isDirectory, name, fullPath, fileSystem) {
+    this.isFile = !!isFile;
+    this.isDirectory = !!isDirectory;
+    this.name = name || '';
+    this.fullPath = fullPath || '';
+    this.filesystem = fileSystem || null;
+}
+
+/**
+ * Look up the metadata of the entry.
+ *
+ * @param successCallback
+ *            {Function} is called with a Metadata object
+ * @param errorCallback
+ *            {Function} is called with a FileError
+ */
+Entry.prototype.getMetadata = function(successCallback, errorCallback) {
+    argscheck.checkArgs('FF', 'Entry.getMetadata', arguments);
+    var success = successCallback && function(lastModified) {
+        var metadata = new Metadata(lastModified);
+        successCallback(metadata);
+    };
+    var fail = errorCallback && function(code) {
+        errorCallback(new FileError(code));
+    };
+
+    exec(success, fail, "File", "getMetadata", [this.fullPath]);
+};
+
+/**
+ * Set the metadata of the entry.
+ *
+ * @param successCallback
+ *            {Function} is called with a Metadata object
+ * @param errorCallback
+ *            {Function} is called with a FileError
+ * @param metadataObject
+ *            {Object} keys and values to set
+ */
+Entry.prototype.setMetadata = function(successCallback, errorCallback, metadataObject) {
+    argscheck.checkArgs('FFO', 'Entry.setMetadata', arguments);
+    exec(successCallback, errorCallback, "File", "setMetadata", [this.fullPath, metadataObject]);
+};
+
+/**
+ * Move a file or directory to a new location.
+ *
+ * @param parent
+ *            {DirectoryEntry} the directory to which to move this entry
+ * @param newName
+ *            {DOMString} new name of the entry, defaults to the current name
+ * @param successCallback
+ *            {Function} called with the new DirectoryEntry object
+ * @param errorCallback
+ *            {Function} called with a FileError
+ */
+Entry.prototype.moveTo = function(parent, newName, successCallback, errorCallback) {
+    argscheck.checkArgs('oSFF', 'Entry.moveTo', arguments);
+    var fail = errorCallback && function(code) {
+        errorCallback(new FileError(code));
+    };
+    // source path
+    var srcPath = this.fullPath,
+        // entry name
+        name = newName || this.name,
+        success = function(entry) {
+            if (entry) {
+                if (successCallback) {
+                    // create appropriate Entry object
+                    var result = (entry.isDirectory) ? new (require('cordova/plugin/DirectoryEntry'))(entry.name, entry.fullPath) : new (require('cordova/plugin/FileEntry'))(entry.name, entry.fullPath);
+                    successCallback(result);
+                }
+            }
+            else {
+                // no Entry object returned
+                fail && fail(FileError.NOT_FOUND_ERR);
+            }
+        };
+
+    // copy
+    exec(success, fail, "File", "moveTo", [srcPath, parent.fullPath, name]);
+};
+
+/**
+ * Copy a directory to a different location.
+ *
+ * @param parent
+ *            {DirectoryEntry} the directory to which to copy the entry
+ * @param newName
+ *            {DOMString} new name of the entry, defaults to the current name
+ * @param successCallback
+ *            {Function} called with the new Entry object
+ * @param errorCallback
+ *            {Function} called with a FileError
+ */
+Entry.prototype.copyTo = function(parent, newName, successCallback, errorCallback) {
+    argscheck.checkArgs('oSFF', 'Entry.copyTo', arguments);
+    var fail = errorCallback && function(code) {
+        errorCallback(new FileError(code));
+    };
+
+        // source path
+    var srcPath = this.fullPath,
+        // entry name
+        name = newName || this.name,
+        // success callback
+        success = function(entry) {
+            if (entry) {
+                if (successCallback) {
+                    // create appropriate Entry object
+                    var result = (entry.isDirectory) ? new (require('cordova/plugin/DirectoryEntry'))(entry.name, entry.fullPath) : new (require('cordova/plugin/FileEntry'))(entry.name, entry.fullPath);
+                    successCallback(result);
+                }
+            }
+            else {
+                // no Entry object returned
+                fail && fail(FileError.NOT_FOUND_ERR);
+            }
+        };
+
+    // copy
+    exec(success, fail, "File", "copyTo", [srcPath, parent.fullPath, name]);
+};
+
+/**
+ * Return a URL that can be used to identify this entry.
+ */
+Entry.prototype.toURL = function() {
+    // fullPath attribute contains the full URL
+    return this.fullPath;
+};
+
+/**
+ * Returns a URI that can be used to identify this entry.
+ *
+ * @param {DOMString} mimeType for a FileEntry, the mime type to be used to interpret the file, when loaded through this URI.
+ * @return uri
+ */
+Entry.prototype.toURI = function(mimeType) {
+    console.log("DEPRECATED: Update your code to use 'toURL'");
+    // fullPath attribute contains the full URI
+    return this.toURL();
+};
+
+/**
+ * Remove a file or directory. It is an error to attempt to delete a
+ * directory that is not empty. It is an error to attempt to delete a
+ * root directory of a file system.
+ *
+ * @param successCallback {Function} called with no parameters
+ * @param errorCallback {Function} called with a FileError
+ */
+Entry.prototype.remove = function(successCallback, errorCallback) {
+    argscheck.checkArgs('FF', 'Entry.remove', arguments);
+    var fail = errorCallback && function(code) {
+        errorCallback(new FileError(code));
+    };
+    exec(successCallback, fail, "File", "remove", [this.fullPath]);
+};
+
+/**
+ * Look up the parent DirectoryEntry of this entry.
+ *
+ * @param successCallback {Function} called with the parent DirectoryEntry object
+ * @param errorCallback {Function} called with a FileError
+ */
+Entry.prototype.getParent = function(successCallback, errorCallback) {
+    argscheck.checkArgs('FF', 'Entry.getParent', arguments);
+    var win = successCallback && function(result) {
+        var DirectoryEntry = require('cordova/plugin/DirectoryEntry');
+        var entry = new DirectoryEntry(result.name, result.fullPath);
+        successCallback(entry);
+    };
+    var fail = errorCallback && function(code) {
+        errorCallback(new FileError(code));
+    };
+    exec(win, fail, "File", "getParent", [this.fullPath]);
+};
+
+module.exports = Entry;
+
+});
+
+// file: lib/common/plugin/File.js
+define("cordova/plugin/File", function(require, exports, module) {
+
+/**
+ * Constructor.
+ * name {DOMString} name of the file, without path information
+ * fullPath {DOMString} the full path of the file, including the name
+ * type {DOMString} mime type
+ * lastModifiedDate {Date} last modified date
+ * size {Number} size of the file in bytes
+ */
+
+var File = function(name, fullPath, type, lastModifiedDate, size){
+    this.name = name || '';
+    this.fullPath = fullPath || null;
+    this.type = type || null;
+    this.lastModifiedDate = lastModifiedDate || null;
+    this.size = size || 0;
+
+    // These store the absolute start and end for slicing the file.
+    this.start = 0;
+    this.end = this.size;
+};
+
+/**
+ * Returns a "slice" of the file. Since Cordova Files don't contain the actual
+ * content, this really returns a File with adjusted start and end.
+ * Slices of slices are supported.
+ * start {Number} The index at which to start the slice (inclusive).
+ * end {Number} The index at which to end the slice (exclusive).
+ */
+File.prototype.slice = function(start, end) {
+    var size = this.end - this.start;
+    var newStart = 0;
+    var newEnd = size;
+    if (arguments.length) {
+        if (start < 0) {
+            newStart = Math.max(size + start, 0);
+        } else {
+            newStart = Math.min(size, start);
+        }
+    }
+
+    if (arguments.length >= 2) {
+        if (end < 0) {
+            newEnd = Math.max(size + end, 0);
+        } else {
+            newEnd = Math.min(end, size);
+        }
+    }
+
+    var newFile = new File(this.name, this.fullPath, this.type, this.lastModifiedData, this.size);
+    newFile.start = this.start + newStart;
+    newFile.end = this.start + newEnd;
+    return newFile;
+};
+
+
+module.exports = File;
+
+});
+
+// file: lib/common/plugin/FileEntry.js
+define("cordova/plugin/FileEntry", function(require, exports, module) {
+
+var utils = require('cordova/utils'),
+    exec = require('cordova/exec'),
+    Entry = require('cordova/plugin/Entry'),
+    FileWriter = require('cordova/plugin/FileWriter'),
+    File = require('cordova/plugin/File'),
+    FileError = require('cordova/plugin/FileError');
+
+/**
+ * An interface representing a file on the file system.
+ *
+ * {boolean} isFile always true (readonly)
+ * {boolean} isDirectory always false (readonly)
+ * {DOMString} name of the file, excluding the path leading to it (readonly)
+ * {DOMString} fullPath the absolute full path to the file (readonly)
+ * {FileSystem} filesystem on which the file resides (readonly)
+ */
+var FileEntry = function(name, fullPath) {
+     FileEntry.__super__.constructor.apply(this, [true, false, name, fullPath]);
+};
+
+utils.extend(FileEntry, Entry);
+
+/**
+ * Creates a new FileWriter associated with the file that this FileEntry represents.
+ *
+ * @param {Function} successCallback is called with the new FileWriter
+ * @param {Function} errorCallback is called with a FileError
+ */
+FileEntry.prototype.createWriter = function(successCallback, errorCallback) {
+    this.file(function(filePointer) {
+        var writer = new FileWriter(filePointer);
+
+        if (writer.fileName === null || writer.fileName === "") {
+            errorCallback && errorCallback(new FileError(FileError.INVALID_STATE_ERR));
+        } else {
+            successCallback && successCallback(writer);
+        }
+    }, errorCallback);
+};
+
+/**
+ * Returns a File that represents the current state of the file that this FileEntry represents.
+ *
+ * @param {Function} successCallback is called with the new File object
+ * @param {Function} errorCallback is called with a FileError
+ */
+FileEntry.prototype.file = function(successCallback, errorCallback) {
+    var win = successCallback && function(f) {
+        var file = new File(f.name, f.fullPath, f.type, f.lastModifiedDate, f.size);
+        successCallback(file);
+    };
+    var fail = errorCallback && function(code) {
+        errorCallback(new FileError(code));
+    };
+    exec(win, fail, "File", "getFileMetadata", [this.fullPath]);
+};
+
+
+module.exports = FileEntry;
+
+});
+
+// file: lib/common/plugin/FileError.js
+define("cordova/plugin/FileError", function(require, exports, module) {
+
+/**
+ * FileError
+ */
+function FileError(error) {
+  this.code = error || null;
+}
+
+// File error codes
+// Found in DOMException
+FileError.NOT_FOUND_ERR = 1;
+FileError.SECURITY_ERR = 2;
+FileError.ABORT_ERR = 3;
+
+// Added by File API specification
+FileError.NOT_READABLE_ERR = 4;
+FileError.ENCODING_ERR = 5;
+FileError.NO_MODIFICATION_ALLOWED_ERR = 6;
+FileError.INVALID_STATE_ERR = 7;
+FileError.SYNTAX_ERR = 8;
+FileError.INVALID_MODIFICATION_ERR = 9;
+FileError.QUOTA_EXCEEDED_ERR = 10;
+FileError.TYPE_MISMATCH_ERR = 11;
+FileError.PATH_EXISTS_ERR = 12;
+
+module.exports = FileError;
+
+});
+
+// file: lib/common/plugin/FileReader.js
+define("cordova/plugin/FileReader", function(require, exports, module) {
+
+var exec = require('cordova/exec'),
+    modulemapper = require('cordova/modulemapper'),
+    utils = require('cordova/utils'),
+    File = require('cordova/plugin/File'),
+    FileError = require('cordova/plugin/FileError'),
+    ProgressEvent = require('cordova/plugin/ProgressEvent'),
+    origFileReader = modulemapper.getOriginalSymbol(this, 'FileReader');
+
+/**
+ * This class reads the mobile device file system.
+ *
+ * For Android:
+ *      The root directory is the root of the file system.
+ *      To read from the SD card, the file name is "sdcard/my_file.txt"
+ * @constructor
+ */
+var FileReader = function() {
+    this._readyState = 0;
+    this._error = null;
+    this._result = null;
+    this._fileName = '';
+    this._realReader = origFileReader ? new origFileReader() : {};
+};
+
+// States
+FileReader.EMPTY = 0;
+FileReader.LOADING = 1;
+FileReader.DONE = 2;
+
+utils.defineGetter(FileReader.prototype, 'readyState', function() {
+    return this._fileName ? this._readyState : this._realReader.readyState;
+});
+
+utils.defineGetter(FileReader.prototype, 'error', function() {
+    return this._fileName ? this._error: this._realReader.error;
+});
+
+utils.defineGetter(FileReader.prototype, 'result', function() {
+    return this._fileName ? this._result: this._realReader.result;
+});
+
+function defineEvent(eventName) {
+    utils.defineGetterSetter(FileReader.prototype, eventName, function() {
+        return this._realReader[eventName] || null;
+    }, function(value) {
+        this._realReader[eventName] = value;
+    });
+}
+defineEvent('onloadstart');    // When the read starts.
+defineEvent('onprogress');     // While reading (and decoding) file or fileBlob data, and reporting partial file data (progress.loaded/progress.total)
+defineEvent('onload');         // When the read has successfully completed.
+defineEvent('onerror');        // When the read has failed (see errors).
+defineEvent('onloadend');      // When the request has completed (either in success or failure).
+defineEvent('onabort');        // When the read has been aborted. For instance, by invoking the abort() method.
+
+function initRead(reader, file) {
+    // Already loading something
+    if (reader.readyState == FileReader.LOADING) {
+      throw new FileError(FileError.INVALID_STATE_ERR);
+    }
+
+    reader._result = null;
+    reader._error = null;
+    reader._readyState = FileReader.LOADING;
+
+    if (typeof file.fullPath == 'string') {
+        reader._fileName = file.fullPath;
+    } else {
+        reader._fileName = '';
+        return true;
+    }
+
+    reader.onloadstart && reader.onloadstart(new ProgressEvent("loadstart", {target:reader}));
+}
+
+/**
+ * Abort reading file.
+ */
+FileReader.prototype.abort = function() {
+    if (origFileReader && !this._fileName) {
+        return this._realReader.abort();
+    }
+    this._result = null;
+
+    if (this._readyState == FileReader.DONE || this._readyState == FileReader.EMPTY) {
+      return;
+    }
+
+    this._readyState = FileReader.DONE;
+
+    // If abort callback
+    if (typeof this.onabort === 'function') {
+        this.onabort(new ProgressEvent('abort', {target:this}));
+    }
+    // If load end callback
+    if (typeof this.onloadend === 'function') {
+        this.onloadend(new ProgressEvent('loadend', {target:this}));
+    }
+};
+
+/**
+ * Read text file.
+ *
+ * @param file          {File} File object containing file properties
+ * @param encoding      [Optional] (see http://www.iana.org/assignments/character-sets)
+ */
+FileReader.prototype.readAsText = function(file, encoding) {
+    if (initRead(this, file)) {
+        return this._realReader.readAsText(file, encoding);
+    }
+
+    // Default encoding is UTF-8
+    var enc = encoding ? encoding : "UTF-8";
+    var me = this;
+    var execArgs = [this._fileName, enc, file.start, file.end];
+
+    // Read file
+    exec(
+        // Success callback
+        function(r) {
+            // If DONE (cancelled), then don't do anything
+            if (me._readyState === FileReader.DONE) {
+                return;
+            }
+
+            // Save result
+            me._result = r;
+
+            // If onload callback
+            if (typeof me.onload === "function") {
+                me.onload(new ProgressEvent("load", {target:me}));
+            }
+
+            // DONE state
+            me._readyState = FileReader.DONE;
+
+            // If onloadend callback
+            if (typeof me.onloadend === "function") {
+                me.onloadend(new ProgressEvent("loadend", {target:me}));
+            }
+        },
+        // Error callback
+        function(e) {
+            // If DONE (cancelled), then don't do anything
+            if (me._readyState === FileReader.DONE) {
+                return;
+            }
+
+            // DONE state
+            me._readyState = FileReader.DONE;
+
+            // null result
+            me._result = null;
+
+            // Save error
+            me._error = new FileError(e);
+
+            // If onerror callback
+            if (typeof me.onerror === "function") {
+                me.onerror(new ProgressEvent("error", {target:me}));
+            }
+
+            // If onloadend callback
+            if (typeof me.onloadend === "function") {
+                me.onloadend(new ProgressEvent("loadend", {target:me}));
+            }
+        }, "File", "readAsText", execArgs);
+};
+
+
+/**
+ * Read file and return data as a base64 encoded data url.
+ * A data url is of the form:
+ *      data:[<mediatype>][;base64],<data>
+ *
+ * @param file          {File} File object containing file properties
+ */
+FileReader.prototype.readAsDataURL = function(file) {
+    if (initRead(this, file)) {
+        return this._realReader.readAsDataURL(file);
+    }
+
+    var me = this;
+    var execArgs = [this._fileName, file.start, file.end];
+
+    // Read file
+    exec(
+        // Success callback
+        function(r) {
+            // If DONE (cancelled), then don't do anything
+            if (me._readyState === FileReader.DONE) {
+                return;
+            }
+
+            // DONE state
+            me._readyState = FileReader.DONE;
+
+            // Save result
+            me._result = r;
+
+            // If onload callback
+            if (typeof me.onload === "function") {
+                me.onload(new ProgressEvent("load", {target:me}));
+            }
+
+            // If onloadend callback
+            if (typeof me.onloadend === "function") {
+                me.onloadend(new ProgressEvent("loadend", {target:me}));
+            }
+        },
+        // Error callback
+        function(e) {
+            // If DONE (cancelled), then don't do anything
+            if (me._readyState === FileReader.DONE) {
+                return;
+            }
+
+            // DONE state
+            me._readyState = FileReader.DONE;
+
+            me._result = null;
+
+            // Save error
+            me._error = new FileError(e);
+
+            // If onerror callback
+            if (typeof me.onerror === "function") {
+                me.onerror(new ProgressEvent("error", {target:me}));
+            }
+
+            // If onloadend callback
+            if (typeof me.onloadend === "function") {
+                me.onloadend(new ProgressEvent("loadend", {target:me}));
+            }
+        }, "File", "readAsDataURL", execArgs);
+};
+
+/**
+ * Read file and return data as a binary data.
+ *
+ * @param file          {File} File object containing file properties
+ */
+FileReader.prototype.readAsBinaryString = function(file) {
+    if (initRead(this, file)) {
+        return this._realReader.readAsBinaryString(file);
+    }
+
+    var me = this;
+    var execArgs = [this._fileName, file.start, file.end];
+
+    // Read file
+    exec(
+        // Success callback
+        function(r) {
+            // If DONE (cancelled), then don't do anything
+            if (me._readyState === FileReader.DONE) {
+                return;
+            }
+
+            // DONE state
+            me._readyState = FileReader.DONE;
+
+            me._result = r;
+
+            // If onload callback
+            if (typeof me.onload === "function") {
+                me.onload(new ProgressEvent("load", {target:me}));
+            }
+
+            // If onloadend callback
+            if (typeof me.onloadend === "function") {
+                me.onloadend(new ProgressEvent("loadend", {target:me}));
+            }
+        },
+        // Error callback
+        function(e) {
+            // If DONE (cancelled), then don't do anything
+            if (me._readyState === FileReader.DONE) {
+                return;
+            }
+
+            // DONE state
+            me._readyState = FileReader.DONE;
+
+            me._result = null;
+
+            // Save error
+            me._error = new FileError(e);
+
+            // If onerror callback
+            if (typeof me.onerror === "function") {
+                me.onerror(new ProgressEvent("error", {target:me}));
+            }
+
+            // If onloadend callback
+            if (typeof me.onloadend === "function") {
+                me.onloadend(new ProgressEvent("loadend", {target:me}));
+            }
+        }, "File", "readAsBinaryString", execArgs);
+};
+
+/**
+ * Read file and return data as a binary data.
+ *
+ * @param file          {File} File object containing file properties
+ */
+FileReader.prototype.readAsArrayBuffer = function(file) {
+    if (initRead(this, file)) {
+        return this._realReader.readAsArrayBuffer(file);
+    }
+
+    var me = this;
+    var execArgs = [this._fileName, file.start, file.end];
+
+    // Read file
+    exec(
+        // Success callback
+        function(r) {
+            // If DONE (cancelled), then don't do anything
+            if (me._readyState === FileReader.DONE) {
+                return;
+            }
+
+            // DONE state
+            me._readyState = FileReader.DONE;
+
+            me._result = r;
+
+            // If onload callback
+            if (typeof me.onload === "function") {
+                me.onload(new ProgressEvent("load", {target:me}));
+            }
+
+            // If onloadend callback
+            if (typeof me.onloadend === "function") {
+                me.onloadend(new ProgressEvent("loadend", {target:me}));
+            }
+        },
+        // Error callback
+        function(e) {
+            // If DONE (cancelled), then don't do anything
+            if (me._readyState === FileReader.DONE) {
+                return;
+            }
+
+            // DONE state
+            me._readyState = FileReader.DONE;
+
+            me._result = null;
+
+            // Save error
+            me._error = new FileError(e);
+
+            // If onerror callback
+            if (typeof me.onerror === "function") {
+                me.onerror(new ProgressEvent("error", {target:me}));
+            }
+
+            // If onloadend callback
+            if (typeof me.onloadend === "function") {
+                me.onloadend(new ProgressEvent("loadend", {target:me}));
+            }
+        }, "File", "readAsArrayBuffer", execArgs);
+};
+
+module.exports = FileReader;
+
+});
+
+// file: lib/common/plugin/FileSystem.js
+define("cordova/plugin/FileSystem", function(require, exports, module) {
+
+var DirectoryEntry = require('cordova/plugin/DirectoryEntry');
+
+/**
+ * An interface representing a file system
+ *
+ * @constructor
+ * {DOMString} name the unique name of the file system (readonly)
+ * {DirectoryEntry} root directory of the file system (readonly)
+ */
+var FileSystem = function(name, root) {
+    this.name = name || null;
+    if (root) {
+        this.root = new DirectoryEntry(root.name, root.fullPath);
+    }
+};
+
+module.exports = FileSystem;
+
+});
+
+// file: lib/common/plugin/FileTransfer.js
+define("cordova/plugin/FileTransfer", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    exec = require('cordova/exec'),
+    FileTransferError = require('cordova/plugin/FileTransferError'),
+    ProgressEvent = require('cordova/plugin/ProgressEvent');
+
+function newProgressEvent(result) {
+    var pe = new ProgressEvent();
+    pe.lengthComputable = result.lengthComputable;
+    pe.loaded = result.loaded;
+    pe.total = result.total;
+    return pe;
+}
+
+function getBasicAuthHeader(urlString) {
+    var header =  null;
+
+    if (window.btoa) {
+        // parse the url using the Location object
+        var url = document.createElement('a');
+        url.href = urlString;
+
+        var credentials = null;
+        var protocol = url.protocol + "//";
+        var origin = protocol + url.host;
+
+        // check whether there are the username:password credentials in the url
+        if (url.href.indexOf(origin) !== 0) { // credentials found
+            var atIndex = url.href.indexOf("@");
+            credentials = url.href.substring(protocol.length, atIndex);
+        }
+
+        if (credentials) {
+            var authHeader = "Authorization";
+            var authHeaderValue = "Basic " + window.btoa(credentials);
+
+            header = {
+                name : authHeader,
+                value : authHeaderValue
+            };
+        }
+    }
+
+    return header;
+}
+
+var idCounter = 0;
+
+/**
+ * FileTransfer uploads a file to a remote server.
+ * @constructor
+ */
+var FileTransfer = function() {
+    this._id = ++idCounter;
+    this.onprogress = null; // optional callback
+};
+
+/**
+* Given an absolute file path, uploads a file on the device to a remote server
+* using a multipart HTTP request.
+* @param filePath {String}           Full path of the file on the device
+* @param server {String}             URL of the server to receive the file
+* @param successCallback (Function}  Callback to be invoked when upload has completed
+* @param errorCallback {Function}    Callback to be invoked upon error
+* @param options {FileUploadOptions} Optional parameters such as file name and mimetype
+* @param trustAllHosts {Boolean} Optional trust all hosts (e.g. for self-signed certs), defaults to false
+*/
+FileTransfer.prototype.upload = function(filePath, server, successCallback, errorCallback, options, trustAllHosts) {
+    argscheck.checkArgs('ssFFO*', 'FileTransfer.upload', arguments);
+    // check for options
+    var fileKey = null;
+    var fileName = null;
+    var mimeType = null;
+    var params = null;
+    var chunkedMode = true;
+    var headers = null;
+    var httpMethod = null;
+    var basicAuthHeader = getBasicAuthHeader(server);
+    if (basicAuthHeader) {
+        options = options || {};
+        options.headers = options.headers || {};
+        options.headers[basicAuthHeader.name] = basicAuthHeader.value;
+    }
+
+    if (options) {
+        fileKey = options.fileKey;
+        fileName = options.fileName;
+        mimeType = options.mimeType;
+        headers = options.headers;
+        httpMethod = options.httpMethod || "POST";
+        if (httpMethod.toUpperCase() == "PUT"){
+            httpMethod = "PUT";
+        } else {
+            httpMethod = "POST";
+        }
+        if (options.chunkedMode !== null || typeof options.chunkedMode != "undefined") {
+            chunkedMode = options.chunkedMode;
+        }
+        if (options.params) {
+            params = options.params;
+        }
+        else {
+            params = {};
+        }
+    }
+
+    var fail = errorCallback && function(e) {
+        var error = new FileTransferError(e.code, e.source, e.target, e.http_status, e.body);
+        errorCallback(error);
+    };
+
+    var self = this;
+    var win = function(result) {
+        if (typeof result.lengthComputable != "undefined") {
+            if (self.onprogress) {
+                self.onprogress(newProgressEvent(result));
+            }
+        } else {
+            successCallback && successCallback(result);
+        }
+    };
+    exec(win, fail, 'FileTransfer', 'upload', [filePath, server, fileKey, fileName, mimeType, params, trustAllHosts, chunkedMode, headers, this._id, httpMethod]);
+};
+
+/**
+ * Downloads a file form a given URL and saves it to the specified directory.
+ * @param source {String}          URL of the server to receive the file
+ * @param target {String}         Full path of the file on the device
+ * @param successCallback (Function}  Callback to be invoked when upload has completed
+ * @param errorCallback {Function}    Callback to be invoked upon error
+ * @param trustAllHosts {Boolean} Optional trust all hosts (e.g. for self-signed certs), defaults to false
+ * @param options {FileDownloadOptions} Optional parameters such as headers
+ */
+FileTransfer.prototype.download = function(source, target, successCallback, errorCallback, trustAllHosts, options) {
+    argscheck.checkArgs('ssFF*', 'FileTransfer.download', arguments);
+    var self = this;
+
+    var basicAuthHeader = getBasicAuthHeader(source);
+    if (basicAuthHeader) {
+        options = options || {};
+        options.headers = options.headers || {};
+        options.headers[basicAuthHeader.name] = basicAuthHeader.value;
+    }
+
+    var headers = null;
+    if (options) {
+        headers = options.headers || null;
+    }
+
+    var win = function(result) {
+        if (typeof result.lengthComputable != "undefined") {
+            if (self.onprogress) {
+                return self.onprogress(newProgressEvent(result));
+            }
+        } else if (successCallback) {
+            var entry = null;
+            if (result.isDirectory) {
+                entry = new (require('cordova/plugin/DirectoryEntry'))();
+            }
+            else if (result.isFile) {
+                entry = new (require('cordova/plugin/FileEntry'))();
+            }
+            entry.isDirectory = result.isDirectory;
+            entry.isFile = result.isFile;
+            entry.name = result.name;
+            entry.fullPath = result.fullPath;
+            successCallback(entry);
+        }
+    };
+
+    var fail = errorCallback && function(e) {
+        var error = new FileTransferError(e.code, e.source, e.target, e.http_status, e.body);
+        errorCallback(error);
+    };
+
+    exec(win, fail, 'FileTransfer', 'download', [source, target, trustAllHosts, this._id, headers]);
+};
+
+/**
+ * Aborts the ongoing file transfer on this object. The original error
+ * callback for the file transfer will be called if necessary.
+ */
+FileTransfer.prototype.abort = function() {
+    exec(null, null, 'FileTransfer', 'abort', [this._id]);
+};
+
+module.exports = FileTransfer;
+
+});
+
+// file: lib/common/plugin/FileTransferError.js
+define("cordova/plugin/FileTransferError", function(require, exports, module) {
+
+/**
+ * FileTransferError
+ * @constructor
+ */
+var FileTransferError = function(code, source, target, status, body) {
+    this.code = code || null;
+    this.source = source || null;
+    this.target = target || null;
+    this.http_status = status || null;
+    this.body = body || null;
+};
+
+FileTransferError.FILE_NOT_FOUND_ERR = 1;
+FileTransferError.INVALID_URL_ERR = 2;
+FileTransferError.CONNECTION_ERR = 3;
+FileTransferError.ABORT_ERR = 4;
+
+module.exports = FileTransferError;
+
+});
+
+// file: lib/common/plugin/FileUploadOptions.js
+define("cordova/plugin/FileUploadOptions", function(require, exports, module) {
+
+/**
+ * Options to customize the HTTP request used to upload files.
+ * @constructor
+ * @param fileKey {String}   Name of file request parameter.
+ * @param fileName {String}  Filename to be used by the server. Defaults to image.jpg.
+ * @param mimeType {String}  Mimetype of the uploaded file. Defaults to image/jpeg.
+ * @param params {Object}    Object with key: value params to send to the server.
+ * @param headers {Object}   Keys are header names, values are header values. Multiple
+ *                           headers of the same name are not supported.
+ */
+var FileUploadOptions = function(fileKey, fileName, mimeType, params, headers, httpMethod) {
+    this.fileKey = fileKey || null;
+    this.fileName = fileName || null;
+    this.mimeType = mimeType || null;
+    this.params = params || null;
+    this.headers = headers || null;
+    this.httpMethod = httpMethod || null;
+};
+
+module.exports = FileUploadOptions;
+
+});
+
+// file: lib/common/plugin/FileUploadResult.js
+define("cordova/plugin/FileUploadResult", function(require, exports, module) {
+
+/**
+ * FileUploadResult
+ * @constructor
+ */
+var FileUploadResult = function() {
+    this.bytesSent = 0;
+    this.responseCode = null;
+    this.response = null;
+};
+
+module.exports = FileUploadResult;
+
+});
+
+// file: lib/common/plugin/FileWriter.js
+define("cordova/plugin/FileWriter", function(require, exports, module) {
+
+var exec = require('cordova/exec'),
+    FileError = require('cordova/plugin/FileError'),
+    ProgressEvent = require('cordova/plugin/ProgressEvent');
+
+/**
+ * This class writes to the mobile device file system.
+ *
+ * For Android:
+ *      The root directory is the root of the file system.
+ *      To write to the SD card, the file name is "sdcard/my_file.txt"
+ *
+ * @constructor
+ * @param file {File} File object containing file properties
+ * @param append if true write to the end of the file, otherwise overwrite the file
+ */
+var FileWriter = function(file) {
+    this.fileName = "";
+    this.length = 0;
+    if (file) {
+        this.fileName = file.fullPath || file;
+        this.length = file.size || 0;
+    }
+    // default is to write at the beginning of the file
+    this.position = 0;
+
+    this.readyState = 0; // EMPTY
+
+    this.result = null;
+
+    // Error
+    this.error = null;
+
+    // Event handlers
+    this.onwritestart = null;   // When writing starts
+    this.onprogress = null;     // While writing the file, and reporting partial file data
+    this.onwrite = null;        // When the write has successfully completed.
+    this.onwriteend = null;     // When the request has completed (either in success or failure).
+    this.onabort = null;        // When the write has been aborted. For instance, by invoking the abort() method.
+    this.onerror = null;        // When the write has failed (see errors).
+};
+
+// States
+FileWriter.INIT = 0;
+FileWriter.WRITING = 1;
+FileWriter.DONE = 2;
+
+/**
+ * Abort writing file.
+ */
+FileWriter.prototype.abort = function() {
+    // check for invalid state
+    if (this.readyState === FileWriter.DONE || this.readyState === FileWriter.INIT) {
+        throw new FileError(FileError.INVALID_STATE_ERR);
+    }
+
+    // set error
+    this.error = new FileError(FileError.ABORT_ERR);
+
+    this.readyState = FileWriter.DONE;
+
+    // If abort callback
+    if (typeof this.onabort === "function") {
+        this.onabort(new ProgressEvent("abort", {"target":this}));
+    }
+
+    // If write end callback
+    if (typeof this.onwriteend === "function") {
+        this.onwriteend(new ProgressEvent("writeend", {"target":this}));
+    }
+};
+
+/**
+ * Writes data to the file
+ *
+ * @param text to be written
+ */
+FileWriter.prototype.write = function(text) {
+    // Throw an exception if we are already writing a file
+    if (this.readyState === FileWriter.WRITING) {
+        throw new FileError(FileError.INVALID_STATE_ERR);
+    }
+
+    // WRITING state
+    this.readyState = FileWriter.WRITING;
+
+    var me = this;
+
+    // If onwritestart callback
+    if (typeof me.onwritestart === "function") {
+        me.onwritestart(new ProgressEvent("writestart", {"target":me}));
+    }
+
+    // Write file
+    exec(
+        // Success callback
+        function(r) {
+            // If DONE (cancelled), then don't do anything
+            if (me.readyState === FileWriter.DONE) {
+                return;
+            }
+
+            // position always increases by bytes written because file would be extended
+            me.position += r;
+            // The length of the file is now where we are done writing.
+
+            me.length = me.position;
+
+            // DONE state
+            me.readyState = FileWriter.DONE;
+
+            // If onwrite callback
+            if (typeof me.onwrite === "function") {
+                me.onwrite(new ProgressEvent("write", {"target":me}));
+            }
+
+            // If onwriteend callback
+            if (typeof me.onwriteend === "function") {
+                me.onwriteend(new ProgressEvent("writeend", {"target":me}));
+            }
+        },
+        // Error callback
+        function(e) {
+            // If DONE (cancelled), then don't do anything
+            if (me.readyState === FileWriter.DONE) {
+                return;
+            }
+
+            // DONE state
+            me.readyState = FileWriter.DONE;
+
+            // Save error
+            me.error = new FileError(e);
+
+            // If onerror callback
+            if (typeof me.onerror === "function") {
+                me.onerror(new ProgressEvent("error", {"target":me}));
+            }
+
+            // If onwriteend callback
+            if (typeof me.onwriteend === "function") {
+                me.onwriteend(new ProgressEvent("writeend", {"target":me}));
+            }
+        }, "File", "write", [this.fileName, text, this.position]);
+};
+
+/**
+ * Moves the file pointer to the location specified.
+ *
+ * If the offset is a negative number the position of the file
+ * pointer is rewound.  If the offset is greater than the file
+ * size the position is set to the end of the file.
+ *
+ * @param offset is the location to move the file pointer to.
+ */
+FileWriter.prototype.seek = function(offset) {
+    // Throw an exception if we are already writing a file
+    if (this.readyState === FileWriter.WRITING) {
+        throw new FileError(FileError.INVALID_STATE_ERR);
+    }
+
+    if (!offset && offset !== 0) {
+        return;
+    }
+
+    // See back from end of file.
+    if (offset < 0) {
+        this.position = Math.max(offset + this.length, 0);
+    }
+    // Offset is bigger than file size so set position
+    // to the end of the file.
+    else if (offset > this.length) {
+        this.position = this.length;
+    }
+    // Offset is between 0 and file size so set the position
+    // to start writing.
+    else {
+        this.position = offset;
+    }
+};
+
+/**
+ * Truncates the file to the size specified.
+ *
+ * @param size to chop the file at.
+ */
+FileWriter.prototype.truncate = function(size) {
+    // Throw an exception if we are already writing a file
+    if (this.readyState === FileWriter.WRITING) {
+        throw new FileError(FileError.INVALID_STATE_ERR);
+    }
+
+    // WRITING state
+    this.readyState = FileWriter.WRITING;
+
+    var me = this;
+
+    // If onwritestart callback
+    if (typeof me.onwritestart === "function") {
+        me.onwritestart(new ProgressEvent("writestart", {"target":this}));
+    }
+
+    // Write file
+    exec(
+        // Success callback
+        function(r) {
+            // If DONE (cancelled), then don't do anything
+            if (me.readyState === FileWriter.DONE) {
+                return;
+            }
+
+            // DONE state
+            me.readyState = FileWriter.DONE;
+
+            // Update the length of the file
+            me.length = r;
+            me.position = Math.min(me.position, r);
+
+            // If onwrite callback
+            if (typeof me.onwrite === "function") {
+                me.onwrite(new ProgressEvent("write", {"target":me}));
+            }
+
+            // If onwriteend callback
+            if (typeof me.onwriteend === "function") {
+                me.onwriteend(new ProgressEvent("writeend", {"target":me}));
+            }
+        },
+        // Error callback
+        function(e) {
+            // If DONE (cancelled), then don't do anything
+            if (me.readyState === FileWriter.DONE) {
+                return;
+            }
+
+            // DONE state
+            me.readyState = FileWriter.DONE;
+
+            // Save error
+            me.error = new FileError(e);
+
+            // If onerror callback
+            if (typeof me.onerror === "function") {
+                me.onerror(new ProgressEvent("error", {"target":me}));
+            }
+
+            // If onwriteend callback
+            if (typeof me.onwriteend === "function") {
+                me.onwriteend(new ProgressEvent("writeend", {"target":me}));
+            }
+        }, "File", "truncate", [this.fileName, size]);
+};
+
+module.exports = FileWriter;
+
+});
+
+// file: lib/common/plugin/Flags.js
+define("cordova/plugin/Flags", function(require, exports, module) {
+
+/**
+ * Supplies arguments to methods that lookup or create files and directories.
+ *
+ * @param create
+ *            {boolean} file or directory if it doesn't exist
+ * @param exclusive
+ *            {boolean} used with create; if true the command will fail if
+ *            target path exists
+ */
+function Flags(create, exclusive) {
+    this.create = create || false;
+    this.exclusive = exclusive || false;
+}
+
+module.exports = Flags;
+
+});
+
+// file: lib/common/plugin/GlobalizationError.js
+define("cordova/plugin/GlobalizationError", function(require, exports, module) {
+
+
+/**
+ * Globalization error object
+ *
+ * @constructor
+ * @param code
+ * @param message
+ */
+var GlobalizationError = function(code, message) {
+    this.code = code || null;
+    this.message = message || '';
+};
+
+// Globalization error codes
+GlobalizationError.UNKNOWN_ERROR = 0;
+GlobalizationError.FORMATTING_ERROR = 1;
+GlobalizationError.PARSING_ERROR = 2;
+GlobalizationError.PATTERN_ERROR = 3;
+
+module.exports = GlobalizationError;
+
+});
+
+// file: lib/common/plugin/InAppBrowser.js
+define("cordova/plugin/InAppBrowser", function(require, exports, module) {
+
+var exec = require('cordova/exec');
+var channel = require('cordova/channel');
+var modulemapper = require('cordova/modulemapper');
+
+function InAppBrowser() {
+   this.channels = {
+        'loadstart': channel.create('loadstart'),
+        'loadstop' : channel.create('loadstop'),
+        'loaderror' : channel.create('loaderror'),
+        'exit' : channel.create('exit')
+   };
+}
+
+InAppBrowser.prototype = {
+    _eventHandler: function (event) {
+        if (event.type in this.channels) {
+            this.channels[event.type].fire(event);
+        }
+    },
+    close: function (eventname) {
+        exec(null, null, "InAppBrowser", "close", []);
+    },
+    addEventListener: function (eventname,f) {
+        if (eventname in this.channels) {
+            this.channels[eventname].subscribe(f);
+        }
+    },
+    removeEventListener: function(eventname, f) {
+        if (eventname in this.channels) {
+            this.channels[eventname].unsubscribe(f);
+        }
+    },
+
+    executeScript: function(injectDetails, cb) {
+        if (injectDetails.code) {
+            exec(cb, null, "InAppBrowser", "injectScriptCode", [injectDetails.code, !!cb]);
+        } else if (injectDetails.file) {
+            exec(cb, null, "InAppBrowser", "injectScriptFile", [injectDetails.file, !!cb]);
+        } else {
+            throw new Error('executeScript requires exactly one of code or file to be specified');
+        }
+    },
+
+    insertCSS: function(injectDetails, cb) {
+        if (injectDetails.code) {
+            exec(cb, null, "InAppBrowser", "injectStyleCode", [injectDetails.code, !!cb]);
+        } else if (injectDetails.file) {
+            exec(cb, null, "InAppBrowser", "injectStyleFile", [injectDetails.file, !!cb]);
+        } else {
+            throw new Error('insertCSS requires exactly one of code or file to be specified');
+        }
+    }
+};
+
+module.exports = function(strUrl, strWindowName, strWindowFeatures) {
+    var iab = new InAppBrowser();
+    var cb = function(eventname) {
+       iab._eventHandler(eventname);
+    };
+
+    // Don't catch calls that write to existing frames (e.g. named iframes).
+    if (window.frames && window.frames[strWindowName]) {
+        var origOpenFunc = modulemapper.getOriginalSymbol(window, 'open');
+        return origOpenFunc.apply(window, arguments);
+    }
+
+    exec(cb, cb, "InAppBrowser", "open", [strUrl, strWindowName, strWindowFeatures]);
+    return iab;
+};
+
+
+});
+
+// file: lib/common/plugin/LocalFileSystem.js
+define("cordova/plugin/LocalFileSystem", function(require, exports, module) {
+
+var exec = require('cordova/exec');
+
+/**
+ * Represents a local file system.
+ */
+var LocalFileSystem = function() {
+
+};
+
+LocalFileSystem.TEMPORARY = 0; //temporary, with no guarantee of persistence
+LocalFileSystem.PERSISTENT = 1; //persistent
+
+module.exports = LocalFileSystem;
+
+});
+
+// file: lib/common/plugin/Media.js
+define("cordova/plugin/Media", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    utils = require('cordova/utils'),
+    exec = require('cordova/exec');
+
+var mediaObjects = {};
+
+/**
+ * This class provides access to the device media, interfaces to both sound and video
+ *
+ * @constructor
+ * @param src                   The file name or url to play
+ * @param successCallback       The callback to be called when the file is done playing or recording.
+ *                                  successCallback()
+ * @param errorCallback         The callback to be called if there is an error.
+ *                                  errorCallback(int errorCode) - OPTIONAL
+ * @param statusCallback        The callback to be called when media status has changed.
+ *                                  statusCallback(int statusCode) - OPTIONAL
+ */
+var Media = function(src, successCallback, errorCallback, statusCallback) {
+    argscheck.checkArgs('SFFF', 'Media', arguments);
+    this.id = utils.createUUID();
+    mediaObjects[this.id] = this;
+    this.src = src;
+    this.successCallback = successCallback;
+    this.errorCallback = errorCallback;
+    this.statusCallback = statusCallback;
+    this._duration = -1;
+    this._position = -1;
+    exec(null, this.errorCallback, "Media", "create", [this.id, this.src]);
+};
+
+// Media messages
+Media.MEDIA_STATE = 1;
+Media.MEDIA_DURATION = 2;
+Media.MEDIA_POSITION = 3;
+Media.MEDIA_ERROR = 9;
+
+// Media states
+Media.MEDIA_NONE = 0;
+Media.MEDIA_STARTING = 1;
+Media.MEDIA_RUNNING = 2;
+Media.MEDIA_PAUSED = 3;
+Media.MEDIA_STOPPED = 4;
+Media.MEDIA_MSG = ["None", "Starting", "Running", "Paused", "Stopped"];
+
+// "static" function to return existing objs.
+Media.get = function(id) {
+    return mediaObjects[id];
+};
+
+/**
+ * Start or resume playing audio file.
+ */
+Media.prototype.play = function(options) {
+    exec(null, null, "Media", "startPlayingAudio", [this.id, this.src, options]);
+};
+
+/**
+ * Stop playing audio file.
+ */
+Media.prototype.stop = function() {
+    var me = this;
+    exec(function() {
+        me._position = 0;
+    }, this.errorCallback, "Media", "stopPlayingAudio", [this.id]);
+};
+
+/**
+ * Seek or jump to a new time in the track..
+ */
+Media.prototype.seekTo = function(milliseconds) {
+    var me = this;
+    exec(function(p) {
+        me._position = p;
+    }, this.errorCallback, "Media", "seekToAudio", [this.id, milliseconds]);
+};
+
+/**
+ * Pause playing audio file.
+ */
+Media.prototype.pause = function() {
+    exec(null, this.errorCallback, "Media", "pausePlayingAudio", [this.id]);
+};
+
+/**
+ * Get duration of an audio file.
+ * The duration is only set for audio that is playing, paused or stopped.
+ *
+ * @return      duration or -1 if not known.
+ */
+Media.prototype.getDuration = function() {
+    return this._duration;
+};
+
+/**
+ * Get position of audio.
+ */
+Media.prototype.getCurrentPosition = function(success, fail) {
+    var me = this;
+    exec(function(p) {
+        me._position = p;
+        success(p);
+    }, fail, "Media", "getCurrentPositionAudio", [this.id]);
+};
+
+/**
+ * Start recording audio file.
+ */
+Media.prototype.startRecord = function() {
+    exec(null, this.errorCallback, "Media", "startRecordingAudio", [this.id, this.src]);
+};
+
+/**
+ * Stop recording audio file.
+ */
+Media.prototype.stopRecord = function() {
+    exec(null, this.errorCallback, "Media", "stopRecordingAudio", [this.id]);
+};
+
+/**
+ * Release the resources.
+ */
+Media.prototype.release = function() {
+    exec(null, this.errorCallback, "Media", "release", [this.id]);
+};
+
+/**
+ * Adjust the volume.
+ */
+Media.prototype.setVolume = function(volume) {
+    exec(null, null, "Media", "setVolume", [this.id, volume]);
+};
+
+/**
+ * Audio has status update.
+ * PRIVATE
+ *
+ * @param id            The media object id (string)
+ * @param msgType       The 'type' of update this is
+ * @param value         Use of value is determined by the msgType
+ */
+Media.onStatus = function(id, msgType, value) {
+
+    var media = mediaObjects[id];
+
+    if(media) {
+        switch(msgType) {
+            case Media.MEDIA_STATE :
+                media.statusCallback && media.statusCallback(value);
+                if(value == Media.MEDIA_STOPPED) {
+                    media.successCallback && media.successCallback();
+                }
+                break;
+            case Media.MEDIA_DURATION :
+                media._duration = value;
+                break;
+            case Media.MEDIA_ERROR :
+                media.errorCallback && media.errorCallback(value);
+                break;
+            case Media.MEDIA_POSITION :
+                media._position = Number(value);
+                break;
+            default :
+                console.error && console.error("Unhandled Media.onStatus :: " + msgType);
+                break;
+        }
+    }
+    else {
+         console.error && console.error("Received Media.onStatus callback for unknown media :: " + id);
+    }
+
+};
+
+module.exports = Media;
+
+});
+
+// file: lib/common/plugin/MediaError.js
+define("cordova/plugin/MediaError", function(require, exports, module) {
+
+/**
+ * This class contains information about any Media errors.
+*/
+/*
+ According to :: http://dev.w3.org/html5/spec-author-view/video.html#mediaerror
+ We should never be creating these objects, we should just implement the interface
+ which has 1 property for an instance, 'code'
+
+ instead of doing :
+    errorCallbackFunction( new MediaError(3,'msg') );
+we should simply use a literal :
+    errorCallbackFunction( {'code':3} );
+ */
+
+ var _MediaError = window.MediaError;
+
+
+if(!_MediaError) {
+    window.MediaError = _MediaError = function(code, msg) {
+        this.code = (typeof code != 'undefined') ? code : null;
+        this.message = msg || ""; // message is NON-standard! do not use!
+    };
+}
+
+_MediaError.MEDIA_ERR_NONE_ACTIVE    = _MediaError.MEDIA_ERR_NONE_ACTIVE    || 0;
+_MediaError.MEDIA_ERR_ABORTED        = _MediaError.MEDIA_ERR_ABORTED        || 1;
+_MediaError.MEDIA_ERR_NETWORK        = _MediaError.MEDIA_ERR_NETWORK        || 2;
+_MediaError.MEDIA_ERR_DECODE         = _MediaError.MEDIA_ERR_DECODE         || 3;
+_MediaError.MEDIA_ERR_NONE_SUPPORTED = _MediaError.MEDIA_ERR_NONE_SUPPORTED || 4;
+// TODO: MediaError.MEDIA_ERR_NONE_SUPPORTED is legacy, the W3 spec now defines it as below.
+// as defined by http://dev.w3.org/html5/spec-author-view/video.html#error-codes
+_MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = _MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED || 4;
+
+module.exports = _MediaError;
+
+});
+
+// file: lib/common/plugin/MediaFile.js
+define("cordova/plugin/MediaFile", function(require, exports, module) {
+
+var utils = require('cordova/utils'),
+    exec = require('cordova/exec'),
+    File = require('cordova/plugin/File'),
+    CaptureError = require('cordova/plugin/CaptureError');
+/**
+ * Represents a single file.
+ *
+ * name {DOMString} name of the file, without path information
+ * fullPath {DOMString} the full path of the file, including the name
+ * type {DOMString} mime type
+ * lastModifiedDate {Date} last modified date
+ * size {Number} size of the file in bytes
+ */
+var MediaFile = function(name, fullPath, type, lastModifiedDate, size){
+    MediaFile.__super__.constructor.apply(this, arguments);
+};
+
+utils.extend(MediaFile, File);
+
+/**
+ * Request capture format data for a specific file and type
+ *
+ * @param {Function} successCB
+ * @param {Function} errorCB
+ */
+MediaFile.prototype.getFormatData = function(successCallback, errorCallback) {
+    if (typeof this.fullPath === "undefined" || this.fullPath === null) {
+        errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT));
+    } else {
+        exec(successCallback, errorCallback, "Capture", "getFormatData", [this.fullPath, this.type]);
+    }
+};
+
+module.exports = MediaFile;
+
+});
+
+// file: lib/common/plugin/MediaFileData.js
+define("cordova/plugin/MediaFileData", function(require, exports, module) {
+
+/**
+ * MediaFileData encapsulates format information of a media file.
+ *
+ * @param {DOMString} codecs
+ * @param {long} bitrate
+ * @param {long} height
+ * @param {long} width
+ * @param {float} duration
+ */
+var MediaFileData = function(codecs, bitrate, height, width, duration){
+    this.codecs = codecs || null;
+    this.bitrate = bitrate || 0;
+    this.height = height || 0;
+    this.width = width || 0;
+    this.duration = duration || 0;
+};
+
+module.exports = MediaFileData;
+
+});
+
+// file: lib/common/plugin/Metadata.js
+define("cordova/plugin/Metadata", function(require, exports, module) {
+
+/**
+ * Information about the state of the file or directory
+ *
+ * {Date} modificationTime (readonly)
+ */
+var Metadata = function(time) {
+    this.modificationTime = (typeof time != 'undefined'?new Date(time):null);
+};
+
+module.exports = Metadata;
+
+});
+
+// file: lib/common/plugin/Position.js
+define("cordova/plugin/Position", function(require, exports, module) {
+
+var Coordinates = require('cordova/plugin/Coordinates');
+
+var Position = function(coords, timestamp) {
+    if (coords) {
+        this.coords = new Coordinates(coords.latitude, coords.longitude, coords.altitude, coords.accuracy, coords.heading, coords.velocity, coords.altitudeAccuracy);
+    } else {
+        this.coords = new Coordinates();
+    }
+    this.timestamp = (timestamp !== undefined) ? timestamp : new Date();
+};
+
+module.exports = Position;
+
+});
+
+// file: lib/common/plugin/PositionError.js
+define("cordova/plugin/PositionError", function(require, exports, module) {
+
+/**
+ * Position error object
+ *
+ * @constructor
+ * @param code
+ * @param message
+ */
+var PositionError = function(code, message) {
+    this.code = code || null;
+    this.message = message || '';
+};
+
+PositionError.PERMISSION_DENIED = 1;
+PositionError.POSITION_UNAVAILABLE = 2;
+PositionError.TIMEOUT = 3;
+
+module.exports = PositionError;
+
+});
+
+// file: lib/common/plugin/ProgressEvent.js
+define("cordova/plugin/ProgressEvent", function(require, exports, module) {
+
+// If ProgressEvent exists in global context, use it already, otherwise use our own polyfill
+// Feature test: See if we can instantiate a native ProgressEvent;
+// if so, use that approach,
+// otherwise fill-in with our own implementation.
+//
+// NOTE: right now we always fill in with our own. Down the road would be nice if we can use whatever is native in the webview.
+var ProgressEvent = (function() {
+    /*
+    var createEvent = function(data) {
+        var event = document.createEvent('Events');
+        event.initEvent('ProgressEvent', false, false);
+        if (data) {
+            for (var i in data) {
+                if (data.hasOwnProperty(i)) {
+                    event[i] = data[i];
+                }
+            }
+            if (data.target) {
+                // TODO: cannot call <some_custom_object>.dispatchEvent
+                // need to first figure out how to implement EventTarget
+            }
+        }
+        return event;
+    };
+    try {
+        var ev = createEvent({type:"abort",target:document});
+        return function ProgressEvent(type, data) {
+            data.type = type;
+            return createEvent(data);
+        };
+    } catch(e){
+    */
+        return function ProgressEvent(type, dict) {
+            this.type = type;
+            this.bubbles = false;
+            this.cancelBubble = false;
+            this.cancelable = false;
+            this.lengthComputable = false;
+            this.loaded = dict && dict.loaded ? dict.loaded : 0;
+            this.total = dict && dict.total ? dict.total : 0;
+            this.target = dict && dict.target ? dict.target : null;
+        };
+    //}
+})();
+
+module.exports = ProgressEvent;
+
+});
+
+// file: lib/common/plugin/accelerometer.js
+define("cordova/plugin/accelerometer", function(require, exports, module) {
+
+/**
+ * This class provides access to device accelerometer data.
+ * @constructor
+ */
+var argscheck = require('cordova/argscheck'),
+    utils = require("cordova/utils"),
+    exec = require("cordova/exec"),
+    Acceleration = require('cordova/plugin/Acceleration');
+
+// Is the accel sensor running?
+var running = false;
+
+// Keeps reference to watchAcceleration calls.
+var timers = {};
+
+// Array of listeners; used to keep track of when we should call start and stop.
+var listeners = [];
+
+// Last returned acceleration object from native
+var accel = null;
+
+// Tells native to start.
+function start() {
+    exec(function(a) {
+        var tempListeners = listeners.slice(0);
+        accel = new Acceleration(a.x, a.y, a.z, a.timestamp);
+        for (var i = 0, l = tempListeners.length; i < l; i++) {
+            tempListeners[i].win(accel);
+        }
+    }, function(e) {
+        var tempListeners = listeners.slice(0);
+        for (var i = 0, l = tempListeners.length; i < l; i++) {
+            tempListeners[i].fail(e);
+        }
+    }, "Accelerometer", "start", []);
+    running = true;
+}
+
+// Tells native to stop.
+function stop() {
+    exec(null, null, "Accelerometer", "stop", []);
+    running = false;
+}
+
+// Adds a callback pair to the listeners array
+function createCallbackPair(win, fail) {
+    return {win:win, fail:fail};
+}
+
+// Removes a win/fail listener pair from the listeners array
+function removeListeners(l) {
+    var idx = listeners.indexOf(l);
+    if (idx > -1) {
+        listeners.splice(idx, 1);
+        if (listeners.length === 0) {
+            stop();
+        }
+    }
+}
+
+var accelerometer = {
+    /**
+     * Asynchronously acquires the current acceleration.
+     *
+     * @param {Function} successCallback    The function to call when the acceleration data is available
+     * @param {Function} errorCallback      The function to call when there is an error getting the acceleration data. (OPTIONAL)
+     * @param {AccelerationOptions} options The options for getting the accelerometer data such as timeout. (OPTIONAL)
+     */
+    getCurrentAcceleration: function(successCallback, errorCallback, options) {
+        argscheck.checkArgs('fFO', 'accelerometer.getCurrentAcceleration', arguments);
+
+        var p;
+        var win = function(a) {
+            removeListeners(p);
+            successCallback(a);
+        };
+        var fail = function(e) {
+            removeListeners(p);
+            errorCallback && errorCallback(e);
+        };
+
+        p = createCallbackPair(win, fail);
+        listeners.push(p);
+
+        if (!running) {
+            start();
+        }
+    },
+
+    /**
+     * Asynchronously acquires the acceleration repeatedly at a given interval.
+     *
+     * @param {Function} successCallback    The function to call each time the acceleration data is available
+     * @param {Function} errorCallback      The function to call when there is an error getting the acceleration data. (OPTIONAL)
+     * @param {AccelerationOptions} options The options for getting the accelerometer data such as timeout. (OPTIONAL)
+     * @return String                       The watch id that must be passed to #clearWatch to stop watching.
+     */
+    watchAcceleration: function(successCallback, errorCallback, options) {
+        argscheck.checkArgs('fFO', 'accelerometer.watchAcceleration', arguments);
+        // Default interval (10 sec)
+        var frequency = (options && options.frequency && typeof options.frequency == 'number') ? options.frequency : 10000;
+
+        // Keep reference to watch id, and report accel readings as often as defined in frequency
+        var id = utils.createUUID();
+
+        var p = createCallbackPair(function(){}, function(e) {
+            removeListeners(p);
+            errorCallback && errorCallback(e);
+        });
+        listeners.push(p);
+
+        timers[id] = {
+            timer:window.setInterval(function() {
+                if (accel) {
+                    successCallback(accel);
+                }
+            }, frequency),
+            listeners:p
+        };
+
+        if (running) {
+            // If we're already running then immediately invoke the success callback
+            // but only if we have retrieved a value, sample code does not check for null ...
+            if (accel) {
+                successCallback(accel);
+            }
+        } else {
+            start();
+        }
+
+        return id;
+    },
+
+    /**
+     * Clears the specified accelerometer watch.
+     *
+     * @param {String} id       The id of the watch returned from #watchAcceleration.
+     */
+    clearWatch: function(id) {
+        // Stop javascript timer & remove from timer list
+        if (id && timers[id]) {
+            window.clearInterval(timers[id].timer);
+            removeListeners(timers[id].listeners);
+            delete timers[id];
+        }
+    }
+};
+
+module.exports = accelerometer;
+
+});
+
+// file: lib/common/plugin/accelerometer/symbols.js
+define("cordova/plugin/accelerometer/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.defaults('cordova/plugin/Acceleration', 'Acceleration');
+modulemapper.defaults('cordova/plugin/accelerometer', 'navigator.accelerometer');
+
+});
+
+// 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/android/plugin/android/device.js
+define("cordova/plugin/android/device", function(require, exports, module) {
+
+var channel = require('cordova/channel'),
+    utils = require('cordova/utils'),
+    exec = require('cordova/exec'),
+    app = require('cordova/plugin/android/app');
+
+module.exports = {
+    /*
+     * DEPRECATED
+     * This is only for Android.
+     *
+     * You must explicitly override the back button.
+     */
+    overrideBackButton:function() {
+        console.log("Device.overrideBackButton() is deprecated.  Use App.overrideBackbutton(true).");
+        app.overrideBackbutton(true);
+    },
+
+    /*
+     * DEPRECATED
+     * This is only for Android.
+     *
+     * This resets the back button to the default behavior
+     */
+    resetBackButton:function() {
+        console.log("Device.resetBackButton() is deprecated.  Use App.overrideBackbutton(false).");
+        app.overrideBackbutton(false);
+    },
+
+    /*
+     * DEPRECATED
+     * This is only for Android.
+     *
+     * This terminates the activity!
+     */
+    exitApp:function() {
+        console.log("Device.exitApp() is deprecated.  Use App.exitApp().");
+        app.exitApp();
+    }
+};
+
+});
+
+// file: lib/android/plugin/android/nativeapiprovider.js
+define("cordova/plugin/android/nativeapiprovider", function(require, exports, module) {
+
+/**
+ * Exports the ExposedJsApi.java object if available, otherwise exports the PromptBasedNativeApi.
+ */
+
+var nativeApi = this._cordovaNative || require('cordova/plugin/android/promptbasednativeapi');
+var currentApi = nativeApi;
+
+module.exports = {
+    get: function() { return currentApi; },
+    setPreferPrompt: function(value) {
+        currentApi = value ? require('cordova/plugin/android/promptbasednativeapi') : nativeApi;
+    },
+    // Used only by tests.
+    set: function(value) {
+        currentApi = value;
+    }
+};
+
+});
+
+// file: lib/android/plugin/android/notification.js
+define("cordova/plugin/android/notification", function(require, exports, module) {
+
+var exec = require('cordova/exec');
+
+/**
+ * Provides Android enhanced notification API.
+ */
+module.exports = {
+    activityStart : function(title, message) {
+        // If title and message not specified then mimic Android behavior of
+        // using default strings.
+        if (typeof title === "undefined" && typeof message == "undefined") {
+            title = "Busy";
+            message = 'Please wait...';
+        }
+
+        exec(null, null, 'Notification', 'activityStart', [ title, message ]);
+    },
+
+    /**
+     * Close an activity dialog
+     */
+    activityStop : function() {
+        exec(null, null, 'Notification', 'activityStop', []);
+    },
+
+    /**
+     * Display a progress dialog with progress bar that goes from 0 to 100.
+     *
+     * @param {String}
+     *            title Title of the progress dialog.
+     * @param {String}
+     *            message Message to display in the dialog.
+     */
+    progressStart : function(title, message) {
+        exec(null, null, 'Notification', 'progressStart', [ title, message ]);
+    },
+
+    /**
+     * Close the progress dialog.
+     */
+    progressStop : function() {
+        exec(null, null, 'Notification', 'progressStop', []);
+    },
+
+    /**
+     * Set the progress dialog value.
+     *
+     * @param {Number}
+     *            value 0-100
+     */
+    progressValue : function(value) {
+        exec(null, null, 'Notification', 'progressValue', [ value ]);
+    }
+};
+
+});
+
+// file: lib/android/plugin/android/promptbasednativeapi.js
+define("cordova/plugin/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() {
+        return prompt('', 'gap_poll:');
+    }
+};
+
+});
+
+// file: lib/android/plugin/android/storage.js
+define("cordova/plugin/android/storage", function(require, exports, module) {
+
+var utils = require('cordova/utils'),
+    exec = require('cordova/exec'),
+    channel = require('cordova/channel');
+
+var queryQueue = {};
+
+/**
+ * SQL result set object
+ * PRIVATE METHOD
+ * @constructor
+ */
+var DroidDB_Rows = function() {
+    this.resultSet = [];    // results array
+    this.length = 0;        // number of rows
+};
+
+/**
+ * Get item from SQL result set
+ *
+ * @param row           The row number to return
+ * @return              The row object
+ */
+DroidDB_Rows.prototype.item = function(row) {
+    return this.resultSet[row];
+};
+
+/**
+ * SQL result set that is returned to user.
+ * PRIVATE METHOD
+ * @constructor
+ */
+var DroidDB_Result = function() {
+    this.rows = new DroidDB_Rows();
+};
+
+/**
+ * Callback from native code when query is complete.
+ * PRIVATE METHOD
+ *
+ * @param id   Query id
+ */
+function completeQuery(id, data) {
+    var query = queryQueue[id];
+    if (query) {
+        try {
+            delete queryQueue[id];
+
+            // Get transaction
+            var tx = query.tx;
+
+            // If transaction hasn't failed
+            // Note: We ignore all query results if previous query
+            //       in the same transaction failed.
+            if (tx && tx.queryList[id]) {
+
+                // Save query results
+                var r = new DroidDB_Result();
+                r.rows.resultSet = data;
+                r.rows.length = data.length;
+                try {
+                    if (typeof query.successCallback === 'function') {
+                        query.successCallback(query.tx, r);
+                    }
+                } catch (ex) {
+                    console.log("executeSql error calling user success callback: "+ex);
+                }
+
+                tx.queryComplete(id);
+            }
+        } catch (e) {
+            console.log("executeSql error: "+e);
+        }
+    }
+}
+
+/**
+ * Callback from native code when query fails
+ * PRIVATE METHOD
+ *
+ * @param reason            Error message
+ * @param id                Query id
+ */
+function failQuery(reason, id) {
+    var query = queryQueue[id];
+    if (query) {
+        try {
+            delete queryQueue[id];
+
+            // Get transaction
+            var tx = query.tx;
+
+            // If transaction hasn't failed
+            // Note: We ignore all query results if previous query
+            //       in the same transaction failed.
+            if (tx && tx.queryList[id]) {
+                tx.queryList = {};
+
+                try {
+                    if (typeof query.errorCallback === 'function') {
+                        query.errorCallback(query.tx, reason);
+                    }
+                } catch (ex) {
+                    console.log("executeSql error calling user error callback: "+ex);
+                }
+
+                tx.queryFailed(id, reason);
+            }
+
+        } catch (e) {
+            console.log("executeSql error: "+e);
+        }
+    }
+}
+
+/**
+ * SQL query object
+ * PRIVATE METHOD
+ *
+ * @constructor
+ * @param tx                The transaction object that this query belongs to
+ */
+var DroidDB_Query = function(tx) {
+
+    // Set the id of the query
+    this.id = utils.createUUID();
+
+    // Add this query to the queue
+    queryQueue[this.id] = this;
+
+    // Init result
+    this.resultSet = [];
+
+    // Set transaction that this query belongs to
+    this.tx = tx;
+
+    // Add this query to transaction list
+    this.tx.queryList[this.id] = this;
+
+    // Callbacks
+    this.successCallback = null;
+    this.errorCallback = null;
+
+};
+
+/**
+ * Transaction object
+ * PRIVATE METHOD
+ * @constructor
+ */
+var DroidDB_Tx = function() {
+
+    // Set the id of the transaction
+    this.id = utils.createUUID();
+
+    // Callbacks
+    this.successCallback = null;
+    this.errorCallback = null;
+
+    // Query list
+    this.queryList = {};
+};
+
+/**
+ * Mark query in transaction as complete.
+ * If all queries are complete, call the user's transaction success callback.
+ *
+ * @param id                Query id
+ */
+DroidDB_Tx.prototype.queryComplete = function(id) {
+    delete this.queryList[id];
+
+    // If no more outstanding queries, then fire transaction success
+    if (this.successCallback) {
+        var count = 0;
+        var i;
+        for (i in this.queryList) {
+            if (this.queryList.hasOwnProperty(i)) {
+                count++;
+            }
+        }
+        if (count === 0) {
+            try {
+                this.successCallback();
+            } catch(e) {
+                console.log("Transaction error calling user success callback: " + e);
+            }
+        }
+    }
+};
+
+/**
+ * Mark query in transaction as failed.
+ *
+ * @param id                Query id
+ * @param reason            Error message
+ */
+DroidDB_Tx.prototype.queryFailed = function(id, reason) {
+
+    // The sql queries in this transaction have already been run, since
+    // we really don't have a real transaction implemented in native code.
+    // However, the user callbacks for the remaining sql queries in transaction
+    // will not be called.
+    this.queryList = {};
+
+    if (this.errorCallback) {
+        try {
+            this.errorCallback(reason);
+        } catch(e) {
+            console.log("Transaction error calling user error callback: " + e);
+        }
+    }
+};
+
+/**
+ * Execute SQL statement
+ *
+ * @param sql                   SQL statement to execute
+ * @param params                Statement parameters
+ * @param successCallback       Success callback
+ * @param errorCallback         Error callback
+ */
+DroidDB_Tx.prototype.executeSql = function(sql, params, successCallback, errorCallback) {
+
+    // Init params array
+    if (typeof params === 'undefined') {
+        params = [];
+    }
+
+    // Create query and add to queue
+    var query = new DroidDB_Query(this);
+    queryQueue[query.id] = query;
+
+    // Save callbacks
+    query.successCallback = successCallback;
+    query.errorCallback = errorCallback;
+
+    // Call native code
+    exec(null, null, "Storage", "executeSql", [sql, params, query.id]);
+};
+
+var DatabaseShell = function() {
+};
+
+/**
+ * Start a transaction.
+ * Does not support rollback in event of failure.
+ *
+ * @param process {Function}            The transaction function
+ * @param successCallback {Function}
+ * @param errorCallback {Function}
+ */
+DatabaseShell.prototype.transaction = function(process, errorCallback, successCallback) {
+    var tx = new DroidDB_Tx();
+    tx.successCallback = successCallback;
+    tx.errorCallback = errorCallback;
+    try {
+        process(tx);
+    } catch (e) {
+        console.log("Transaction error: "+e);
+        if (tx.errorCallback) {
+            try {
+                tx.errorCallback(e);
+            } catch (ex) {
+                console.log("Transaction error calling user error callback: "+e);
+            }
+        }
+    }
+};
+
+/**
+ * Open database
+ *
+ * @param name              Database name
+ * @param version           Database version
+ * @param display_name      Database display name
+ * @param size              Database size in bytes
+ * @return                  Database object
+ */
+var DroidDB_openDatabase = function(name, version, display_name, size) {
+    exec(null, null, "Storage", "openDatabase", [name, version, display_name, size]);
+    var db = new DatabaseShell();
+    return db;
+};
+
+
+module.exports = {
+  openDatabase:DroidDB_openDatabase,
+  failQuery:failQuery,
+  completeQuery:completeQuery
+};
+
+});
+
+// file: lib/android/plugin/android/storage/openDatabase.js
+define("cordova/plugin/android/storage/openDatabase", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper'),
+    storage = require('cordova/plugin/android/storage');
+
+var originalOpenDatabase = modulemapper.getOriginalSymbol(window, 'openDatabase');
+
+module.exports = function(name, version, desc, size) {
+    // First patch WebSQL if necessary
+    if (!originalOpenDatabase) {
+        // Not defined, create an openDatabase function for all to use!
+        return storage.openDatabase.apply(this, arguments);
+    }
+
+    // Defined, but some Android devices will throw a SECURITY_ERR -
+    // so we wrap the whole thing in a try-catch and shim in our own
+    // if the device has Android bug 16175.
+    try {
+        return originalOpenDatabase(name, version, desc, size);
+    } catch (ex) {
+        if (ex.code !== 18) {
+            throw ex;
+        }
+    }
+    return storage.openDatabase(name, version, desc, size);
+};
+
+
+
+});
+
+// file: lib/android/plugin/android/storage/symbols.js
+define("cordova/plugin/android/storage/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/android/storage/openDatabase', 'openDatabase');
+
+
+});
+
+// file: lib/common/plugin/battery.js
+define("cordova/plugin/battery", function(require, exports, module) {
+
+/**
+ * This class contains information about the current battery status.
+ * @constructor
+ */
+var cordova = require('cordova'),
+    exec = require('cordova/exec');
+
+function handlers() {
+  return battery.channels.batterystatus.numHandlers +
+         battery.channels.batterylow.numHandlers +
+         battery.channels.batterycritical.numHandlers;
+}
+
+var Battery = function() {
+    this._level = null;
+    this._isPlugged = null;
+    // Create new event handlers on the window (returns a channel instance)
+    this.channels = {
+      batterystatus:cordova.addWindowEventHandler("batterystatus"),
+      batterylow:cordova.addWindowEventHandler("batterylow"),
+      batterycritical:cordova.addWindowEventHandler("batterycritical")
+    };
+    for (var key in this.channels) {
+        this.channels[key].onHasSubscribersChange = Battery.onHasSubscribersChange;
+    }
+};
+/**
+ * Event handlers for when callbacks get registered for the battery.
+ * Keep track of how many handlers we have so we can start and stop the native battery listener
+ * appropriately (and hopefully save on battery life!).
+ */
+Battery.onHasSubscribersChange = function() {
+  // If we just registered the first handler, make sure native listener is started.
+  if (this.numHandlers === 1 && handlers() === 1) {
+      exec(battery._status, battery._error, "Battery", "start", []);
+  } else if (handlers() === 0) {
+      exec(null, null, "Battery", "stop", []);
+  }
+};
+
+/**
+ * Callback for battery status
+ *
+ * @param {Object} info            keys: level, isPlugged
+ */
+Battery.prototype._status = function(info) {
+    if (info) {
+        var me = battery;
+    var level = info.level;
+        if (me._level !== level || me._isPlugged !== info.isPlugged) {
+            // Fire batterystatus event
+            cordova.fireWindowEvent("batterystatus", info);
+
+            // Fire low battery event
+            if (level === 20 || level === 5) {
+                if (level === 20) {
+                    cordova.fireWindowEvent("batterylow", info);
+                }
+                else {
+                    cordova.fireWindowEvent("batterycritical", info);
+                }
+            }
+        }
+        me._level = level;
+        me._isPlugged = info.isPlugged;
+    }
+};
+
+/**
+ * Error callback for battery start
+ */
+Battery.prototype._error = function(e) {
+    console.log("Error initializing Battery: " + e);
+};
+
+var battery = new Battery();
+
+module.exports = battery;
+
+});
+
+// file: lib/common/plugin/battery/symbols.js
+define("cordova/plugin/battery/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.defaults('cordova/plugin/battery', 'navigator.battery');
+
+});
+
+// file: lib/common/plugin/camera/symbols.js
+define("cordova/plugin/camera/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.defaults('cordova/plugin/Camera', 'navigator.camera');
+modulemapper.defaults('cordova/plugin/CameraConstants', 'Camera');
+modulemapper.defaults('cordova/plugin/CameraPopoverOptions', 'CameraPopoverOptions');
+
+});
+
+// file: lib/common/plugin/capture.js
+define("cordova/plugin/capture", function(require, exports, module) {
+
+var exec = require('cordova/exec'),
+    MediaFile = require('cordova/plugin/MediaFile');
+
+/**
+ * Launches a capture of different types.
+ *
+ * @param (DOMString} type
+ * @param {Function} successCB
+ * @param {Function} errorCB
+ * @param {CaptureVideoOptions} options
+ */
+function _capture(type, successCallback, errorCallback, options) {
+    var win = function(pluginResult) {
+        var mediaFiles = [];
+        var i;
+        for (i = 0; i < pluginResult.length; i++) {
+            var mediaFile = new MediaFile();
+            mediaFile.name = pluginResult[i].name;
+            mediaFile.fullPath = pluginResult[i].fullPath;
+            mediaFile.type = pluginResult[i].type;
+            mediaFile.lastModifiedDate = pluginResult[i].lastModifiedDate;
+            mediaFile.size = pluginResult[i].size;
+            mediaFiles.push(mediaFile);
+        }
+        successCallback(mediaFiles);
+    };
+    exec(win, errorCallback, "Capture", type, [options]);
+}
+/**
+ * The Capture interface exposes an interface to the camera and microphone of the hosting device.
+ */
+function Capture() {
+    this.supportedAudioModes = [];
+    this.supportedImageModes = [];
+    this.supportedVideoModes = [];
+}
+
+/**
+ * Launch audio recorder application for recording audio clip(s).
+ *
+ * @param {Function} successCB
+ * @param {Function} errorCB
+ * @param {CaptureAudioOptions} options
+ */
+Capture.prototype.captureAudio = function(successCallback, errorCallback, options){
+    _capture("captureAudio", successCallback, errorCallback, options);
+};
+
+/**
+ * Launch camera application for taking image(s).
+ *
+ * @param {Function} successCB
+ * @param {Function} errorCB
+ * @param {CaptureImageOptions} options
+ */
+Capture.prototype.captureImage = function(successCallback, errorCallback, options){
+    _capture("captureImage", successCallback, errorCallback, options);
+};
+
+/**
+ * Launch device camera application for recording video(s).
+ *
+ * @param {Function} successCB
+ * @param {Function} errorCB
+ * @param {CaptureVideoOptions} options
+ */
+Capture.prototype.captureVideo = function(successCallback, errorCallback, options){
+    _capture("captureVideo", successCallback, errorCallback, options);
+};
+
+
+module.exports = new Capture();
+
+});
+
+// file: lib/common/plugin/capture/symbols.js
+define("cordova/plugin/capture/symbols", function(require, exports, module) {
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/CaptureError', 'CaptureError');
+modulemapper.clobbers('cordova/plugin/CaptureAudioOptions', 'CaptureAudioOptions');
+modulemapper.clobbers('cordova/plugin/CaptureImageOptions', 'CaptureImageOptions');
+modulemapper.clobbers('cordova/plugin/CaptureVideoOptions', 'CaptureVideoOptions');
+modulemapper.clobbers('cordova/plugin/ConfigurationData', 'ConfigurationData');
+modulemapper.clobbers('cordova/plugin/MediaFile', 'MediaFile');
+modulemapper.clobbers('cordova/plugin/MediaFileData', 'MediaFileData');
+modulemapper.clobbers('cordova/plugin/capture', 'navigator.device.capture');
+
+});
+
+// file: lib/common/plugin/compass.js
+define("cordova/plugin/compass", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    exec = require('cordova/exec'),
+    utils = require('cordova/utils'),
+    CompassHeading = require('cordova/plugin/CompassHeading'),
+    CompassError = require('cordova/plugin/CompassError'),
+    timers = {},
+    compass = {
+        /**
+         * Asynchronously acquires the current heading.
+         * @param {Function} successCallback The function to call when the heading
+         * data is available
+         * @param {Function} errorCallback The function to call when there is an error
+         * getting the heading data.
+         * @param {CompassOptions} options The options for getting the heading data (not used).
+         */
+        getCurrentHeading:function(successCallback, errorCallback, options) {
+            argscheck.checkArgs('fFO', 'compass.getCurrentHeading', arguments);
+
+            var win = function(result) {
+                var ch = new CompassHeading(result.magneticHeading, result.trueHeading, result.headingAccuracy, result.timestamp);
+                successCallback(ch);
+            };
+            var fail = errorCallback && function(code) {
+                var ce = new CompassError(code);
+                errorCallback(ce);
+            };
+
+            // Get heading
+            exec(win, fail, "Compass", "getHeading", [options]);
+        },
+
+        /**
+         * Asynchronously acquires the heading repeatedly at a given interval.
+         * @param {Function} successCallback The function to call each time the heading
+         * data is available
+         * @param {Function} errorCallback The function to call when there is an error
+         * getting the heading data.
+         * @param {HeadingOptions} options The options for getting the heading data
+         * such as timeout and the frequency of the watch. For iOS, filter parameter
+         * specifies to watch via a distance filter rather than time.
+         */
+        watchHeading:function(successCallback, errorCallback, options) {
+            argscheck.checkArgs('fFO', 'compass.watchHeading', arguments);
+            // Default interval (100 msec)
+            var frequency = (options !== undefined && options.frequency !== undefined) ? options.frequency : 100;
+            var filter = (options !== undefined && options.filter !== undefined) ? options.filter : 0;
+
+            var id = utils.createUUID();
+            if (filter > 0) {
+                // is an iOS request for watch by filter, no timer needed
+                timers[id] = "iOS";
+                compass.getCurrentHeading(successCallback, errorCallback, options);
+            } else {
+                // Start watch timer to get headings
+                timers[id] = window.setInterval(function() {
+                    compass.getCurrentHeading(successCallback, errorCallback);
+                }, frequency);
+            }
+
+            return id;
+        },
+
+        /**
+         * Clears the specified heading watch.
+         * @param {String} watchId The ID of the watch returned from #watchHeading.
+         */
+        clearWatch:function(id) {
+            // Stop javascript timer & remove from timer list
+            if (id && timers[id]) {
+                if (timers[id] != "iOS") {
+                    clearInterval(timers[id]);
+                } else {
+                    // is iOS watch by filter so call into device to stop
+                    exec(null, null, "Compass", "stopHeading", []);
+                }
+                delete timers[id];
+            }
+        }
+    };
+
+module.exports = compass;
+
+});
+
+// file: lib/common/plugin/compass/symbols.js
+define("cordova/plugin/compass/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/CompassHeading', 'CompassHeading');
+modulemapper.clobbers('cordova/plugin/CompassError', 'CompassError');
+modulemapper.clobbers('cordova/plugin/compass', 'navigator.compass');
+
+});
+
+// file: lib/common/plugin/console-via-logger.js
+define("cordova/plugin/console-via-logger", function(require, exports, module) {
+
+//------------------------------------------------------------------------------
+
+var logger = require("cordova/plugin/logger");
+var utils  = require("cordova/utils");
+
+//------------------------------------------------------------------------------
+// object that we're exporting
+//------------------------------------------------------------------------------
+var console = module.exports;
+
+//------------------------------------------------------------------------------
+// copy of the original console object
+//------------------------------------------------------------------------------
+var WinConsole = window.console;
+
+//------------------------------------------------------------------------------
+// whether to use the logger
+//------------------------------------------------------------------------------
+var UseLogger = false;
+
+//------------------------------------------------------------------------------
+// Timers
+//------------------------------------------------------------------------------
+var Timers = {};
+
+//------------------------------------------------------------------------------
+// used for unimplemented methods
+//------------------------------------------------------------------------------
+function noop() {}
+
+//------------------------------------------------------------------------------
+// used for unimplemented methods
+//------------------------------------------------------------------------------
+console.useLogger = function (value) {
+    if (arguments.length) UseLogger = !!value;
+
+    if (UseLogger) {
+        if (logger.useConsole()) {
+            throw new Error("console and logger are too intertwingly");
+        }
+    }
+
+    return UseLogger;
+};
+
+//------------------------------------------------------------------------------
+console.log = function() {
+    if (logger.useConsole()) return;
+    logger.log.apply(logger, [].slice.call(arguments));
+};
+
+//------------------------------------------------------------------------------
+console.error = function() {
+    if (logger.useConsole()) return;
+    logger.error.apply(logger, [].slice.call(arguments));
+};
+
+//------------------------------------------------------------------------------
+console.warn = function() {
+    if (logger.useConsole()) return;
+    logger.warn.apply(logger, [].slice.call(arguments));
+};
+
+//------------------------------------------------------------------------------
+console.info = function() {
+    if (logger.useConsole()) return;
+    logger.info.apply(logger, [].slice.call(arguments));
+};
+
+//------------------------------------------------------------------------------
+console.debug = function() {
+    if (logger.useConsole()) return;
+    logger.debug.apply(logger, [].slice.call(arguments));
+};
+
+//------------------------------------------------------------------------------
+console.assert = function(expression) {
+    if (expression) return;
+
+    var message = logger.format.apply(logger.format, [].slice.call(arguments, 1));
+    console.log("ASSERT: " + message);
+};
+
+//------------------------------------------------------------------------------
+console.clear = function() {};
+
+//------------------------------------------------------------------------------
+console.dir = function(object) {
+    console.log("%o", object);
+};
+
+//------------------------------------------------------------------------------
+console.dirxml = function(node) {
+    console.log(node.innerHTML);
+};
+
+//------------------------------------------------------------------------------
+console.trace = noop;
+
+//------------------------------------------------------------------------------
+console.group = console.log;
+
+//------------------------------------------------------------------------------
+console.groupCollapsed = console.log;
+
+//------------------------------------------------------------------------------
+console.groupEnd = noop;
+
+//------------------------------------------------------------------------------
+console.time = function(name) {
+    Timers[name] = new Date().valueOf();
+};
+
+//------------------------------------------------------------------------------
+console.timeEnd = function(name) {
+    var timeStart = Timers[name];
+    if (!timeStart) {
+        console.warn("unknown timer: " + name);
+        return;
+    }
+
+    var timeElapsed = new Date().valueOf() - timeStart;
+    console.log(name + ": " + timeElapsed + "ms");
+};
+
+//------------------------------------------------------------------------------
+console.timeStamp = noop;
+
+//------------------------------------------------------------------------------
+console.profile = noop;
+
+//------------------------------------------------------------------------------
+console.profileEnd = noop;
+
+//------------------------------------------------------------------------------
+console.count = noop;
+
+//------------------------------------------------------------------------------
+console.exception = console.log;
+
+//------------------------------------------------------------------------------
+console.table = function(data, columns) {
+    console.log("%o", data);
+};
+
+//------------------------------------------------------------------------------
+// return a new function that calls both functions passed as args
+//------------------------------------------------------------------------------
+function wrappedOrigCall(orgFunc, newFunc) {
+    return function() {
+        var args = [].slice.call(arguments);
+        try { orgFunc.apply(WinConsole, args); } catch (e) {}
+        try { newFunc.apply(console,    args); } catch (e) {}
+    };
+}
+
+//------------------------------------------------------------------------------
+// For every function that exists in the original console object, that
+// also exists in the new console object, wrap the new console method
+// with one that calls both
+//------------------------------------------------------------------------------
+for (var key in console) {
+    if (typeof WinConsole[key] == "function") {
+        console[key] = wrappedOrigCall(WinConsole[key], console[key]);
+    }
+}
+
+});
+
+// file: lib/common/plugin/contacts.js
+define("cordova/plugin/contacts", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    exec = require('cordova/exec'),
+    ContactError = require('cordova/plugin/ContactError'),
+    utils = require('cordova/utils'),
+    Contact = require('cordova/plugin/Contact');
+
+/**
+* Represents a group of Contacts.
+* @constructor
+*/
+var contacts = {
+    /**
+     * Returns an array of Contacts matching the search criteria.
+     * @param fields that should be searched
+     * @param successCB success callback
+     * @param errorCB error callback
+     * @param {ContactFindOptions} options that can be applied to contact searching
+     * @return array of Contacts matching search criteria
+     */
+    find:function(fields, successCB, errorCB, options) {
+        argscheck.checkArgs('afFO', 'contacts.find', arguments);
+        if (!fields.length) {
+            errorCB && errorCB(new ContactError(ContactError.INVALID_ARGUMENT_ERROR));
+        } else {
+            var win = function(result) {
+                var cs = [];
+                for (var i = 0, l = result.length; i < l; i++) {
+                    cs.push(contacts.create(result[i]));
+                }
+                successCB(cs);
+            };
+            exec(win, errorCB, "Contacts", "search", [fields, options]);
+        }
+    },
+
+    /**
+     * This function creates a new contact, but it does not persist the contact
+     * to device storage. To persist the contact to device storage, invoke
+     * contact.save().
+     * @param properties an object whose properties will be examined to create a new Contact
+     * @returns new Contact object
+     */
+    create:function(properties) {
+        argscheck.checkArgs('O', 'contacts.create', arguments);
+        var contact = new Contact();
+        for (var i in properties) {
+            if (typeof contact[i] !== 'undefined' && properties.hasOwnProperty(i)) {
+                contact[i] = properties[i];
+            }
+        }
+        return contact;
+    }
+};
+
+module.exports = contacts;
+
+});
+
+// file: lib/common/plugin/contacts/symbols.js
+define("cordova/plugin/contacts/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/contacts', 'navigator.contacts');
+modulemapper.clobbers('cordova/plugin/Contact', 'Contact');
+modulemapper.clobbers('cordova/plugin/ContactAddress', 'ContactAddress');
+modulemapper.clobbers('cordova/plugin/ContactError', 'ContactError');
+modulemapper.clobbers('cordova/plugin/ContactField', 'ContactField');
+modulemapper.clobbers('cordova/plugin/ContactFindOptions', 'ContactFindOptions');
+modulemapper.clobbers('cordova/plugin/ContactName', 'ContactName');
+modulemapper.clobbers('cordova/plugin/ContactOrganization', 'ContactOrganization');
+
+});
+
+// file: lib/common/plugin/device.js
+define("cordova/plugin/device", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    channel = require('cordova/channel'),
+    utils = require('cordova/utils'),
+    exec = require('cordova/exec');
+
+// Tell cordova channel to wait on the CordovaInfoReady event
+channel.waitForInitialization('onCordovaInfoReady');
+
+/**
+ * This represents the mobile device, and provides properties for inspecting the model, version, UUID of the
+ * phone, etc.
+ * @constructor
+ */
+function Device() {
+    this.available = false;
+    this.platform = null;
+    this.version = null;
+    this.uuid = null;
+    this.cordova = null;
+    this.model = null;
+
+    var me = this;
+
+    channel.onCordovaReady.subscribe(function() {
+        me.getInfo(function(info) {
+            var buildLabel = info.cordova;
+            if (buildLabel != CORDOVA_JS_BUILD_LABEL) {
+                buildLabel += ' JS=' + CORDOVA_JS_BUILD_LABEL;
+            }
+            me.available = true;
+            me.platform = info.platform;
+            me.version = info.version;
+            me.uuid = info.uuid;
+            me.cordova = buildLabel;
+            me.model = info.model;
+            channel.onCordovaInfoReady.fire();
+        },function(e) {
+            me.available = false;
+            utils.alert("[ERROR] Error initializing Cordova: " + e);
+        });
+    });
+}
+
+/**
+ * Get device info
+ *
+ * @param {Function} successCallback The function to call when the heading data is available
+ * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL)
+ */
+Device.prototype.getInfo = function(successCallback, errorCallback) {
+    argscheck.checkArgs('fF', 'Device.getInfo', arguments);
+    exec(successCallback, errorCallback, "Device", "getDeviceInfo", []);
+};
+
+module.exports = new Device();
+
+});
+
+// file: lib/android/plugin/device/symbols.js
+define("cordova/plugin/device/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/device', 'device');
+modulemapper.merges('cordova/plugin/android/device', 'device');
+
+});
+
+// file: lib/common/plugin/echo.js
+define("cordova/plugin/echo", function(require, exports, module) {
+
+var exec = require('cordova/exec'),
+    utils = require('cordova/utils');
+
+/**
+ * Sends the given message through exec() to the Echo plugin, which sends it back to the successCallback.
+ * @param successCallback  invoked with a FileSystem object
+ * @param errorCallback  invoked if error occurs retrieving file system
+ * @param message  The string to be echoed.
+ * @param forceAsync  Whether to force an async return value (for testing native->js bridge).
+ */
+module.exports = function(successCallback, errorCallback, message, forceAsync) {
+    var action = 'echo';
+    var messageIsMultipart = (utils.typeName(message) == "Array");
+    var args = messageIsMultipart ? message : [message];
+
+    if (utils.typeName(message) == 'ArrayBuffer') {
+        if (forceAsync) {
+            console.warn('Cannot echo ArrayBuffer with forced async, falling back to sync.');
+        }
+        action += 'ArrayBuffer';
+    } else if (messageIsMultipart) {
+        if (forceAsync) {
+            console.warn('Cannot echo MultiPart Array with forced async, falling back to sync.');
+        }
+        action += 'MultiPart';
+    } else if (forceAsync) {
+        action += 'Async';
+    }
+
+    exec(successCallback, errorCallback, "Echo", action, args);
+};
+
+
+});
+
+// file: lib/android/plugin/file/symbols.js
+define("cordova/plugin/file/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper'),
+    symbolshelper = require('cordova/plugin/file/symbolshelper');
+
+symbolshelper(modulemapper.clobbers);
+
+});
+
+// file: lib/common/plugin/file/symbolshelper.js
+define("cordova/plugin/file/symbolshelper", function(require, exports, module) {
+
+module.exports = function(exportFunc) {
+    exportFunc('cordova/plugin/DirectoryEntry', 'DirectoryEntry');
+    exportFunc('cordova/plugin/DirectoryReader', 'DirectoryReader');
+    exportFunc('cordova/plugin/Entry', 'Entry');
+    exportFunc('cordova/plugin/File', 'File');
+    exportFunc('cordova/plugin/FileEntry', 'FileEntry');
+    exportFunc('cordova/plugin/FileError', 'FileError');
+    exportFunc('cordova/plugin/FileReader', 'FileReader');
+    exportFunc('cordova/plugin/FileSystem', 'FileSystem');
+    exportFunc('cordova/plugin/FileUploadOptions', 'FileUploadOptions');
+    exportFunc('cordova/plugin/FileUploadResult', 'FileUploadResult');
+    exportFunc('cordova/plugin/FileWriter', 'FileWriter');
+    exportFunc('cordova/plugin/Flags', 'Flags');
+    exportFunc('cordova/plugin/LocalFileSystem', 'LocalFileSystem');
+    exportFunc('cordova/plugin/Metadata', 'Metadata');
+    exportFunc('cordova/plugin/ProgressEvent', 'ProgressEvent');
+    exportFunc('cordova/plugin/requestFileSystem', 'requestFileSystem');
+    exportFunc('cordova/plugin/resolveLocalFileSystemURI', 'resolveLocalFileSystemURI');
+};
+
+});
+
+// file: lib/common/plugin/filetransfer/symbols.js
+define("cordova/plugin/filetransfer/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/FileTransfer', 'FileTransfer');
+modulemapper.clobbers('cordova/plugin/FileTransferError', 'FileTransferError');
+
+});
+
+// file: lib/common/plugin/geolocation.js
+define("cordova/plugin/geolocation", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    utils = require('cordova/utils'),
+    exec = require('cordova/exec'),
+    PositionError = require('cordova/plugin/PositionError'),
+    Position = require('cordova/plugin/Position');
+
+var timers = {};   // list of timers in use
+
+// Returns default params, overrides if provided with values
+function parseParameters(options) {
+    var opt = {
+        maximumAge: 0,
+        enableHighAccuracy: false,
+        timeout: Infinity
+    };
+
+    if (options) {
+        if (options.maximumAge !== undefined && !isNaN(options.maximumAge) && options.maximumAge > 0) {
+            opt.maximumAge = options.maximumAge;
+        }
+        if (options.enableHighAccuracy !== undefined) {
+            opt.enableHighAccuracy = options.enableHighAccuracy;
+        }
+        if (options.timeout !== undefined && !isNaN(options.timeout)) {
+            if (options.timeout < 0) {
+                opt.timeout = 0;
+            } else {
+                opt.timeout = options.timeout;
+            }
+        }
+    }
+
+    return opt;
+}
+
+// Returns a timeout failure, closed over a specified timeout value and error callback.
+function createTimeout(errorCallback, timeout) {
+    var t = setTimeout(function() {
+        clearTimeout(t);
+        t = null;
+        errorCallback({
+            code:PositionError.TIMEOUT,
+            message:"Position retrieval timed out."
+        });
+    }, timeout);
+    return t;
+}
+
+var geolocation = {
+    lastPosition:null, // reference to last known (cached) position returned
+    /**
+   * Asynchronously acquires the current position.
+   *
+   * @param {Function} successCallback    The function to call when the position data is available
+   * @param {Function} errorCallback      The function to call when there is an error getting the heading position. (OPTIONAL)
+   * @param {PositionOptions} options     The options for getting the position data. (OPTIONAL)
+   */
+    getCurrentPosition:function(successCallback, errorCallback, options) {
+        argscheck.checkArgs('fFO', 'geolocation.getCurrentPosition', arguments);
+        options = parseParameters(options);
+
+        // Timer var that will fire an error callback if no position is retrieved from native
+        // before the "timeout" param provided expires
+        var timeoutTimer = {timer:null};
+
+        var win = function(p) {
+            clearTimeout(timeoutTimer.timer);
+            if (!(timeoutTimer.timer)) {
+                // Timeout already happened, or native fired error callback for
+                // this geo request.
+                // Don't continue with success callback.
+                return;
+            }
+            var pos = new Position(
+                {
+                    latitude:p.latitude,
+                    longitude:p.longitude,
+                    altitude:p.altitude,
+                    accuracy:p.accuracy,
+                    heading:p.heading,
+                    velocity:p.velocity,
+                    altitudeAccuracy:p.altitudeAccuracy
+                },
+                (p.timestamp === undefined ? new Date() : ((p.timestamp instanceof Date) ? p.timestamp : new Date(p.timestamp)))
+            );
+            geolocation.lastPosition = pos;
+            successCallback(pos);
+        };
+        var fail = function(e) {
+            clearTimeout(timeoutTimer.timer);
+            timeoutTimer.timer = null;
+            var err = new PositionError(e.code, e.message);
+            if (errorCallback) {
+                errorCallback(err);
+            }
+        };
+
+        // Check our cached position, if its timestamp difference with current time is less than the maximumAge, then just
+        // fire the success callback with the cached position.
+        if (geolocation.lastPosition && options.maximumAge && (((new Date()).getTime() - geolocation.lastPosition.timestamp.getTime()) <= options.maximumAge)) {
+            successCallback(geolocation.lastPosition);
+        // If the cached position check failed and the timeout was set to 0, error out with a TIMEOUT error object.
+        } else if (options.timeout === 0) {
+            fail({
+                code:PositionError.TIMEOUT,
+                message:"timeout value in PositionOptions set to 0 and no cached Position object available, or cached Position object's age exceeds provided PositionOptions' maximumAge parameter."
+            });
+        // Otherwise we have to call into native to retrieve a position.
+        } else {
+            if (options.timeout !== Infinity) {
+                // If the timeout value was not set to Infinity (default), then
+                // set up a timeout function that will fire the error callback
+                // if no successful position was retrieved before timeout expired.
+                timeoutTimer.timer = createTimeout(fail, options.timeout);
+            } else {
+                // This is here so the check in the win function doesn't mess stuff up
+                // may seem weird but this guarantees timeoutTimer is
+                // always truthy before we call into native
+                timeoutTimer.timer = true;
+            }
+            exec(win, fail, "Geolocation", "getLocation", [options.enableHighAccuracy, options.maximumAge]);
+        }
+        return timeoutTimer;
+    },
+    /**
+     * Asynchronously watches the geolocation for changes to geolocation.  When a change occurs,
+     * the successCallback is called with the new location.
+     *
+     * @param {Function} successCallback    The function to call each time the location data is available
+     * @param {Function} errorCallback      The function to call when there is an error getting the location data. (OPTIONAL)
+     * @param {PositionOptions} options     The options for getting the location data such as frequency. (OPTIONAL)
+     * @return String                       The watch id that must be passed to #clearWatch to stop watching.
+     */
+    watchPosition:function(successCallback, errorCallback, options) {
+        argscheck.checkArgs('fFO', 'geolocation.getCurrentPosition', arguments);
+        options = parseParameters(options);
+
+        var id = utils.createUUID();
+
+        // Tell device to get a position ASAP, and also retrieve a reference to the timeout timer generated in getCurrentPosition
+        timers[id] = geolocation.getCurrentPosition(successCallback, errorCallback, options);
+
+        var fail = function(e) {
+            clearTimeout(timers[id].timer);
+            var err = new PositionError(e.code, e.message);
+            if (errorCallback) {
+                errorCallback(err);
+            }
+        };
+
+        var win = function(p) {
+            clearTimeout(timers[id].timer);
+            if (options.timeout !== Infinity) {
+                timers[id].timer = createTimeout(fail, options.timeout);
+            }
+            var pos = new Position(
+                {
+                    latitude:p.latitude,
+                    longitude:p.longitude,
+                    altitude:p.altitude,
+                    accuracy:p.accuracy,
+                    heading:p.heading,
+                    velocity:p.velocity,
+                    altitudeAccuracy:p.altitudeAccuracy
+                },
+                (p.timestamp === undefined ? new Date() : ((p.timestamp instanceof Date) ? p.timestamp : new Date(p.timestamp)))
+            );
+            geolocation.lastPosition = pos;
+            successCallback(pos);
+        };
+
+        exec(win, fail, "Geolocation", "addWatch", [id, options.enableHighAccuracy]);
+
+        return id;
+    },
+    /**
+     * Clears the specified heading watch.
+     *
+     * @param {String} id       The ID of the watch returned from #watchPosition
+     */
+    clearWatch:function(id) {
+        if (id && timers[id] !== undefined) {
+            clearTimeout(timers[id].timer);
+            timers[id].timer = false;
+            exec(null, null, "Geolocation", "clearWatch", [id]);
+        }
+    }
+};
+
+module.exports = geolocation;
+
+});
+
+// file: lib/common/plugin/geolocation/symbols.js
+define("cordova/plugin/geolocation/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.defaults('cordova/plugin/geolocation', 'navigator.geolocation');
+modulemapper.clobbers('cordova/plugin/PositionError', 'PositionError');
+modulemapper.clobbers('cordova/plugin/Position', 'Position');
+modulemapper.clobbers('cordova/plugin/Coordinates', 'Coordinates');
+
+});
+
+// file: lib/common/plugin/globalization.js
+define("cordova/plugin/globalization", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    exec = require('cordova/exec'),
+    GlobalizationError = require('cordova/plugin/GlobalizationError');
+
+var globalization = {
+
+/**
+* Returns the string identifier for the client's current language.
+* It returns the language identifier string to the successCB callback with a
+* properties object as a parameter. If there is an error getting the language,
+* then the errorCB callback is invoked.
+*
+* @param {Function} successCB
+* @param {Function} errorCB
+*
+* @return Object.value {String}: The language identifier
+*
+* @error GlobalizationError.UNKNOWN_ERROR
+*
+* Example
+*    globalization.getPreferredLanguage(function (language) {alert('language:' + language.value + '\n');},
+*                                function () {});
+*/
+getPreferredLanguage:function(successCB, failureCB) {
+    argscheck.checkArgs('fF', 'Globalization.getPreferredLanguage', arguments);
+    exec(successCB, failureCB, "Globalization","getPreferredLanguage", []);
+},
+
+/**
+* Returns the string identifier for the client's current locale setting.
+* It returns the locale identifier string to the successCB callback with a
+* properties object as a parameter. If there is an error getting the locale,
+* then the errorCB callback is invoked.
+*
+* @param {Function} successCB
+* @param {Function} errorCB
+*
+* @return Object.value {String}: The locale identifier
+*
+* @error GlobalizationError.UNKNOWN_ERROR
+*
+* Example
+*    globalization.getLocaleName(function (locale) {alert('locale:' + locale.value + '\n');},
+*                                function () {});
+*/
+getLocaleName:function(successCB, failureCB) {
+    argscheck.checkArgs('fF', 'Globalization.getLocaleName', arguments);
+    exec(successCB, failureCB, "Globalization","getLocaleName", []);
+},
+
+
+/**
+* Returns a date formatted as a string according to the client's user preferences and
+* calendar using the time zone of the client. It returns the formatted date string to the
+* successCB callback with a properties object as a parameter. If there is an error
+* formatting the date, then the errorCB callback is invoked.
+*
+* The defaults are: formatLenght="short" and selector="date and time"
+*
+* @param {Date} date
+* @param {Function} successCB
+* @param {Function} errorCB
+* @param {Object} options {optional}
+*            formatLength {String}: 'short', 'medium', 'long', or 'full'
+*            selector {String}: 'date', 'time', or 'date and time'
+*
+* @return Object.value {String}: The localized date string
+*
+* @error GlobalizationError.FORMATTING_ERROR
+*
+* Example
+*    globalization.dateToString(new Date(),
+*                function (date) {alert('date:' + date.value + '\n');},
+*                function (errorCode) {alert(errorCode);},
+*                {formatLength:'short'});
+*/
+dateToString:function(date, successCB, failureCB, options) {
+    argscheck.checkArgs('dfFO', 'Globalization.dateToString', arguments);
+    var dateValue = date.valueOf();
+    exec(successCB, failureCB, "Globalization", "dateToString", [{"date": dateValue, "options": options}]);
+},
+
+
+/**
+* Parses a date formatted as a string according to the client's user
+* preferences and calendar using the time zone of the client and returns
+* the corresponding date object. It returns the date to the successCB
+* callback with a properties object as a parameter. If there is an error
+* parsing the date string, then the errorCB callback is invoked.
+*
+* The defaults are: formatLength="short" and selector="date and time"
+*
+* @param {String} dateString
+* @param {Function} successCB
+* @param {Function} errorCB
+* @param {Object} options {optional}
+*            formatLength {String}: 'short', 'medium', 'long', or 'full'
+*            selector {String}: 'date', 'time', or 'date and time'
+*
+* @return    Object.year {Number}: The four digit year
+*            Object.month {Number}: The month from (0 - 11)
+*            Object.day {Number}: The day from (1 - 31)
+*            Object.hour {Number}: The hour from (0 - 23)
+*            Object.minute {Number}: The minute from (0 - 59)
+*            Object.second {Number}: The second from (0 - 59)
+*            Object.millisecond {Number}: The milliseconds (from 0 - 999),
+*                                        not available on all platforms
+*
+* @error GlobalizationError.PARSING_ERROR
+*
+* Example
+*    globalization.stringToDate('4/11/2011',
+*                function (date) { alert('Month:' + date.month + '\n' +
+*                    'Day:' + date.day + '\n' +
+*                    'Year:' + date.year + '\n');},
+*                function (errorCode) {alert(errorCode);},
+*                {selector:'date'});
+*/
+stringToDate:function(dateString, successCB, failureCB, options) {
+    argscheck.checkArgs('sfFO', 'Globalization.stringToDate', arguments);
+    exec(successCB, failureCB, "Globalization", "stringToDate", [{"dateString": dateString, "options": options}]);
+},
+
+
+/**
+* Returns a pattern string for formatting and parsing dates according to the client's
+* user preferences. It returns the pattern to the successCB callback with a
+* properties object as a parameter. If there is an error obtaining the pattern,
+* then the errorCB callback is invoked.
+*
+* The defaults are: formatLength="short" and selector="date and time"
+*
+* @param {Function} successCB
+* @param {Function} errorCB
+* @param {Object} options {optional}
+*            formatLength {String}: 'short', 'medium', 'long', or 'full'
+*            selector {String}: 'date', 'time', or 'date and time'
+*
+* @return    Object.pattern {String}: The date and time pattern for formatting and parsing dates.
+*                                    The patterns follow Unicode Technical Standard #35
+*                                    http://unicode.org/reports/tr35/tr35-4.html
+*            Object.timezone {String}: The abbreviated name of the time zone on the client
+*            Object.utc_offset {Number}: The current difference in seconds between the client's
+*                                        time zone and coordinated universal time.
+*            Object.dst_offset {Number}: The current daylight saving time offset in seconds
+*                                        between the client's non-daylight saving's time zone
+*                                        and the client's daylight saving's time zone.
+*
+* @error GlobalizationError.PATTERN_ERROR
+*
+* Example
+*    globalization.getDatePattern(
+*                function (date) {alert('pattern:' + date.pattern + '\n');},
+*                function () {},
+*                {formatLength:'short'});
+*/
+getDatePattern:function(successCB, failureCB, options) {
+    argscheck.checkArgs('fFO', 'Globalization.getDatePattern', arguments);
+    exec(successCB, failureCB, "Globalization", "getDatePattern", [{"options": options}]);
+},
+
+
+/**
+* Returns an array of either the names of the months or days of the week
+* according to the client's user preferences and calendar. It returns the array of names to the
+* successCB callback with a properties object as a parameter. If there is an error obtaining the
+* names, then the errorCB callback is invoked.
+*
+* The defaults are: type="wide" and item="months"
+*
+* @param {Function} successCB
+* @param {Function} errorCB
+* @param {Object} options {optional}
+*            type {String}: 'narrow' or 'wide'
+*            item {String}: 'months', or 'days'
+*
+* @return Object.value {Array{String}}: The array of names starting from either
+*                                        the first month in the year or the
+*                                        first day of the week.
+* @error GlobalizationError.UNKNOWN_ERROR
+*
+* Example
+*    globalization.getDateNames(function (names) {
+*        for(var i = 0; i < names.value.length; i++) {
+*            alert('Month:' + names.value[i] + '\n');}},
+*        function () {});
+*/
+getDateNames:function(successCB, failureCB, options) {
+    argscheck.checkArgs('fFO', 'Globalization.getDateNames', arguments);
+    exec(successCB, failureCB, "Globalization", "getDateNames", [{"options": options}]);
+},
+
+/**
+* Returns whether daylight savings time is in effect for a given date using the client's
+* time zone and calendar. It returns whether or not daylight savings time is in effect
+* to the successCB callback with a properties object as a parameter. If there is an error
+* reading the date, then the errorCB callback is invoked.
+*
+* @param {Date} date
+* @param {Function} successCB
+* @param {Function} errorCB
+*
+* @return Object.dst {Boolean}: The value "true" indicates that daylight savings time is
+*                                in effect for the given date and "false" indicate that it is not.
+*
+* @error GlobalizationError.UNKNOWN_ERROR
+*
+* Example
+*    globalization.isDayLightSavingsTime(new Date(),
+*                function (date) {alert('dst:' + date.dst + '\n');}
+*                function () {});
+*/
+isDayLightSavingsTime:function(date, successCB, failureCB) {
+    argscheck.checkArgs('dfF', 'Globalization.isDayLightSavingsTime', arguments);
+    var dateValue = date.valueOf();
+    exec(successCB, failureCB, "Globalization", "isDayLightSavingsTime", [{"date": dateValue}]);
+},
+
+/**
+* Returns the first day of the week according to the client's user preferences and calendar.
+* The days of the week are numbered starting from 1 where 1 is considered to be Sunday.
+* It returns the day to the successCB callback with a properties object as a parameter.
+* If there is an error obtaining the pattern, then the errorCB callback is invoked.
+*
+* @param {Function} successCB
+* @param {Function} errorCB
+*
+* @return Object.value {Number}: The number of the first day of the week.
+*
+* @error GlobalizationError.UNKNOWN_ERROR
+*
+* Example
+*    globalization.getFirstDayOfWeek(function (day)
+*                { alert('Day:' + day.value + '\n');},
+*                function () {});
+*/
+getFirstDayOfWeek:function(successCB, failureCB) {
+    argscheck.checkArgs('fF', 'Globalization.getFirstDayOfWeek', arguments);
+    exec(successCB, failureCB, "Globalization", "getFirstDayOfWeek", []);
+},
+
+
+/**
+* Returns a number formatted as a string according to the client's user preferences.
+* It returns the formatted number string to the successCB callback with a properties object as a
+* parameter. If there is an error formatting the number, then the errorCB callback is invoked.
+*
+* The defaults are: type="decimal"
+*
+* @param {Number} number
+* @param {Function} successCB
+* @param {Function} errorCB
+* @param {Object} options {optional}
+*            type {String}: 'decimal', "percent", or 'currency'
+*
+* @return Object.value {String}: The formatted number string.
+*
+* @error GlobalizationError.FORMATTING_ERROR
+*
+* Example
+*    globalization.numberToString(3.25,
+*                function (number) {alert('number:' + number.value + '\n');},
+*                function () {},
+*                {type:'decimal'});
+*/
+numberToString:function(number, successCB, failureCB, options) {
+    argscheck.checkArgs('nfFO', 'Globalization.numberToString', arguments);
+    exec(successCB, failureCB, "Globalization", "numberToString", [{"number": number, "options": options}]);
+},
+
+/**
+* Parses a number formatted as a string according to the client's user preferences and
+* returns the corresponding number. It returns the number to the successCB callback with a
+* properties object as a parameter. If there is an error parsing the number string, then
+* the errorCB callback is invoked.
+*
+* The defaults are: type="decimal"
+*
+* @param {String} numberString
+* @param {Function} successCB
+* @param {Function} errorCB
+* @param {Object} options {optional}
+*            type {String}: 'decimal', "percent", or 'currency'
+*
+* @return Object.value {Number}: The parsed number.
+*
+* @error GlobalizationError.PARSING_ERROR
+*
+* Example
+*    globalization.stringToNumber('1234.56',
+*                function (number) {alert('Number:' + number.value + '\n');},
+*                function () { alert('Error parsing number');});
+*/
+stringToNumber:function(numberString, successCB, failureCB, options) {
+    argscheck.checkArgs('sfFO', 'Globalization.stringToNumber', arguments);
+    exec(successCB, failureCB, "Globalization", "stringToNumber", [{"numberString": numberString, "options": options}]);
+},
+
+/**
+* Returns a pattern string for formatting and parsing numbers according to the client's user
+* preferences. It returns the pattern to the successCB callback with a properties object as a
+* parameter. If there is an error obtaining the pattern, then the errorCB callback is invoked.
+*
+* The defaults are: type="decimal"
+*
+* @param {Function} successCB
+* @param {Function} errorCB
+* @param {Object} options {optional}
+*            type {String}: 'decimal', "percent", or 'currency'
+*
+* @return    Object.pattern {String}: The number pattern for formatting and parsing numbers.
+*                                    The patterns follow Unicode Technical Standard #35.
+*                                    http://unicode.org/reports/tr35/tr35-4.html
+*            Object.symbol {String}: The symbol to be used when formatting and parsing
+*                                    e.g., percent or currency symbol.
+*            Object.fraction {Number}: The number of fractional digits to use when parsing and
+*                                    formatting numbers.
+*            Object.rounding {Number}: The rounding increment to use when parsing and formatting.
+*            Object.positive {String}: The symbol to use for positive numbers when parsing and formatting.
+*            Object.negative: {String}: The symbol to use for negative numbers when parsing and formatting.
+*            Object.decimal: {String}: The decimal symbol to use for parsing and formatting.
+*            Object.grouping: {String}: The grouping symbol to use for parsing and formatting.
+*
+* @error GlobalizationError.PATTERN_ERROR
+*
+* Example
+*    globalization.getNumberPattern(
+*                function (pattern) {alert('Pattern:' + pattern.pattern + '\n');},
+*                function () {});
+*/
+getNumberPattern:function(successCB, failureCB, options) {
+    argscheck.checkArgs('fFO', 'Globalization.getNumberPattern', arguments);
+    exec(successCB, failureCB, "Globalization", "getNumberPattern", [{"options": options}]);
+},
+
+/**
+* Returns a pattern string for formatting and parsing currency values according to the client's
+* user preferences and ISO 4217 currency code. It returns the pattern to the successCB callback with a
+* properties object as a parameter. If there is an error obtaining the pattern, then the errorCB
+* callback is invoked.
+*
+* @param {String} currencyCode
+* @param {Function} successCB
+* @param {Function} errorCB
+*
+* @return    Object.pattern {String}: The currency pattern for formatting and parsing currency values.
+*                                    The patterns follow Unicode Technical Standard #35
+*                                    http://unicode.org/reports/tr35/tr35-4.html
+*            Object.code {String}: The ISO 4217 currency code for the pattern.
+*            Object.fraction {Number}: The number of fractional digits to use when parsing and
+*                                    formatting currency.
+*            Object.rounding {Number}: The rounding increment to use when parsing and formatting.
+*            Object.decimal: {String}: The decimal symbol to use for parsing and formatting.
+*            Object.grouping: {String}: The grouping symbol to use for parsing and formatting.
+*
+* @error GlobalizationError.FORMATTING_ERROR
+*
+* Example
+*    globalization.getCurrencyPattern('EUR',
+*                function (currency) {alert('Pattern:' + currency.pattern + '\n');}
+*                function () {});
+*/
+getCurrencyPattern:function(currencyCode, successCB, failureCB) {
+    argscheck.checkArgs('sfF', 'Globalization.getCurrencyPattern', arguments);
+    exec(successCB, failureCB, "Globalization", "getCurrencyPattern", [{"currencyCode": currencyCode}]);
+}
+
+};
+
+module.exports = globalization;
+
+});
+
+// file: lib/common/plugin/globalization/symbols.js
+define("cordova/plugin/globalization/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/globalization', 'navigator.globalization');
+modulemapper.clobbers('cordova/plugin/GlobalizationError', 'GlobalizationError');
+
+});
+
+// file: lib/android/plugin/inappbrowser/symbols.js
+define("cordova/plugin/inappbrowser/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/InAppBrowser', 'open');
+
+});
+
+// file: lib/common/plugin/logger.js
+define("cordova/plugin/logger", function(require, exports, module) {
+
+//------------------------------------------------------------------------------
+// The logger module exports the following properties/functions:
+//
+// LOG                          - constant for the level LOG
+// ERROR                        - constant for the level ERROR
+// WARN                         - constant for the level WARN
+// INFO                         - constant for the level INFO
+// DEBUG                        - constant for the level DEBUG
+// logLevel()                   - returns current log level
+// logLevel(value)              - sets and returns a new log level
+// useConsole()                 - returns whether logger is using console
+// useConsole(value)            - sets and returns whether logger is using console
+// log(message,...)             - logs a message at level LOG
+// error(message,...)           - logs a message at level ERROR
+// warn(message,...)            - logs a message at level WARN
+// info(message,...)            - logs a message at level INFO
+// debug(message,...)           - logs a message at level DEBUG
+// logLevel(level,message,...)  - logs a message specified level
+//
+//------------------------------------------------------------------------------
+
+var logger = exports;
+
+var exec    = require('cordova/exec');
+var utils   = require('cordova/utils');
+
+var UseConsole   = true;
+var UseLogger    = true;
+var Queued       = [];
+var DeviceReady  = false;
+var CurrentLevel;
+
+var originalConsole = console;
+
+/**
+ * Logging levels
+ */
+
+var Levels = [
+    "LOG",
+    "ERROR",
+    "WARN",
+    "INFO",
+    "DEBUG"
+];
+
+/*
+ * add the logging levels to the logger object and
+ * to a separate levelsMap object for testing
+ */
+
+var LevelsMap = {};
+for (var i=0; i<Levels.length; i++) {
+    var level = Levels[i];
+    LevelsMap[level] = i;
+    logger[level]    = level;
+}
+
+CurrentLevel = LevelsMap.WARN;
+
+/**
+ * Getter/Setter for the logging level
+ *
+ * Returns the current logging level.
+ *
+ * When a value is passed, sets the logging level to that value.
+ * The values should be one of the following constants:
+ *    logger.LOG
+ *    logger.ERROR
+ *    logger.WARN
+ *    logger.INFO
+ *    logger.DEBUG
+ *
+ * The value used determines which messages get printed.  The logging
+ * values above are in order, and only messages logged at the logging
+ * level or above will actually be displayed to the user.  E.g., the
+ * default level is WARN, so only messages logged with LOG, ERROR, or
+ * WARN will be displayed; INFO and DEBUG messages will be ignored.
+ */
+logger.level = function (value) {
+    if (arguments.length) {
+        if (LevelsMap[value] === null) {
+            throw new Error("invalid logging level: " + value);
+        }
+        CurrentLevel = LevelsMap[value];
+    }
+
+    return Levels[CurrentLevel];
+};
+
+/**
+ * Getter/Setter for the useConsole functionality
+ *
+ * When useConsole is true, the logger will log via the
+ * browser 'console' object.
+ */
+logger.useConsole = function (value) {
+    if (arguments.length) UseConsole = !!value;
+
+    if (UseConsole) {
+        if (typeof console == "undefined") {
+            throw new Error("global console object is not defined");
+        }
+
+        if (typeof console.log != "function") {
+            throw new Error("global console object does not have a log function");
+        }
+
+        if (typeof console.useLogger == "function") {
+            if (console.useLogger()) {
+                throw new Error("console and logger are too intertwingly");
+            }
+        }
+    }
+
+    return UseConsole;
+};
+
+/**
+ * Getter/Setter for the useLogger functionality
+ *
+ * When useLogger is true, the logger will log via the
+ * native Logger plugin.
+ */
+logger.useLogger = function (value) {
+    // Enforce boolean
+    if (arguments.length) UseLogger = !!value;
+    return UseLogger;
+};
+
+/**
+ * Logs a message at the LOG level.
+ *
+ * Parameters passed after message are used applied to
+ * the message with utils.format()
+ */
+logger.log   = function(message) { logWithArgs("LOG",   arguments); };
+
+/**
+ * Logs a message at the ERROR level.
+ *
+ * Parameters passed after message are used applied to
+ * the message with utils.format()
+ */
+logger.error = function(message) { logWithArgs("ERROR", arguments); };
+
+/**
+ * Logs a message at the WARN level.
+ *
+ * Parameters passed after message are used applied to
+ * the message with utils.format()
+ */
+logger.warn  = function(message) { logWithArgs("WARN",  arguments); };
+
+/**
+ * Logs a message at the INFO level.
+ *
+ * Parameters passed after message are used applied to
+ * the message with utils.format()
+ */
+logger.info  = function(message) { logWithArgs("INFO",  arguments); };
+
+/**
+ * Logs a message at the DEBUG level.
+ *
+ * Parameters passed after message are used applied to
+ * the message with utils.format()
+ */
+logger.debug = function(message) { logWithArgs("DEBUG", arguments); };
+
+// log at the specified level with args
+function logWithArgs(level, args) {
+    args = [level].concat([].slice.call(args));
+    logger.logLevel.apply(logger, args);
+}
+
+/**
+ * Logs a message at the specified level.
+ *
+ * Parameters passed after message are used applied to
+ * the message with utils.format()
+ */
+logger.logLevel = function(level /* , ... */) {
+    // format the message with the parameters
+    var formatArgs = [].slice.call(arguments, 1);
+    var message    = logger.format.apply(logger.format, formatArgs);
+
+    if (LevelsMap[level] === null) {
+        throw new Error("invalid logging level: " + level);
+    }
+
+    if (LevelsMap[level] > CurrentLevel) return;
+
+    // queue the message if not yet at deviceready
+    if (!DeviceReady && !UseConsole) {
+        Queued.push([level, message]);
+        return;
+    }
+
+    // Log using the native logger if that is enabled
+    if (UseLogger) {
+        exec(null, null, "Logger", "logLevel", [level, message]);
+    }
+
+    // Log using the console if that is enabled
+    if (UseConsole) {
+        // make sure console is not using logger
+        if (console.__usingCordovaLogger) {
+            throw new Error("console and logger are too intertwingly");
+        }
+
+        // log to the console
+        switch (level) {
+            case logger.LOG:   originalConsole.log(message); break;
+            case logger.ERROR: originalConsole.log("ERROR: " + message); break;
+            case logger.WARN:  originalConsole.log("WARN: "  + message); break;
+            case logger.INFO:  originalConsole.log("INFO: "  + message); break;
+            case logger.DEBUG: originalConsole.log("DEBUG: " + message); break;
+        }
+    }
+};
+
+
+/**
+ * Formats a string and arguments following it ala console.log()
+ *
+ * Any remaining arguments will be appended to the formatted string.
+ *
+ * for rationale, see FireBug's Console API:
+ *    http://getfirebug.com/wiki/index.php/Console_API
+ */
+logger.format = function(formatString, args) {
+    return __format(arguments[0], [].slice.call(arguments,1)).join(' ');
+};
+
+
+//------------------------------------------------------------------------------
+/**
+ * Formats a string and arguments following it ala vsprintf()
+ *
+ * format chars:
+ *   %j - format arg as JSON
+ *   %o - format arg as JSON
+ *   %c - format arg as ''
+ *   %% - replace with '%'
+ * any other char following % will format it's
+ * arg via toString().
+ *
+ * Returns an array containing the formatted string and any remaining
+ * arguments.
+ */
+function __format(formatString, args) {
+    if (formatString === null || formatString === undefined) return [""];
+    if (arguments.length == 1) return [formatString.toString()];
+
+    if (typeof formatString != "string")
+        formatString = formatString.toString();
+
+    var pattern = /(.*?)%(.)(.*)/;
+    var rest    = formatString;
+    var result  = [];
+
+    while (args.length) {
+        var match = pattern.exec(rest);
+        if (!match) break;
+
+        var arg   = args.shift();
+        rest = match[3];
+        result.push(match[1]);
+
+        if (match[2] == '%') {
+            result.push('%');
+            args.unshift(arg);
+            continue;
+        }
+
+        result.push(__formatted(arg, match[2]));
+    }
+
+    result.push(rest);
+
+    var remainingArgs = [].slice.call(args);
+    remainingArgs.unshift(result.join(''));
+    return remainingArgs;
+}
+
+function __formatted(object, formatChar) {
+
+    try {
+        switch(formatChar) {
+            case 'j':
+            case 'o': return JSON.stringify(object);
+            case 'c': return '';
+        }
+    }
+    catch (e) {
+        return "error JSON.stringify()ing argument: " + e;
+    }
+
+    if ((object === null) || (object === undefined)) {
+        return Object.prototype.toString.call(object);
+    }
+
+    return object.toString();
+}
+
+
+//------------------------------------------------------------------------------
+// when deviceready fires, log queued messages
+logger.__onDeviceReady = function() {
+    if (DeviceReady) return;
+
+    DeviceReady = true;
+
+    for (var i=0; i<Queued.length; i++) {
+        var messageArgs = Queued[i];
+        logger.logLevel(messageArgs[0], messageArgs[1]);
+    }
+
+    Queued = null;
+};
+
+// add a deviceready event to log queued messages
+document.addEventListener("deviceready", logger.__onDeviceReady, false);
+
+});
+
+// file: lib/common/plugin/logger/symbols.js
+define("cordova/plugin/logger/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/logger', 'cordova.logger');
+
+});
+
+// file: lib/android/plugin/media/symbols.js
+define("cordova/plugin/media/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.defaults('cordova/plugin/Media', 'Media');
+modulemapper.clobbers('cordova/plugin/MediaError', 'MediaError');
+
+});
+
+// file: lib/common/plugin/network.js
+define("cordova/plugin/network", function(require, exports, module) {
+
+var exec = require('cordova/exec'),
+    cordova = require('cordova'),
+    channel = require('cordova/channel'),
+    utils = require('cordova/utils');
+
+// Link the onLine property with the Cordova-supplied network info.
+// This works because we clobber the naviagtor object with our own
+// object in bootstrap.js.
+if (typeof navigator != 'undefined') {
+    utils.defineGetter(navigator, 'onLine', function() {
+        return this.connection.type != 'none';
+    });
+}
+
+function NetworkConnection() {
+    this.type = 'unknown';
+}
+
+/**
+ * Get connection info
+ *
+ * @param {Function} successCallback The function to call when the Connection data is available
+ * @param {Function} errorCallback The function to call when there is an error getting the Connection data. (OPTIONAL)
+ */
+NetworkConnection.prototype.getInfo = function(successCallback, errorCallback) {
+    exec(successCallback, errorCallback, "NetworkStatus", "getConnectionInfo", []);
+};
+
+var me = new NetworkConnection();
+var timerId = null;
+var timeout = 500;
+
+channel.onCordovaReady.subscribe(function() {
+    me.getInfo(function(info) {
+        me.type = info;
+        if (info === "none") {
+            // set a timer if still offline at the end of timer send the offline event
+            timerId = setTimeout(function(){
+                cordova.fireDocumentEvent("offline");
+                timerId = null;
+            }, timeout);
+        } else {
+            // If there is a current offline event pending clear it
+            if (timerId !== null) {
+                clearTimeout(timerId);
+                timerId = null;
+            }
+            cordova.fireDocumentEvent("online");
+        }
+
+        // should only fire this once
+        if (channel.onCordovaConnectionReady.state !== 2) {
+            channel.onCordovaConnectionReady.fire();
+        }
+    },
+    function (e) {
+        // If we can't get the network info we should still tell Cordova
+        // to fire the deviceready event.
+        if (channel.onCordovaConnectionReady.state !== 2) {
+            channel.onCordovaConnectionReady.fire();
+        }
+        console.log("Error initializing Network Connection: " + e);
+    });
+});
+
+module.exports = me;
+
+});
+
+// file: lib/common/plugin/networkstatus/symbols.js
+define("cordova/plugin/networkstatus/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/network', 'navigator.network.connection', 'navigator.network.connection is deprecated. Use navigator.connection instead.');
+modulemapper.clobbers('cordova/plugin/network', 'navigator.connection');
+modulemapper.defaults('cordova/plugin/Connection', 'Connection');
+
+});
+
+// file: lib/common/plugin/notification.js
+define("cordova/plugin/notification", function(require, exports, module) {
+
+var exec = require('cordova/exec');
+var platform = require('cordova/platform');
+
+/**
+ * Provides access to notifications on the device.
+ */
+
+module.exports = {
+
+    /**
+     * Open a native alert dialog, with a customizable title and button text.
+     *
+     * @param {String} message              Message to print in the body of the alert
+     * @param {Function} completeCallback   The callback that is called when user clicks on a button.
+     * @param {String} title                Title of the alert dialog (default: Alert)
+     * @param {String} buttonLabel          Label of the close button (default: OK)
+     */
+    alert: function(message, completeCallback, title, buttonLabel) {
+        var _title = (title || "Alert");
+        var _buttonLabel = (buttonLabel || "OK");
+        exec(completeCallback, null, "Notification", "alert", [message, _title, _buttonLabel]);
+    },
+
+    /**
+     * Open a native confirm dialog, with a customizable title and button text.
+     * The result that the user selects is returned to the result callback.
+     *
+     * @param {String} message              Message to print in the body of the alert
+     * @param {Function} resultCallback     The callback that is called when user clicks on a button.
+     * @param {String} title                Title of the alert dialog (default: Confirm)
+     * @param {Array} buttonLabels          Array of the labels of the buttons (default: ['OK', 'Cancel'])
+     */
+    confirm: function(message, resultCallback, title, buttonLabels) {
+        var _title = (title || "Confirm");
+        var _buttonLabels = (buttonLabels || ["OK", "Cancel"]);
+
+        // Strings are deprecated!
+        if (typeof _buttonLabels === 'string') {
+            console.log("Notification.confirm(string, function, string, string) is deprecated.  Use Notification.confirm(string, function, string, array).");
+        }
+
+        // Some platforms take an array of button label names.
+        // Other platforms take a comma separated list.
+        // For compatibility, we convert to the desired type based on the platform.
+        if (platform.id == "android" || platform.id == "ios" || platform.id == "windowsphone" || platform.id == "blackberry10") {
+            if (typeof _buttonLabels === 'string') {
+                var buttonLabelString = _buttonLabels;
+                _buttonLabels = _buttonLabels.split(","); // not crazy about changing the var type here
+            }
+        } else {
+            if (Array.isArray(_buttonLabels)) {
+                var buttonLabelArray = _buttonLabels;
+                _buttonLabels = buttonLabelArray.toString();
+            }
+        }
+        exec(resultCallback, null, "Notification", "confirm", [message, _title, _buttonLabels]);
+    },
+
+    /**
+     * Open a native prompt dialog, with a customizable title and button text.
+     * The following results are returned to the result callback:
+     *  buttonIndex     Index number of the button selected.
+     *  input1          The text entered in the prompt dialog box.
+     *
+     * @param {String} message              Dialog message to display (default: "Prompt message")
+     * @param {Function} resultCallback     The callback that is called when user clicks on a button.
+     * @param {String} title                Title of the dialog (default: "Prompt")
+     * @param {Array} buttonLabels          Array of strings for the button labels (default: ["OK","Cancel"])
+     * @param {String} defaultText          Textbox input value (default: "Default text")
+     */
+    prompt: function(message, resultCallback, title, buttonLabels, defaultText) {
+        var _message = (message || "Prompt message");
+        var _title = (title || "Prompt");
+        var _buttonLabels = (buttonLabels || ["OK","Cancel"]);
+        var _defaultText = (defaultText || "Default text");
+        exec(resultCallback, null, "Notification", "prompt", [_message, _title, _buttonLabels, _defaultText]);
+    },
+
+    /**
+     * Causes the device to vibrate.
+     *
+     * @param {Integer} mills       The number of milliseconds to vibrate for.
+     */
+    vibrate: function(mills) {
+        exec(null, null, "Notification", "vibrate", [mills]);
+    },
+
+    /**
+     * Causes the device to beep.
+     * On Android, the default notification ringtone is played "count" times.
+     *
+     * @param {Integer} count       The number of beeps.
+     */
+    beep: function(count) {
+        exec(null, null, "Notification", "beep", [count]);
+    }
+};
+
+});
+
+// file: lib/android/plugin/notification/symbols.js
+define("cordova/plugin/notification/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/notification', 'navigator.notification');
+modulemapper.merges('cordova/plugin/android/notification', 'navigator.notification');
+
+});
+
+// file: lib/common/plugin/requestFileSystem.js
+define("cordova/plugin/requestFileSystem", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    FileError = require('cordova/plugin/FileError'),
+    FileSystem = require('cordova/plugin/FileSystem'),
+    exec = require('cordova/exec');
+
+/**
+ * Request a file system in which to store application data.
+ * @param type  local file system type
+ * @param size  indicates how much storage space, in bytes, the application expects to need
+ * @param successCallback  invoked with a FileSystem object
+ * @param errorCallback  invoked if error occurs retrieving file system
+ */
+var requestFileSystem = function(type, size, successCallback, errorCallback) {
+    argscheck.checkArgs('nnFF', 'requestFileSystem', arguments);
+    var fail = function(code) {
+        errorCallback && errorCallback(new FileError(code));
+    };
+
+    if (type < 0 || type > 3) {
+        fail(FileError.SYNTAX_ERR);
+    } else {
+        // if successful, return a FileSystem object
+        var success = function(file_system) {
+            if (file_system) {
+                if (successCallback) {
+                    // grab the name and root from the file system object
+                    var result = new FileSystem(file_system.name, file_system.root);
+                    successCallback(result);
+                }
+            }
+            else {
+                // no FileSystem object returned
+                fail(FileError.NOT_FOUND_ERR);
+            }
+        };
+        exec(success, fail, "File", "requestFileSystem", [type, size]);
+    }
+};
+
+module.exports = requestFileSystem;
+
+});
+
+// file: lib/common/plugin/resolveLocalFileSystemURI.js
+define("cordova/plugin/resolveLocalFileSystemURI", function(require, exports, module) {
+
+var argscheck = require('cordova/argscheck'),
+    DirectoryEntry = require('cordova/plugin/DirectoryEntry'),
+    FileEntry = require('cordova/plugin/FileEntry'),
+    FileError = require('cordova/plugin/FileError'),
+    exec = require('cordova/exec');
+
+/**
+ * Look up file system Entry referred to by local URI.
+ * @param {DOMString} uri  URI referring to a local file or directory
+ * @param successCallback  invoked with Entry object corresponding to URI
+ * @param errorCallback    invoked if error occurs retrieving file system entry
+ */
+module.exports = function(uri, successCallback, errorCallback) {
+    argscheck.checkArgs('sFF', 'resolveLocalFileSystemURI', arguments);
+    // error callback
+    var fail = function(error) {
+        errorCallback && errorCallback(new FileError(error));
+    };
+    // sanity check for 'not:valid:filename'
+    if(!uri || uri.split(":").length > 2) {
+        setTimeout( function() {
+            fail(FileError.ENCODING_ERR);
+        },0);
+        return;
+    }
+    // if successful, return either a file or directory entry
+    var success = function(entry) {
+        var result;
+        if (entry) {
+            if (successCallback) {
+                // create appropriate Entry object
+                result = (entry.isDirectory) ? new DirectoryEntry(entry.name, entry.fullPath) : new FileEntry(entry.name, entry.fullPath);
+                successCallback(result);
+            }
+        }
+        else {
+            // no Entry object returned
+            fail(FileError.NOT_FOUND_ERR);
+        }
+    };
+
+    exec(success, fail, "File", "resolveLocalFileSystemURI", [uri]);
+};
+
+});
+
+// file: lib/common/plugin/splashscreen.js
+define("cordova/plugin/splashscreen", function(require, exports, module) {
+
+var exec = require('cordova/exec');
+
+var splashscreen = {
+    show:function() {
+        exec(null, null, "SplashScreen", "show", []);
+    },
+    hide:function() {
+        exec(null, null, "SplashScreen", "hide", []);
+    }
+};
+
+module.exports = splashscreen;
+
+});
+
+// file: lib/common/plugin/splashscreen/symbols.js
+define("cordova/plugin/splashscreen/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/splashscreen', 'navigator.splashscreen');
+
+});
+
+// file: lib/common/symbols.js
+define("cordova/symbols", function(require, exports, module) {
+
+var modulemapper = require('cordova/modulemapper');
+
+// Use merges here in case others symbols files depend on this running first,
+// but fail to declare the dependency with a require().
+modulemapper.merges('cordova', 'cordova');
+modulemapper.clobbers('cordova/exec', 'cordova.exec');
+modulemapper.clobbers('cordova/exec', 'Cordova.exec');
+
+});
+
+// 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
+
+(function (context) {
+    if (context._cordovaJsLoaded) {
+        throw new Error('cordova.js included multiple times.');
+    }
+    context._cordovaJsLoaded = true;
+
+    var channel = require('cordova/channel');
+    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 (context.navigator) {
+        context.navigator = replaceNavigator(context.navigator);
+    }
+
+    // _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();
+    }
+
+    /**
+     * Create all cordova objects once native side is ready.
+     */
+    channel.join(function() {
+        // Call the platform-specific initialization
+        require('cordova/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.
+        // This join call is deliberately made after platform.initialize() in
+        // order that plugins may manipulate channel.deviceReadyChannelsArray
+        // if necessary.
+        channel.join(function() {
+            require('cordova').fireDocumentEvent('deviceready');
+        }, channel.deviceReadyChannelsArray);
+
+    }, platformInitChannelsArray);
+
+}(window));
+
+// file: lib/scripts/bootstrap-android.js
+
+require('cordova/channel').onNativeReady.fire();
+
+// file: lib/scripts/plugin_loader.js
+
+// 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.
+(function (context) {
+    // To be populated with the handler by handlePluginsObject.
+    var onScriptLoadingComplete;
+
+    var scriptCounter = 0;
+    function scriptLoadedCallback() {
+        scriptCounter--;
+        if (scriptCounter === 0) {
+            onScriptLoadingComplete && onScriptLoadingComplete();
+        }
+    }
+
+    // Helper function to inject a <script> tag.
+    function injectScript(path) {
+        scriptCounter++;
+        var script = document.createElement("script");
+        script.onload = scriptLoadedCallback;
+        script.src = path;
+        document.head.appendChild(script);
+    }
+
+    // Called when:
+    // * There are plugins defined and all plugins are finished loading.
+    // * There are no plugins to load.
+    function finishPluginLoading() {
+        context.cordova.require('cordova/channel').onPluginsReady.fire();
+    }
+
+    // Handler for the cordova_plugins.json 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 XHR response handler will just call finishPluginLoading().
+    function handlePluginsObject(modules, path) {
+        // First create the callback for when all plugins are loaded.
+        var mapper = context.cordova.require('cordova/modulemapper');
+        onScriptLoadingComplete = function() {
+            // Loop through all the plugins and then through their clobbers and merges.
+            for (var i = 0; i < modules.length; i++) {
+                var module = modules[i];
+                if (!module) continue;
+
+                if (module.clobbers && module.clobbers.length) {
+                    for (var j = 0; j < module.clobbers.length; j++) {
+                        mapper.clobbers(module.id, module.clobbers[j]);
+                    }
+                }
+
+                if (module.merges && module.merges.length) {
+                    for (var k = 0; k < module.merges.length; k++) {
+                        mapper.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)) {
+                    context.cordova.require(module.id);
+                }
+            }
+
+            finishPluginLoading();
+        };
+
+        // Now inject the scripts.
+        for (var i = 0; i < modules.length; i++) {
+            injectScript(path + modules[i].file);
+        }
+    }
+
+    // Find the root of the app
+    var path = '';
+    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;
+        }
+    }
+    // Try to XHR the cordova_plugins.json file asynchronously.
+    var xhr = new XMLHttpRequest();
+    xhr.onload = function() {
+        // If the response is a JSON string which composes an array, call handlePluginsObject.
+        // If the request fails, or the response is not a JSON array, just call finishPluginLoading.
+        var obj;
+        try {
+            obj = (this.status == 0 || this.status == 200) && this.responseText && JSON.parse(this.responseText);
+        } catch (err) {
+            // obj will be undefined.
+        }
+        if (Array.isArray(obj) && obj.length > 0) {
+            handlePluginsObject(obj, path);
+        } else {
+            finishPluginLoading();
+        }
+    };
+    xhr.onerror = function() {
+        finishPluginLoading();
+    };
+    var plugins_json = path + 'cordova_plugins.json';
+    try { // we commented we were going to try, so let us actually try and catch
+        xhr.open('GET', plugins_json, true); // Async
+        xhr.send();
+    } catch(err){
+        finishPluginLoading();
+    }
+}(window));
+
+
+})();
\ No newline at end of file
diff --git a/test/assets/www/cordova.js b/test/assets/www/cordova.js
new file mode 100755
index 0000000..4fc9746
--- /dev/null
+++ b/test/assets/www/cordova.js
@@ -0,0 +1,2115 @@
+// Platform: android
+// 2.7.0rc1-169-g87cc336
+/*
+ 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 = '2.7.0rc1-169-g87cc336';
+// file: lib/scripts/require.js
+
+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');
+
+/**
+ * Listen for DOMContentLoaded and notify our channel subscribers.
+ */
+document.addEventListener('DOMContentLoaded', function() {
+    channel.onDOMContentLoaded.fire();
+}, false);
+if (document.readyState == 'complete' || document.readyState == 'interactive') {
+    channel.onDOMContentLoaded.fire();
+}
+
+/**
+ * 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;
+}
+
+if(typeof window.console === "undefined") {
+    window.console = {
+        log:function(){}
+    };
+}
+// there are places in the framework where we call `warn` also, so we should make sure it exists
+if(typeof window.console.warn === "undefined") {
+    window.console.warn = function(msg) {
+        this.log("warn: " + msg);
+    }
+}
+
+var cordova = {
+    define:define,
+    require:require,
+    /**
+     * 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);
+            }
+        });
+    }
+};
+
+// 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');
+
+module.exports = cordova;
+
+});
+
+// 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/common/commandProxy.js
+define("cordova/commandProxy", function(require, exports, module) {
+
+
+// internal map of proxy function
+var CommandProxyMap = {};
+
+module.exports = {
+
+    // example: cordova.commandProxy.add("Accelerometer",{getCurrentAcceleration: function(successCallback, errorCallback, options) {...},...);
+    add:function(id,proxyObj) {
+        console.log("adding proxy for " + id);
+        CommandProxyMap[id] = proxyObj;
+        return proxyObj;
+    },
+
+    // cordova.commandProxy.remove("Accelerometer");
+    remove:function(id) {
+        var proxy = CommandProxyMap[id];
+        delete CommandProxyMap[id];
+        CommandProxyMap[id] = null;
+        return proxy;
+    },
+
+    get:function(service,action) {
+        return ( CommandProxyMap[service] ? CommandProxyMap[service][action] : null );
+    }
+};
+});
+
+// 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/plugin/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] = utils.encodeBase64(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 pollOnce() {
+    var msg = nativeApiProvider.get().retrieveJsMessages();
+    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', pollOnce, false);
+    window.addEventListener('offline', pollOnce, 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/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.loadMatchingModules = function(matchingRegExp) {
+    for (var k in moduleMap) {
+        if (matchingRegExp.exec(k)) {
+            require(k);
+        }
+    }
+};
+
+exports.reset();
+
+
+});
+
+// file: lib/android/platform.js
+define("cordova/platform", function(require, exports, module) {
+
+module.exports = {
+    id: "android",
+    initialize:function() {
+        var channel = require("cordova/channel"),
+            cordova = require('cordova'),
+            exec = require('cordova/exec'),
+            modulemapper = require('cordova/modulemapper');
+
+        modulemapper.loadMatchingModules(/cordova.*\/symbols$/);
+        modulemapper.clobbers('cordova/plugin/android/app', 'navigator.app');
+
+        modulemapper.mapModules(window);
+
+        // 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.join(function() {
+            exec(null, null, "App", "show", []);
+        }, [channel.onCordovaReady]);
+    }
+};
+
+});
+
+// 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/android/plugin/android/nativeapiprovider.js
+define("cordova/plugin/android/nativeapiprovider", function(require, exports, module) {
+
+/**
+ * Exports the ExposedJsApi.java object if available, otherwise exports the PromptBasedNativeApi.
+ */
+
+var nativeApi = this._cordovaNative || require('cordova/plugin/android/promptbasednativeapi');
+var currentApi = nativeApi;
+
+module.exports = {
+    get: function() { return currentApi; },
+    setPreferPrompt: function(value) {
+        currentApi = value ? require('cordova/plugin/android/promptbasednativeapi') : nativeApi;
+    },
+    // Used only by tests.
+    set: function(value) {
+        currentApi = value;
+    }
+};
+
+});
+
+// file: lib/android/plugin/android/promptbasednativeapi.js
+define("cordova/plugin/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() {
+        return prompt('', 'gap_poll:');
+    }
+};
+
+});
+
+// file: lib/android/plugin/android/storage.js
+define("cordova/plugin/android/storage", function(require, exports, module) {
+
+var utils = require('cordova/utils'),
+    exec = require('cordova/exec'),
+    channel = require('cordova/channel');
+
+var queryQueue = {};
+
+/**
+ * SQL result set object
+ * PRIVATE METHOD
+ * @constructor
+ */
+var DroidDB_Rows = function() {
+    this.resultSet = [];    // results array
+    this.length = 0;        // number of rows
+};
+
+/**
+ * Get item from SQL result set
+ *
+ * @param row           The row number to return
+ * @return              The row object
+ */
+DroidDB_Rows.prototype.item = function(row) {
+    return this.resultSet[row];
+};
+
+/**
+ * SQL result set that is returned to user.
+ * PRIVATE METHOD
+ * @constructor
+ */
+var DroidDB_Result = function() {
+    this.rows = new DroidDB_Rows();
+};
+
+/**
+ * Callback from native code when query is complete.
+ * PRIVATE METHOD
+ *
+ * @param id   Query id
+ */
+function completeQuery(id, data) {
+    var query = queryQueue[id];
+    if (query) {
+        try {
+            delete queryQueue[id];
+
+            // Get transaction
+            var tx = query.tx;
+
+            // If transaction hasn't failed
+            // Note: We ignore all query results if previous query
+            //       in the same transaction failed.
+            if (tx && tx.queryList[id]) {
+
+                // Save query results
+                var r = new DroidDB_Result();
+                r.rows.resultSet = data;
+                r.rows.length = data.length;
+                try {
+                    if (typeof query.successCallback === 'function') {
+                        query.successCallback(query.tx, r);
+                    }
+                } catch (ex) {
+                    console.log("executeSql error calling user success callback: "+ex);
+                }
+
+                tx.queryComplete(id);
+            }
+        } catch (e) {
+            console.log("executeSql error: "+e);
+        }
+    }
+}
+
+/**
+ * Callback from native code when query fails
+ * PRIVATE METHOD
+ *
+ * @param reason            Error message
+ * @param id                Query id
+ */
+function failQuery(reason, id) {
+    var query = queryQueue[id];
+    if (query) {
+        try {
+            delete queryQueue[id];
+
+            // Get transaction
+            var tx = query.tx;
+
+            // If transaction hasn't failed
+            // Note: We ignore all query results if previous query
+            //       in the same transaction failed.
+            if (tx && tx.queryList[id]) {
+                tx.queryList = {};
+
+                try {
+                    if (typeof query.errorCallback === 'function') {
+                        query.errorCallback(query.tx, reason);
+                    }
+                } catch (ex) {
+                    console.log("executeSql error calling user error callback: "+ex);
+                }
+
+                tx.queryFailed(id, reason);
+            }
+
+        } catch (e) {
+            console.log("executeSql error: "+e);
+        }
+    }
+}
+
+/**
+ * SQL query object
+ * PRIVATE METHOD
+ *
+ * @constructor
+ * @param tx                The transaction object that this query belongs to
+ */
+var DroidDB_Query = function(tx) {
+
+    // Set the id of the query
+    this.id = utils.createUUID();
+
+    // Add this query to the queue
+    queryQueue[this.id] = this;
+
+    // Init result
+    this.resultSet = [];
+
+    // Set transaction that this query belongs to
+    this.tx = tx;
+
+    // Add this query to transaction list
+    this.tx.queryList[this.id] = this;
+
+    // Callbacks
+    this.successCallback = null;
+    this.errorCallback = null;
+
+};
+
+/**
+ * Transaction object
+ * PRIVATE METHOD
+ * @constructor
+ */
+var DroidDB_Tx = function() {
+
+    // Set the id of the transaction
+    this.id = utils.createUUID();
+
+    // Callbacks
+    this.successCallback = null;
+    this.errorCallback = null;
+
+    // Query list
+    this.queryList = {};
+};
+
+/**
+ * Mark query in transaction as complete.
+ * If all queries are complete, call the user's transaction success callback.
+ *
+ * @param id                Query id
+ */
+DroidDB_Tx.prototype.queryComplete = function(id) {
+    delete this.queryList[id];
+
+    // If no more outstanding queries, then fire transaction success
+    if (this.successCallback) {
+        var count = 0;
+        var i;
+        for (i in this.queryList) {
+            if (this.queryList.hasOwnProperty(i)) {
+                count++;
+            }
+        }
+        if (count === 0) {
+            try {
+                this.successCallback();
+            } catch(e) {
+                console.log("Transaction error calling user success callback: " + e);
+            }
+        }
+    }
+};
+
+/**
+ * Mark query in transaction as failed.
+ *
+ * @param id                Query id
+ * @param reason            Error message
+ */
+DroidDB_Tx.prototype.queryFailed = function(id, reason) {
+
+    // The sql queries in this transaction have already been run, since
+    // we really don't have a real transaction implemented in native code.
+    // However, the user callbacks for the remaining sql queries in transaction
+    // will not be called.
+    this.queryList = {};
+
+    if (this.errorCallback) {
+        try {
+            this.errorCallback(reason);
+        } catch(e) {
+            console.log("Transaction error calling user error callback: " + e);
+        }
+    }
+};
+
+/**
+ * Execute SQL statement
+ *
+ * @param sql                   SQL statement to execute
+ * @param params                Statement parameters
+ * @param successCallback       Success callback
+ * @param errorCallback         Error callback
+ */
+DroidDB_Tx.prototype.executeSql = function(sql, params, successCallback, errorCallback) {
+
+    // Init params array
+    if (typeof params === 'undefined') {
+        params = [];
+    }
+
+    // Create query and add to queue
+    var query = new DroidDB_Query(this);
+    queryQueue[query.id] = query;
+
+    // Save callbacks
+    query.successCallback = successCallback;
+    query.errorCallback = errorCallback;
+
+    // Call native code
+    exec(null, null, "Storage", "executeSql", [sql, params, query.id]);
+};
+
+var DatabaseShell = function() {
+};
+
+/**
+ * Start a transaction.
+ * Does not support rollback in event of failure.
+ *
+ * @param process {Function}            The transaction function
+ * @param successCallback {Function}
+ * @param errorCallback {Function}
+ */
+DatabaseShell.prototype.transaction = function(process, errorCallback, successCallback) {
+    var tx = new DroidDB_Tx();
+    tx.successCallback = successCallback;
+    tx.errorCallback = errorCallback;
+    try {
+        process(tx);
+    } catch (e) {
+        console.log("Transaction error: "+e);
+        if (tx.errorCallback) {
+            try {
+                tx.errorCallback(e);
+            } catch (ex) {
+                console.log("Transaction error calling user error callback: "+e);
+            }
+        }
+    }
+};
+
+/**
+ * Open database
+ *
+ * @param name              Database name
+ * @param version           Database version
+ * @param display_name      Database display name
+ * @param size              Database size in bytes
+ * @return                  Database object
+ */
+var DroidDB_openDatabase = function(name, version, display_name, size) {
+    exec(null, null, "Storage", "openDatabase", [name, version, display_name, size]);
+    var db = new DatabaseShell();
+    return db;
+};
+
+
+module.exports = {
+  openDatabase:DroidDB_openDatabase,
+  failQuery:failQuery,
+  completeQuery:completeQuery
+};
+
+});
+
+// file: lib/android/plugin/android/storage/openDatabase.js
+define("cordova/plugin/android/storage/openDatabase", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper'),
+    storage = require('cordova/plugin/android/storage');
+
+var originalOpenDatabase = modulemapper.getOriginalSymbol(window, 'openDatabase');
+
+module.exports = function(name, version, desc, size) {
+    // First patch WebSQL if necessary
+    if (!originalOpenDatabase) {
+        // Not defined, create an openDatabase function for all to use!
+        return storage.openDatabase.apply(this, arguments);
+    }
+
+    // Defined, but some Android devices will throw a SECURITY_ERR -
+    // so we wrap the whole thing in a try-catch and shim in our own
+    // if the device has Android bug 16175.
+    try {
+        return originalOpenDatabase(name, version, desc, size);
+    } catch (ex) {
+        if (ex.code !== 18) {
+            throw ex;
+        }
+    }
+    return storage.openDatabase(name, version, desc, size);
+};
+
+
+
+});
+
+// file: lib/android/plugin/android/storage/symbols.js
+define("cordova/plugin/android/storage/symbols", function(require, exports, module) {
+
+
+var modulemapper = require('cordova/modulemapper');
+
+modulemapper.clobbers('cordova/plugin/android/storage/openDatabase', 'openDatabase');
+
+
+});
+
+// file: lib/common/plugin/echo.js
+define("cordova/plugin/echo", function(require, exports, module) {
+
+var exec = require('cordova/exec'),
+    utils = require('cordova/utils');
+
+/**
+ * Sends the given message through exec() to the Echo plugin, which sends it back to the successCallback.
+ * @param successCallback  invoked with a FileSystem object
+ * @param errorCallback  invoked if error occurs retrieving file system
+ * @param message  The string to be echoed.
+ * @param forceAsync  Whether to force an async return value (for testing native->js bridge).
+ */
+module.exports = function(successCallback, errorCallback, message, forceAsync) {
+    var action = 'echo';
+    var messageIsMultipart = (utils.typeName(message) == "Array");
+    var args = messageIsMultipart ? message : [message];
+
+    if (utils.typeName(message) == 'ArrayBuffer') {
+        if (forceAsync) {
+            console.warn('Cannot echo ArrayBuffer with forced async, falling back to sync.');
+        }
+        action += 'ArrayBuffer';
+    } else if (messageIsMultipart) {
+        if (forceAsync) {
+            console.warn('Cannot echo MultiPart Array with forced async, falling back to sync.');
+        }
+        action += 'MultiPart';
+    } else if (forceAsync) {
+        action += 'Async';
+    }
+
+    exec(successCallback, errorCallback, "Echo", action, args);
+};
+
+
+});
+
+// file: lib/common/pluginloader.js
+define("cordova/pluginloader", function(require, exports, module) {
+
+var channel = require('cordova/channel');
+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) {
+    // 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();
+}
+
+// Called when:
+// * There are plugins defined and all plugins are finished loading.
+// * There are no plugins to load.
+function finishPluginLoading() {
+    channel.onPluginsReady.fire();
+}
+
+// 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) {
+    // Now inject the scripts.
+    var scriptCounter = moduleList.length;
+    function scriptLoadedCallback() {
+        if (!--scriptCounter) {
+            onScriptLoadingComplete(moduleList);
+        }
+    }
+
+    for (var i = 0; i < moduleList.length; i++) {
+        injectScript(path + moduleList[i].file, scriptLoadedCallback);
+    }
+}
+
+function injectPluginScript(pathPrefix) {
+    injectScript(pathPrefix + 'cordova_plugins.js', function(){
+        try {
+            var moduleList = require("cordova/plugin_list");
+            handlePluginsObject(pathPrefix, moduleList);
+        } 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() {
+    var pathPrefix = findCordovaPath();
+    if (pathPrefix === null) {
+        console.log('Could not find cordova.js script tag. Plugin loading may fail.');
+        pathPrefix = '';
+    }
+    injectPluginScript(pathPrefix);
+};
+
+
+});
+
+// file: lib/common/symbols.js
+define("cordova/symbols", function(require, exports, module) {
+
+var modulemapper = require('cordova/modulemapper');
+
+// Use merges here in case others symbols files depend on this running first,
+// but fail to declare the dependency with a require().
+modulemapper.merges('cordova', 'cordova');
+modulemapper.clobbers('cordova/exec', 'cordova.exec');
+modulemapper.clobbers('cordova/exec', 'Cordova.exec');
+
+});
+
+// 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
+
+(function (context) {
+    if (context._cordovaJsLoaded) {
+        throw new Error('cordova.js included multiple times.');
+    }
+    context._cordovaJsLoaded = true;
+
+    var channel = require('cordova/channel');
+    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 (context.navigator) {
+        context.navigator = replaceNavigator(context.navigator);
+    }
+
+    // _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();
+    }
+
+    /**
+     * Create all cordova objects once native side is ready.
+     */
+    channel.join(function() {
+        // Call the platform-specific initialization
+        require('cordova/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.
+        // This join call is deliberately made after platform.initialize() in
+        // order that plugins may manipulate channel.deviceReadyChannelsArray
+        // if necessary.
+        channel.join(function() {
+            require('cordova').fireDocumentEvent('deviceready');
+        }, channel.deviceReadyChannelsArray);
+
+    }, platformInitChannelsArray);
+
+    // Don't attempt to load when running unit tests.
+    if (typeof XMLHttpRequest != 'undefined') {
+        pluginloader.load();
+    }
+}(window));
+
+// file: lib/scripts/bootstrap-android.js
+
+// Tell the native code that a page change has occurred.
+require('cordova/exec')(null, null, 'PluginManager', 'startup', []);
+require('cordova/channel').onNativeReady.fire();
+
+})();
\ No newline at end of file
diff --git a/test/assets/www/fullscreen/index.html b/test/assets/www/fullscreen/index.html
new file mode 100755
index 0000000..8fa24da
--- /dev/null
+++ b/test/assets/www/fullscreen/index.html
@@ -0,0 +1,42 @@
+<!--
+         Licensed to the Apache Software Foundation (ASF) under one
+         or more contributor license agreements.  See the NOTICE file
+         distributed with this work for additional information
+         regarding copyright ownership.  The ASF licenses this file
+         to you under the Apache License, Version 2.0 (the
+         "License"); you may not use this file except in compliance
+         with the License.  You may obtain a copy of the License at
+
+           http://www.apache.org/licenses/LICENSE-2.0
+
+         Unless required by applicable law or agreed to in writing,
+         software distributed under the License is distributed on an
+         "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+         KIND, either express or implied.  See the License for the
+         specific language governing permissions and limitations
+         under the License.
+-->
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta name="viewport" content="width=320; user-scalable=no" />
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>Cordova Tests</title>
+      <link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+      <script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+      <script type="text/javascript" charset="utf-8" src="../main.js"></script>
+  </head>
+  <body onload="init();" id="stage" class="theme">
+    <h1>Full Screen Test</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;
+                   </span>, Color Depth: <span id="colorDepth"></span></h4>
+     </div>
+     <div id="info">
+     The app should take over the entire screen. <br>
+     The top Android status bar should not be shown.
+     </div>
+  </body>
+</html>
diff --git a/test/assets/www/htmlnotfound/error.html b/test/assets/www/htmlnotfound/error.html
new file mode 100755
index 0000000..c1f1329
--- /dev/null
+++ b/test/assets/www/htmlnotfound/error.html
@@ -0,0 +1,19 @@
+<!--
+         Licensed to the Apache Software Foundation (ASF) under one$
+         or more contributor license agreements.  See the NOTICE file$
+         distributed with this work for additional information$
+         regarding copyright ownership.  The ASF licenses this file$
+         to you under the Apache License, Version 2.0 (the$
+         "License"); you may not use this file except in compliance$
+         with the License.  You may obtain a copy of the License at$
+$
+           http://www.apache.org/licenses/LICENSE-2.0$
+$
+         Unless required by applicable law or agreed to in writing,$
+         software distributed under the License is distributed on an$
+         "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY$
+         KIND, either express or implied.  See the License for the$
+         specific language governing permissions and limitations$
+         under the License.$
+-->
+This is an error page.
diff --git a/test/assets/www/iframe/index.html b/test/assets/www/iframe/index.html
new file mode 100755
index 0000000..0b3c3d7
--- /dev/null
+++ b/test/assets/www/iframe/index.html
@@ -0,0 +1,51 @@
+<!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 name="viewport" content="width=320; user-scalable=no" />
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>Cordova Tests</title>
+      <link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+      <script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+      <script type="text/javascript" charset="utf-8" src="../main.js"></script>
+      <script>
+      function loadUrl(url) {
+    	  document.getElementById('iframe').src = url;
+      }</script>
+  </head>
+  <body onload="init();" id="stage" class="theme">
+    <h1>IFrame</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;
+                   </span>, Color Depth: <span id="colorDepth"></span></h4>
+    </div>
+    <div id="info">
+    Press the two buttons:<br>
+    1. Google Maps should display in iframe.<br>
+    2. Page 2 replaces current page.<br>
+    (NOTE: THIS BEHAVIOR IS WRONG - AND NEEDS TO BE FIXED IN FUTURE RELEASE.)
+    </div>
+    <iframe id="iframe" src="" width="90%" height="200px"></iframe>
+    <a href="javascript:" class="btn large" onclick="loadUrl('http://maps.google.com/maps?output=embed');">Google Maps</a>
+    <a href="javascript:" class="btn large" onclick="loadUrl('index2.html');">Page 2</a>
+  </body>
+</html>
diff --git a/test/assets/www/iframe/index2.html b/test/assets/www/iframe/index2.html
new file mode 100755
index 0000000..abe692b
--- /dev/null
+++ b/test/assets/www/iframe/index2.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 name="viewport" content="width=320; user-scalable=no" />
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>Cordova Tests</title>
+      <link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+      <script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+      <script type="text/javascript" charset="utf-8" src="../main.js"></script>
+  </head>
+  <body onload="init();" id="stage" class="theme">
+    <h1>IFrame window</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;
+                   </span>, Color Depth: <span id="colorDepth"></span></h4>
+     </div>
+     <div id="info">
+     This should display a Cordova page inside an iframe.  The info above should be filled out.
+     (NOTE: THIS DOES NOT WORK AND NEEDS TO BE FIXED IN FUTURE RELEASE.)
+     </div>
+  </body>
+</html>
diff --git a/test/assets/www/index.html b/test/assets/www/index.html
new file mode 100755
index 0000000..13f0825
--- /dev/null
+++ b/test/assets/www/index.html
@@ -0,0 +1,68 @@
+<!--
+         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.
+-->
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta name="viewport" content="width=320; user-scalable=no" />
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>Cordova Tests</title>
+      <link rel="stylesheet" href="master.css" type="text/css" media="screen" title="no title">
+      <script type="text/javascript" charset="utf-8" src="cordova.js"></script>
+      <script type="text/javascript" charset="utf-8" src="main.js"></script>
+      <script>
+      function startActivity(className) {
+          cordova.exec(function() {console.log("Success");}, function(e) {console.log("Error: "+e);}, "Activity", "start", [className]);
+      };
+      
+      localStorage.lifecyclestatus = "";
+      localStorage.backgroundstatus = "";
+      </script>
+
+  </head>
+  <body onload="init();" id="stage" class="theme">
+    <h1>Cordova Android Tests</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;
+                   </span>, Color Depth: <span id="colorDepth"></span></h4>
+        <h4>Cordova Version: <span id="cordova">&nbsp;</span></h4>
+     </div>
+    <div id="info">
+        <h4>Run each of the test activities below:</h4>
+    </div>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.jqmtabbackbutton');">Backbutton jQM tab</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.backbuttonmultipage');">Backbutton with multiple pages</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.backgroundcolor');">Background Color</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.basicauth');">Basic Authentication</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.errorurl');">Error URL</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.fullscreen');">Full Screen</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.htmlnotfound');">HTML not found</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.iframe');">IFrame</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.lifecycle');">Lifecycle</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.loading');">Loading indicator</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.menus');">Menus</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.background');">No multitasking</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.splashscreen');">Splash screen</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.timeout');">Load timeout</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.userwebview');">User WebView/Client/Chrome</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.whitelist');">Whitelist</button>
+    <button class="btn large" onclick="startActivity('org.apache.cordova.test.actions.xhr');">XHR</button>
+  </body>
+</html>
diff --git a/test/assets/www/jqmtabbackbutton/index.html b/test/assets/www/jqmtabbackbutton/index.html
new file mode 100755
index 0000000..057cea7
--- /dev/null
+++ b/test/assets/www/jqmtabbackbutton/index.html
@@ -0,0 +1,67 @@
+<!--
+         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 http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
+<title>Backbutton</title> 
+
+<link rel="stylesheet" href="http://code.jquery.com/mobile/1.0.1/jquery.mobile-1.0.1.min.css" />
+<script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
+<script src="http://code.jquery.com/mobile/1.0.1/jquery.mobile-1.0.1.min.js"></script>
+<script type="text/javascript" src="../cordova.js"></script>
+
+<script>
+document.addEventListener("deviceready", onDeviceReady, false);
+
+// Without backbutton handler, each tab loaded will be popped off history until history is empty, then it will exit app.
+function handleBackButton() {
+    alert("Back Button Pressed! - exiting app");
+    navigator.app.exitApp();
+}
+
+function onDeviceReady() {
+    console.log("onDeviceReady()");
+    document.addEventListener("backbutton", handleBackButton, false);
+}
+
+
+</script>
+</head> 
+<body>
+<div data-role="page" id="tabTab">
+    <div data-role="header">
+        <h1>Main</h1>
+    </div>
+    <div data-role="content" id="tabContent">
+        To test, press several tabs.<br>
+        The "backbutton" can be pressed any time to exit app.
+    </div>
+    <div data-role="footer" data-position="fixed">
+        <div data-role="navbar">
+            <ul>
+                <li><a href="tab1.html" data-transition="none">Tab 1</a>
+                </li>
+                <li><a href="tab2.html" data-transition="none">Tab 2</a>
+                </li>
+                <li><a href="tab3.html" data-transition="none">Tab 3</a>
+                </li>
+            </ul>
+        </div>
+    </div>
+</div>
diff --git a/test/assets/www/jqmtabbackbutton/tab1.html b/test/assets/www/jqmtabbackbutton/tab1.html
new file mode 100755
index 0000000..ea7e3a9
--- /dev/null
+++ b/test/assets/www/jqmtabbackbutton/tab1.html
@@ -0,0 +1,47 @@
+<!--
+         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 http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
+<title>Tab 1</title> 
+
+</head> 
+<body>
+<div data-role="page" id="tab1Tab">
+    <div data-role="header">
+        <h1>Tab 1</h1>
+    </div>
+    <div data-role="content" id="tab1Content">
+Tab 1 content.
+    </div>
+    <div data-role="footer" data-position="fixed">
+        <div data-role="navbar">
+            <ul>
+                <li><a href="tab1.html" data-transition="none">Tab 1</a>
+                </li>
+                <li><a href="tab2.html" data-transition="none">Tab 2</a>
+                </li>
+                <li><a href="tab3.html" data-transition="none">Tab 3</a>
+                </li>
+            </ul>
+        </div>
+        <!-- /navbar -->
+    </div>
+    <!-- /footer -->
+</div>
diff --git a/test/assets/www/jqmtabbackbutton/tab2.html b/test/assets/www/jqmtabbackbutton/tab2.html
new file mode 100755
index 0000000..afd7c02
--- /dev/null
+++ b/test/assets/www/jqmtabbackbutton/tab2.html
@@ -0,0 +1,48 @@
+<!--
+         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 http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
+<title>Tab 2</title> 
+
+
+</head> 
+<body>
+<div data-role="page" id="tab2Tab">
+    <div data-role="header">
+        <h1>Tab 2</h1>
+    </div>
+    <div data-role="content" id="tab2Content">
+Tab 2 content.
+    </div>
+    <div data-role="footer" data-position="fixed">
+        <div data-role="navbar">
+            <ul>
+                <li><a href="tab1.html" data-transition="none">Tab 1</a>
+                </li>
+                <li><a href="tab2.html" data-transition="none">Tab 2</a>
+                </li>
+                <li><a href="tab3.html" data-transition="none">Tab 3</a>
+                </li>
+            </ul>
+        </div>
+        <!-- /navbar -->
+    </div>
+    <!-- /footer -->
+</div>
diff --git a/test/assets/www/jqmtabbackbutton/tab3.html b/test/assets/www/jqmtabbackbutton/tab3.html
new file mode 100755
index 0000000..9ed47df
--- /dev/null
+++ b/test/assets/www/jqmtabbackbutton/tab3.html
@@ -0,0 +1,48 @@
+<!--
+         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 http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
+<title>Tab 3</title> 
+
+
+</head> 
+<body>
+<div data-role="page" id="tab3Tab">
+    <div data-role="header">
+        <h1>Tab 3</h1>
+    </div>
+    <div data-role="content" id="tab3Content">
+Tab 3 content.
+    </div>
+    <div data-role="footer" data-position="fixed">
+        <div data-role="navbar">
+            <ul>
+                <li><a href="tab1.html" data-transition="none">Tab 1</a>
+                </li>
+                <li><a href="tab2.html" data-transition="none">Tab 2</a>
+                </li>
+                <li><a href="tab3.html" data-transition="none">Tab 3</a>
+                </li>
+            </ul>
+        </div>
+        <!-- /navbar -->
+    </div>
+    <!-- /footer -->
+</div>
diff --git a/test/assets/www/lifecycle/index.html b/test/assets/www/lifecycle/index.html
new file mode 100755
index 0000000..d965924
--- /dev/null
+++ b/test/assets/www/lifecycle/index.html
@@ -0,0 +1,126 @@
+<!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>
+<head>
+<meta http-equiv="Content-type" content="text/html; charset=utf-8">
+<title>Lifecycle Page 1</title>
+<link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+<script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+
+<script type="text/javascript" charset="utf-8">
+
+	function onLoad() {
+		console.log("Page1: onload");
+		log("Page1: onload @ " + new Date().toLocaleTimeString());
+		document.addEventListener("deviceready", onDeviceReady, false);
+	}
+
+	function onUnLoaded() {
+		console.log("Page1: onunload");
+		log("Page1: onunload @ " + new Date().toLocaleTimeString());
+	}
+
+	function onDeviceReady() {
+		// Register the event listener
+		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;
+
+		document.addEventListener("pause", onPause, false);
+		document.addEventListener("resume", onResume, false);
+
+	    window.setInterval(function() {
+	        log("Page1: Running");
+	    }, 2000);
+}
+
+	function onPause() {
+		console.log("Page1: onpause");
+		log("Page1: onpause @ " + new Date().toLocaleTimeString());
+	}
+
+	function onResume() {
+		console.log("Page1: onresume");
+		log("Page1: onresume @ " + new Date().toLocaleTimeString());
+	}
+
+	function log(s) {
+		var el = document.getElementById('status');
+		var status = el.innerHTML + s + "<br>";
+		el.innerHTML = status;
+		localStorage.lifecyclestatus = status;
+	}
+	
+	function clearStatus() {
+		console.log("clear()");
+		localStorage.lifecyclestatus = "";
+		document.getElementById('status').innerHTML = "";
+	}
+		
+</script>
+</head>
+<body onload="onLoad()" onunload="onUnLoaded()"  id="stage" class="theme">
+	<h1>Events</h1>
+	<div id="info">
+		<h4>
+			Platform: <span id="platform"> &nbsp;</span>, Version: <span
+				id="version">&nbsp;</span>
+		</h4>
+		<h4>
+			UUID: <span id="uuid"> &nbsp;</span>, Name: <span id="name">&nbsp;</span>
+		</h4>
+		<h4>
+			Width: <span id="width"> &nbsp;</span>, Height: <span id="height">&nbsp;
+			</span>, Color Depth: <span id="colorDepth"></span>
+		</h4>
+	</div>
+	<div id="info">
+	   <h4>Test 1</h4>
+	   Press "Home" button, then return to this app to see pause/resume.<br>
+       There should be "Running" entries between pause and resume since app continues to run in the background.
+       <h4>Test 2</h4>
+       Press "Load new page" button to load a new Cordova page.<br>
+       When returning, you should see 
+       <ul>
+            <li>Page2: onunload</li>
+            <li>Page1: onload</li>
+            <li>Page1: Running</li>
+       </ul>
+	</div>
+	<div id="info">
+	   <h4>Info for event testing:</h4>
+	   <div id="status"></div>
+	</div>
+    
+    <a href="index2.html" class="btn large" >Load new page</a>
+    <a href="javascript:" class="btn large" onclick="clearStatus();">Clear status</a>
+    
+    <script>
+    document.getElementById('status').innerHTML = localStorage.lifecyclestatus;
+    </script>
+</body>
+</html>
+
+
diff --git a/test/assets/www/lifecycle/index2.html b/test/assets/www/lifecycle/index2.html
new file mode 100755
index 0000000..bfa3ed8
--- /dev/null
+++ b/test/assets/www/lifecycle/index2.html
@@ -0,0 +1,122 @@
+<!--
+         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.$
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+<head>
+<meta http-equiv="Content-type" content="text/html; charset=utf-8">
+<title>Lifecycle Page 2</title>
+<link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+<script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+
+<script type="text/javascript" charset="utf-8">
+
+    function onLoad() {
+        console.log("Page2: onload");
+        log("Page2: onload @ " + new Date().toLocaleTimeString());
+        document.addEventListener("deviceready", onDeviceReady, false);
+    }
+
+    function onUnLoaded() {
+        console.log("Page2: onunload");
+        log("Page2: onunload @ " + new Date().toLocaleTimeString());
+    }
+
+    function onDeviceReady() {
+        // Register the event listener
+        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;
+
+        document.addEventListener("pause", onPause, false);
+        document.addEventListener("resume", onResume, false);
+
+        window.setInterval(function() {
+            log("Page2: Running");
+        }, 2000);
+    }
+
+    function onPause() {
+        console.log("Page2: onpause");
+        log("Page2: onpause @ " + new Date().toLocaleTimeString());
+    }
+
+    function onResume() {
+        console.log("Page2: onresume");
+        log("Page2: onresume @ " + new Date().toLocaleTimeString());
+    }
+
+    function log(s) {
+        var el = document.getElementById('status');
+        var status = el.innerHTML + s + "<br>";
+        el.innerHTML = status;
+        localStorage.lifecyclestatus = status;
+    }
+    
+    function clearStatus() {
+        console.log("clear()");
+        localStorage.lifecyclestatus = "";
+        document.getElementById('status').innerHTML = "";
+    }
+        
+</script>
+</head>
+<body onload="onLoad()" onunload="onUnLoaded()"  id="stage" class="theme">
+    <h1>Events</h1>
+    <div id="info">
+        <h4>
+            Platform: <span id="platform"> &nbsp;</span>, Version: <span
+                id="version">&nbsp;</span>
+        </h4>
+        <h4>
+            UUID: <span id="uuid"> &nbsp;</span>, Name: <span id="name">&nbsp;</span>
+        </h4>
+        <h4>
+            Width: <span id="width"> &nbsp;</span>, Height: <span id="height">&nbsp;
+            </span>, Color Depth: <span id="colorDepth"></span>
+        </h4>
+    </div>
+    <div id="info">
+       You should see<br>
+       <ul>
+        <li>Page1: onunload</li>
+        <li>Page2: onload</li>
+        <li>Page2: Running</li>
+       </ul>
+       Press "backbutton" to return to Page 1.
+    </div>
+    <div id="info">
+       <h4>Info for event testing:</h4>
+       <div id="status"></div>
+    </div>
+    
+    <a href="index.html" class="btn large" >Load new page</a>
+    <a href="javascript:" class="btn large" onclick="clearStatus();">Clear status</a>
+    
+    <script>
+    document.getElementById('status').innerHTML = localStorage.lifecyclestatus;
+    </script>
+</body>
+</html>
+
+
diff --git a/test/assets/www/main.js b/test/assets/www/main.js
new file mode 100755
index 0000000..aa1b226
--- /dev/null
+++ b/test/assets/www/main.js
@@ -0,0 +1,169 @@
+/*
+       Licensed to the Apache Software Foundation (ASF) under one
+       or more contributor license agreements.  See the NOTICE file
+       distributed with this work for additional information
+       regarding copyright ownership.  The ASF licenses this file
+       to you under the Apache License, Version 2.0 (the
+       "License"); you may not use this file except in compliance
+       with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing,
+       software distributed under the License is distributed on an
+       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+       KIND, either express or implied.  See the License for the
+       specific language governing permissions and limitations
+       under the License.
+*/
+
+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 el = document.getElementById("cordova");
+    if (el) {
+    	el.innerHTML = device.cordova;
+    }
+};
+
+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:image/jpeg;base64," + 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/test/assets/www/master.css b/test/assets/www/master.css
new file mode 100755
index 0000000..c3e3c45
--- /dev/null
+++ b/test/assets/www/master.css
@@ -0,0 +1,136 @@
+/*
+       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;
+  }
+
+  /* Stage Buttons */
+  #stage.theme button.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 button.btn.large{
+    width:308px;
+    padding:1.2em 0;
+  }
+
diff --git a/test/assets/www/menus/index.html b/test/assets/www/menus/index.html
new file mode 100755
index 0000000..38322bc
--- /dev/null
+++ b/test/assets/www/menus/index.html
@@ -0,0 +1,47 @@
+<!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 name="viewport" content="width=320; user-scalable=no" />
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>Cordova Tests</title>
+	  <link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+	  <script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+      <script type="text/javascript" charset="utf-8" src="../main.js"></script>
+  </head>
+  <body onload="init();" id="stage" class="theme">
+    <h1>Menu Test</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;
+                   </span>, Color Depth: <span id="colorDepth"></span></h4>
+     </div>
+     <div id="info">
+     <h4>The menu items should be:</h4>
+     <li>Item1<br>
+     <li>Item2<br>
+     <li>Item3<br>
+     <h4>There is also a context menu.  Touch and hold finger here to see:</h4>
+     <li>Context Item1<br>
+     </div>
+
+  </body>
+</html>
diff --git a/test/assets/www/splashscreen/index.html b/test/assets/www/splashscreen/index.html
new file mode 100755
index 0000000..8d2cd9f
--- /dev/null
+++ b/test/assets/www/splashscreen/index.html
@@ -0,0 +1,40 @@
+<!--
+         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.$
+-->
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta name="viewport" content="width=320; user-scalable=no" />
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>Cordova Tests</title>
+	  <link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+	  <script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+      <script type="text/javascript" charset="utf-8" src="../main.js"></script>
+  </head>
+  <body onload="init();" id="stage" class="theme">
+    <h1>Splash Screen Test</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;
+                   </span>, Color Depth: <span id="colorDepth"></span></h4>
+     </div>
+     <div id="info">
+     You should have seen the splash screen for 2 seconds.</div>
+  </body>
+</html>
diff --git a/test/assets/www/userwebview/index.html b/test/assets/www/userwebview/index.html
new file mode 100755
index 0000000..51eec6a
--- /dev/null
+++ b/test/assets/www/userwebview/index.html
@@ -0,0 +1,67 @@
+<!--
+         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.$
+-->
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta name="viewport" content="width=320; user-scalable=no" />
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>Cordova Tests</title>
+	  <link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+	  <script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+      <script type="text/javascript" charset="utf-8" src="../main.js"></script>
+  </head>
+  <body onload="init();" id="stage" class="theme">
+    <h1>User WebView/Client/Chrome Test</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;
+                   </span>, Color Depth: <span id="colorDepth"></span></h4>
+     </div>
+     <div id="info">
+     <h4>The following should be seen in LogCat:</h4>
+     <li>userwebview: TestViewClient()<br>
+     <li>userwebview: TestChromeClient()<br>
+     <li>userwebview: onGeolocationPermissionsShowPrompt(file://)<br>
+     <li>userwebview: shouldOverrideUrlLoading(test://this_will_call_shouldOverrideUrlLoading)<br>
+     
+     </div>
+     <script>
+     /**
+      * Get current location
+      */
+     console.log("getLocation()");
+
+     // Success callback
+     var success = function(p){
+         console.log("Location = "+p.coords.latitude+","+p.coords.longitude);
+         window.location = "test://this_will_call_shouldOverrideUrlLoading";
+     };
+
+     // Fail callback
+     var fail = function(e){
+         console.log("Error: "+e.code);
+     };
+
+     // Get location
+     navigator.geolocation.getCurrentPosition(success, fail, {enableHighAccuracy: true});
+
+     </script>
+  </body>
+</html>
diff --git a/test/assets/www/whitelist/index.html b/test/assets/www/whitelist/index.html
new file mode 100755
index 0000000..cf09d4c
--- /dev/null
+++ b/test/assets/www/whitelist/index.html
@@ -0,0 +1,47 @@
+<!--
+         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.$
+-->
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta name="viewport" content="width=320; user-scalable=no" />
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>Cordova Tests</title>
+      <link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+      <script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+      <script type="text/javascript" charset="utf-8" src="../main.js"></script>
+  </head>
+  <body onload="init();" id="stage" class="theme">
+    <h1>Whitelist Page 1</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;
+                   </span>, Color Depth: <span id="colorDepth"></span></h4>
+     </div>
+     <div id="info">
+     Loading Page 2 should be successful.<br>
+     Loading Page 3 should be in web browser.<br>
+     Loading Page 2 with target=_blank should be in web browser? <br>
+     (THIS DOESN'T HAPPEN.) https://issues.apache.org/jira/browse/CB-362 
+     </div>
+    <a href="index2.html" class="btn large">Page 2</a>
+    <a href="http://www.google.com" class="btn large">Page 3</a>
+    <a href="index2.html" class="btn large" target="_blank">Page 2 with target=_blank</a>
+  </body>
+</html>
diff --git a/test/assets/www/whitelist/index2.html b/test/assets/www/whitelist/index2.html
new file mode 100755
index 0000000..1ba780b
--- /dev/null
+++ b/test/assets/www/whitelist/index2.html
@@ -0,0 +1,41 @@
+<!--
+         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.$
+-->
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta name="viewport" content="width=320; user-scalable=no" />
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>Cordova Tests</title>
+      <link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+      <script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+      <script type="text/javascript" charset="utf-8" src="../main.js"></script>
+  </head>
+  <body onload="init();" id="stage" class="theme">
+    <h1>Whitelist Page 2</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;
+                   </span>, Color Depth: <span id="colorDepth"></span></h4>
+     </div>
+     <div id="info">
+     Press "backbutton"
+     </div>
+  </body>
+</html>
diff --git a/test/assets/www/xhr/index.html b/test/assets/www/xhr/index.html
new file mode 100755
index 0000000..9eddb1d
--- /dev/null
+++ b/test/assets/www/xhr/index.html
@@ -0,0 +1,66 @@
+<!--
+         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.$
+-->
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta name="viewport" content="width=320; user-scalable=no" />
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>Cordova Tests</title>
+      <link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title">
+      <script type="text/javascript" charset="utf-8" src="../cordova.js"></script>
+      <script type="text/javascript" charset="utf-8" src="../main.js"></script>
+      <script>
+      function XHR(url) {
+          var xmlhttp = new XMLHttpRequest();
+          xmlhttp.onreadystatechange=function(){
+              if(xmlhttp.readyState === 4){
+                  // If success
+                  if (xmlhttp.status === 200) {
+                      alert("XHR success.  Result="+xmlhttp.responseText);
+                  }
+                  // If error
+                  else {
+                      alert("XHR error.  Status="+xmlhttp.status);
+                  }
+              }
+          };
+          console.log("GET "+url);
+          xmlhttp.open("GET", url , true);
+          xmlhttp.send();
+      }
+      </script>
+  </head>
+  <body onload="init();" id="stage" class="theme">
+    <h1>XHR</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"> &nbsp;</span>,   Version: <span id="version">&nbsp;</span></h4>
+        <h4>UUID: <span id="uuid"> &nbsp;</span>,   Name: <span id="name">&nbsp;</span></h4>
+        <h4>Width: <span id="width"> &nbsp;</span>,   Height: <span id="height">&nbsp;
+                   </span>, Color Depth: <span id="colorDepth"></span></h4>
+     </div>
+     <div id="info">
+     Press buttons below to test.  You should see an alert with results displayed.
+     </div>
+    <a href="javascript:" class="btn large" onclick="XHR('file:///android_asset/www/xhr/index.html')">Load file://</a>
+    <a href="javascript:" class="btn large" onclick="XHR('http://www.google.com');">Load Google</a>
+    <!-- 
+    <a href="javascript:" class="btn large" onclick="XHR('content://com.android.contacts/data/1');">Load content://</a>
+    -->
+  </body>
+</html>
diff --git a/test/build.xml b/test/build.xml
new file mode 100755
index 0000000..edede82
--- /dev/null
+++ b/test/build.xml
@@ -0,0 +1,103 @@
+<?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="tests" default="help">
+
+    <!-- The local.properties file is created and updated by the 'android' tool.
+         It contains the path to the SDK. It should *NOT* be checked into
+         Version Control Systems. -->
+    <property file="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' or to inject it through an env var"
+            unless="sdk.dir"
+    />
+
+
+<!-- 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"
+    -->
+    <!-- version-tag: 1 -->
+    <import file="${sdk.dir}/tools/ant/build.xml" />
+
+</project>
diff --git a/test/proguard.cfg b/test/proguard.cfg
new file mode 100644
index 0000000..b1cdf17
--- /dev/null
+++ b/test/proguard.cfg
@@ -0,0 +1,40 @@
+-optimizationpasses 5
+-dontusemixedcaseclassnames
+-dontskipnonpubliclibraryclasses
+-dontpreverify
+-verbose
+-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
+
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Application
+-keep public class * extends android.app.Service
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.preference.Preference
+-keep public class com.android.vending.licensing.ILicensingService
+
+-keepclasseswithmembernames class * {
+    native <methods>;
+}
+
+-keepclasseswithmembers class * {
+    public <init>(android.content.Context, android.util.AttributeSet);
+}
+
+-keepclasseswithmembers class * {
+    public <init>(android.content.Context, android.util.AttributeSet, int);
+}
+
+-keepclassmembers class * extends android.app.Activity {
+   public void *(android.view.View);
+}
+
+-keepclassmembers enum * {
+    public static **[] values();
+    public static ** valueOf(java.lang.String);
+}
+
+-keep class * implements android.os.Parcelable {
+  public static final android.os.Parcelable$Creator *;
+}
diff --git a/test/project.properties b/test/project.properties
new file mode 100644
index 0000000..8d88a8c
--- /dev/null
+++ b/test/project.properties
@@ -0,0 +1,12 @@
+# 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.
+
+# Project target.
+target=android-17
+android.library.reference.1=../framework
diff --git a/test/res/drawable-hdpi/ic_launcher.png b/test/res/drawable-hdpi/ic_launcher.png
new file mode 100755
index 0000000..4d27634
--- /dev/null
+++ b/test/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/test/res/drawable-ldpi/ic_launcher.png b/test/res/drawable-ldpi/ic_launcher.png
new file mode 100755
index 0000000..cd5032a
--- /dev/null
+++ b/test/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/test/res/drawable-mdpi/ic_launcher.png b/test/res/drawable-mdpi/ic_launcher.png
new file mode 100755
index 0000000..e79c606
--- /dev/null
+++ b/test/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/test/res/drawable/icon.png b/test/res/drawable/icon.png
new file mode 100755
index 0000000..697df7f
--- /dev/null
+++ b/test/res/drawable/icon.png
Binary files differ
diff --git a/test/res/drawable/sandy.jpg b/test/res/drawable/sandy.jpg
new file mode 100755
index 0000000..c956d30
--- /dev/null
+++ b/test/res/drawable/sandy.jpg
Binary files differ
diff --git a/test/res/layout/main.xml b/test/res/layout/main.xml
new file mode 100644
index 0000000..9b002ab
--- /dev/null
+++ b/test/res/layout/main.xml
@@ -0,0 +1,31 @@
+<?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:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical" >
+    
+    <org.apache.cordova.CordovaWebView
+        android:id="@+id/cordovaWebView"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent" />
+
+
+</LinearLayout>
diff --git a/test/res/values/strings.xml b/test/res/values/strings.xml
new file mode 100644
index 0000000..9e13e36
--- /dev/null
+++ b/test/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+   <string name="app_name">CordovaTests</string>
+</resources>
diff --git a/test/res/xml/config.xml b/test/res/xml/config.xml
new file mode 100644
index 0000000..60a00cd
--- /dev/null
+++ b/test/res/xml/config.xml
@@ -0,0 +1,52 @@
+<?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="callback-dev@incubator.apache.org">
+        Apache Cordova Team
+    </author>
+
+    <access origin="*.apache.org"/>
+
+    <!-- <content src="http://mysite.com/myapp.html" /> for external pages -->
+    <content src="index.html" />
+
+    <log level="DEBUG"/>
+
+    <!-- Preferences for Android -->
+    <preference name="useBrowserHistory" value="true" />
+    <preference name="exit-on-suspend" value="false" />
+
+    <feature name="Activity">
+        <param name="android-package" value="org.apache.cordova.test.ActivityPlugin" />
+    </feature>
+    <feature name="App">
+      <param name="android-package" value="org.apache.cordova.App"/>
+    </feature>
+         
+</widget>
+
diff --git a/test/src/org/apache/cordova/test/ActivityPlugin.java b/test/src/org/apache/cordova/test/ActivityPlugin.java
new file mode 100755
index 0000000..19a50a2
--- /dev/null
+++ b/test/src/org/apache/cordova/test/ActivityPlugin.java
@@ -0,0 +1,85 @@
+/*
+       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 org.apache.cordova.test;
+
+import org.apache.cordova.CordovaArgs;
+import org.apache.cordova.LOG;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import android.content.Intent;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.PluginResult;
+
+/**
+ * This class provides a service.
+ */
+public class ActivityPlugin extends CordovaPlugin {
+
+    static String TAG = "ActivityPlugin";
+
+    /**
+     * Constructor.
+     */
+    public ActivityPlugin() {
+    }
+
+    /**
+     * Executes the request and returns PluginResult.
+     *
+     * @param action        The action to execute.
+     * @param args          JSONArry of arguments for the plugin.
+     * @param callbackId    The callback id used when calling back into JavaScript.
+     * @return              A PluginResult object with a status and message.
+     */
+    public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) {
+        PluginResult result = new PluginResult(PluginResult.Status.OK, "");
+        try {
+            if (action.equals("start")) {
+                this.startActivity(args.getString(0));
+                callbackContext.sendPluginResult(result);
+                callbackContext.success();
+                return true;
+            }
+        } catch (JSONException e) {
+            result = new PluginResult(PluginResult.Status.JSON_EXCEPTION, "JSON Exception");
+            callbackContext.sendPluginResult(result);
+            return false;
+        }
+        return false;
+    }
+
+    // --------------------------------------------------------------------------
+    // LOCAL METHODS
+    // --------------------------------------------------------------------------
+
+    public void startActivity(String className) {
+        try {
+            Intent intent = new Intent().setClass(this.cordova.getActivity(), Class.forName(className));
+            LOG.d(TAG, "Starting activity %s", className);
+            this.cordova.getActivity().startActivity(intent);
+        } catch (ClassNotFoundException e) {
+            e.printStackTrace();
+            LOG.e(TAG, "Error starting activity %s", className);
+        }
+    }
+
+}
diff --git a/test/src/org/apache/cordova/test/AmazonWebViewOnUiThread.java b/test/src/org/apache/cordova/test/AmazonWebViewOnUiThread.java
new file mode 100644
index 0000000..ed96108
--- /dev/null
+++ b/test/src/org/apache/cordova/test/AmazonWebViewOnUiThread.java
@@ -0,0 +1,173 @@
+/*

+ * 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 org.apache.cordova.test;

+

+import org.apache.cordova.CordovaWebView;

+import org.apache.cordova.LOG;

+import android.os.Looper;

+import android.test.InstrumentationTestCase;

+import com.amazon.android.webkit.AmazonWebBackForwardList;

+import com.amazon.android.webkit.AmazonWebHistoryItem;

+import com.amazon.android.webkit.AmazonWebView;

+

+import junit.framework.Assert;

+

+/**

+ * Many tests need to run WebView code in the UI thread. This class wraps a WebView so that calls are ensured to arrive

+ * on the UI thread. All methods may be run on either the UI thread or test thread.

+ */

+public class AmazonWebViewOnUiThread {

+

+    /**

+     * The test that this class is being used in. Used for runTestOnUiThread.

+     */

+    private InstrumentationTestCase mTest;

+

+    /**

+     * The WebView that calls will be made on.

+     */

+    private AmazonWebView mWebView;

+

+    /**

+     * Initializes the webView with a WebViewClient, WebChromeClient, and PictureListener to prepare for

+     * loadUrlAndWaitForCompletion. A new WebViewOnUiThread should be called during setUp so as to reinitialize between

+     * calls.

+     * 

+     * @param test

+     *            The test in which this is being run.

+     * @param webView

+     *            The webView that the methods should call.

+     * @see loadUrlAndWaitForCompletion

+     */

+    public AmazonWebViewOnUiThread(InstrumentationTestCase test,

+        CordovaWebView webView) {

+        mTest = test;

+        mWebView = webView;

+    }

+

+    public void loadUrl(final String url) {

+        runOnUiThread(new Runnable() {

+            @Override

+            public void run() {

+                mWebView.loadUrl(url);

+            }

+        });

+    }

+

+    public AmazonWebBackForwardList copyBackForwardList() {

+        return getValue(new ValueGetter<AmazonWebBackForwardList>() {

+            @Override

+            public AmazonWebBackForwardList capture() {

+                return mWebView.copyBackForwardList();

+            }

+        });

+    }

+

+    public void printBackForwardList() {

+

+        runOnUiThread(new Runnable() {

+            @Override

+            public void run() {

+                AmazonWebBackForwardList currentList = copyBackForwardList();

+                int currentSize = currentList.getSize();

+                for (int i = 0; i < currentSize; ++i) {

+                    AmazonWebHistoryItem item = currentList.getItemAtIndex(i);

+                    String url = item.getUrl();

+                    LOG.d("cordovaamzn", "The URL at index: " + Integer.toString(i)

+                        + "is " + url);

+                }

+            }

+        });

+    }

+

+    public String getUrl() {

+        return getValue(new ValueGetter<String>() {

+            @Override

+            public String capture() {

+                return mWebView.getUrl();

+            }

+        });

+    }

+

+    public boolean backHistory() {

+

+        return getValue(new ValueGetter<Boolean>() {

+            @Override

+            public Boolean capture() {

+                // Check webview first to see if there is a history

+                // This is needed to support curPage#diffLink, since they are

+                // added to appView's history, but not our history url array

+                // (JQMobile behavior)

+                if (mWebView.canGoBack()) {

+                    printBackForwardList();

+                    mWebView.goBack();

+

+                    return true;

+                }

+                return false;

+            }

+        });

+    }

+

+    /**

+     * Helper for running code on the UI thread where an exception is a test failure. If this is already the UI thread

+     * then it runs the code immediately.

+     * 

+     * @see runTestOnUiThread

+     * @param r

+     *            The code to run in the UI thread

+     */

+    public void runOnUiThread(Runnable r) {

+        try {

+            if (isUiThread()) {

+                r.run();

+            } else {

+                mTest.runTestOnUiThread(r);

+            }

+        } catch (Throwable t) {

+            Assert.fail("Unexpected error while running on UI thread: "

+                + t.getMessage());

+        }

+    }

+

+    private <T> T getValue(ValueGetter<T> getter) {

+        runOnUiThread(getter);

+        return getter.getValue();

+    }

+

+    private abstract class ValueGetter<T> implements Runnable {

+        private T mValue;

+

+        @Override

+        public void run() {

+            mValue = capture();

+        }

+

+        protected abstract T capture();

+

+        public T getValue() {

+            return mValue;

+        }

+    }

+

+    /*

+     * Returns true if the current thread is the UI thread based on the Looper.

+     */

+    private static boolean isUiThread() {

+        return (Looper.myLooper() == Looper.getMainLooper());

+    }

+}

diff --git a/test/src/org/apache/cordova/test/BackButtonMultiPageTest.java b/test/src/org/apache/cordova/test/BackButtonMultiPageTest.java
new file mode 100644
index 0000000..887c2e4
--- /dev/null
+++ b/test/src/org/apache/cordova/test/BackButtonMultiPageTest.java
@@ -0,0 +1,170 @@
+package org.apache.cordova.test;
+
+/*
+ *
+ * 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.
+ *
+ */
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.test.actions.backbuttonmultipage;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.inputmethod.BaseInputConnection;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+public class BackButtonMultiPageTest extends
+		ActivityInstrumentationTestCase2<backbuttonmultipage> {
+
+	private int TIMEOUT = 1000;
+	backbuttonmultipage testActivity;
+	private FrameLayout containerView;
+	private LinearLayout innerContainer;
+	private CordovaWebView testView;
+
+	private AmazonWebViewOnUiThread mUiThread;
+
+	public BackButtonMultiPageTest() {
+		super("org.apache.cordova.test", backbuttonmultipage.class);
+	}
+
+	protected void setUp() throws Exception {
+		super.setUp();
+		testActivity = this.getActivity();
+		// Sleep to make sure main page is properly loaded
+		sleep();
+		containerView = (FrameLayout) testActivity
+				.findViewById(android.R.id.content);
+		innerContainer = (LinearLayout) containerView.getChildAt(0);
+		testView = (CordovaWebView) innerContainer.getChildAt(0);
+		mUiThread = new AmazonWebViewOnUiThread(this, testView);
+	}
+
+	public void testPreconditions() {
+		assertNotNull(innerContainer);
+		assertNotNull(testView);
+	}
+
+	public void testViaHref() {
+		testView.sendJavascript("window.location = 'sample2.html';");
+		sleep();
+		String url = mUiThread.getUrl();
+		assertTrue(url.endsWith("sample2.html"));
+		testView.sendJavascript("window.location = 'sample3.html';");
+		sleep();
+		url = mUiThread.getUrl();
+		assertTrue(url.endsWith("sample3.html"));
+		boolean didGoBack = mUiThread.backHistory();
+		sleep();
+		url = mUiThread.getUrl();
+		assertTrue(url.endsWith("sample2.html"));
+		assertTrue(didGoBack);
+		didGoBack = mUiThread.backHistory();
+		sleep();
+		url = mUiThread.getUrl();		
+		assertTrue(url.endsWith("index.html"));
+		assertTrue(didGoBack);
+	}
+
+	public void testViaLoadUrl() {
+		testView.loadUrl("file:///android_asset/www/backbuttonmultipage/sample2.html");
+		sleep();
+		String url = mUiThread.getUrl();
+		assertTrue(url.endsWith("sample2.html"));
+		testView.loadUrl("file:///android_asset/www/backbuttonmultipage/sample3.html");
+		sleep();
+		url = mUiThread.getUrl();
+		assertTrue(url.endsWith("sample3.html"));
+		boolean didGoBack = mUiThread.backHistory();
+		sleep();
+		url = mUiThread.getUrl();
+		assertTrue(url.endsWith("sample2.html"));
+		assertTrue(didGoBack);
+		didGoBack = mUiThread.backHistory();
+		sleep();
+		url = mUiThread.getUrl();
+		assertTrue(url.endsWith("index.html"));
+		assertTrue(didGoBack);
+	}
+
+	public void testViaBackButtonOnView() {
+		testView.loadUrl("file:///android_asset/www/backbuttonmultipage/sample2.html");
+		sleep();
+		String url = mUiThread.getUrl();
+		assertTrue(url.endsWith("sample2.html"));
+		testView.loadUrl("file:///android_asset/www/backbuttonmultipage/sample3.html");
+		sleep();
+		url = mUiThread.getUrl();
+		assertTrue(url.endsWith("sample3.html"));
+		BaseInputConnection viewConnection = new BaseInputConnection(testView,
+				true);
+		KeyEvent backDown = new KeyEvent(KeyEvent.ACTION_DOWN,
+				KeyEvent.KEYCODE_BACK);
+		KeyEvent backUp = new KeyEvent(KeyEvent.ACTION_UP,
+				KeyEvent.KEYCODE_BACK);
+		viewConnection.sendKeyEvent(backDown);
+		viewConnection.sendKeyEvent(backUp);
+		sleep();
+		url = mUiThread.getUrl();
+		assertTrue(url.endsWith("sample2.html"));
+		viewConnection.sendKeyEvent(backDown);
+		viewConnection.sendKeyEvent(backUp);
+		sleep();
+		url = mUiThread.getUrl();
+		assertTrue(url.endsWith("index.html"));
+	}
+
+	public void testViaBackButtonOnLayout() {
+		testView.loadUrl("file:///android_asset/www/backbuttonmultipage/sample2.html");
+		sleep();
+		String url = mUiThread.getUrl();
+		assertTrue(url.endsWith("sample2.html"));
+		testView.loadUrl("file:///android_asset/www/backbuttonmultipage/sample3.html");
+		sleep();
+		url = mUiThread.getUrl();
+		assertTrue(url.endsWith("sample3.html"));
+		BaseInputConnection viewConnection = new BaseInputConnection(
+				containerView, true);
+		KeyEvent backDown = new KeyEvent(KeyEvent.ACTION_DOWN,
+				KeyEvent.KEYCODE_BACK);
+		KeyEvent backUp = new KeyEvent(KeyEvent.ACTION_UP,
+				KeyEvent.KEYCODE_BACK);
+		viewConnection.sendKeyEvent(backDown);
+		viewConnection.sendKeyEvent(backUp);
+		sleep();
+		url = mUiThread.getUrl();
+		assertTrue(url.endsWith("sample2.html"));
+		viewConnection.sendKeyEvent(backDown);
+		viewConnection.sendKeyEvent(backUp);
+		sleep();
+		url = mUiThread.getUrl();
+		assertTrue(url.endsWith("index.html"));
+	}
+
+	private void sleep() {
+		try {
+			Thread.sleep(TIMEOUT);
+		} catch (InterruptedException e) {
+			fail("Unexpected Timeout");
+		}
+	}
+
+}
diff --git a/test/src/org/apache/cordova/test/CordovaActivityTest.java b/test/src/org/apache/cordova/test/CordovaActivityTest.java
new file mode 100644
index 0000000..ac0a8ca
--- /dev/null
+++ b/test/src/org/apache/cordova/test/CordovaActivityTest.java
@@ -0,0 +1,96 @@
+/*
+       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 org.apache.cordova.test;
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.PluginManager;
+import org.apache.cordova.test.actions.CordovaActivity;
+
+import android.app.Instrumentation;
+import android.test.ActivityInstrumentationTestCase2;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+public class CordovaActivityTest extends ActivityInstrumentationTestCase2<CordovaActivity> {
+
+    private CordovaActivity testActivity;
+    private FrameLayout containerView;
+    private LinearLayout innerContainer;
+    private CordovaWebView testView;
+    private Instrumentation mInstr;
+    private int TIMEOUT = 1000;
+    
+    @SuppressWarnings("deprecation")
+    public CordovaActivityTest()
+    {
+        super("org.apache.cordova.test",CordovaActivity.class);
+    }
+    
+    protected void setUp() throws Exception {
+        super.setUp();
+        mInstr = this.getInstrumentation();
+        testActivity = this.getActivity();
+        containerView = (FrameLayout) testActivity.findViewById(android.R.id.content);
+        innerContainer = (LinearLayout) containerView.getChildAt(0);
+        testView = (CordovaWebView) innerContainer.getChildAt(0);
+        
+    }
+    
+    public void testPreconditions(){
+        assertNotNull(innerContainer);
+        assertNotNull(testView);
+    }
+    
+
+    public void testForCordovaView() {
+        String className = testView.getClass().getSimpleName();
+        assertTrue(className.equals("CordovaWebView"));
+    }
+    
+    public void testForLinearLayout() {
+        String className = innerContainer.getClass().getSimpleName();
+        assertTrue(className.equals("LinearLayoutSoftKeyboardDetect"));
+    }
+    
+
+    public void testPauseAndResume() throws Throwable
+    {
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mInstr.callActivityOnPause(testActivity);
+                sleep();
+                assertTrue(testView.isPaused());
+                mInstr.callActivityOnResume(testActivity);
+                sleep();
+                assertFalse(testView.isPaused());
+            }
+        });
+
+    }
+    
+    private void sleep() {
+        try {
+          Thread.sleep(TIMEOUT);
+        } catch (InterruptedException e) {
+          fail("Unexpected Timeout");
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/src/org/apache/cordova/test/CordovaResourceApiTest.java b/test/src/org/apache/cordova/test/CordovaResourceApiTest.java
new file mode 100644
index 0000000..2966021
--- /dev/null
+++ b/test/src/org/apache/cordova/test/CordovaResourceApiTest.java
@@ -0,0 +1,281 @@
+
+package org.apache.cordova.test;
+
+/*
+ *
+ * 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.
+ *
+ */
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.test.ActivityInstrumentationTestCase2;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CordovaResourceApi;
+import org.apache.cordova.CordovaResourceApi.OpenForReadResult;
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.PluginEntry;
+import org.apache.cordova.test.actions.CordovaWebViewTestActivity;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Scanner;
+
+public class CordovaResourceApiTest extends ActivityInstrumentationTestCase2<CordovaWebViewTestActivity> {
+
+    public CordovaResourceApiTest()
+    {
+        super(CordovaWebViewTestActivity.class);
+    }
+
+    CordovaWebView cordovaWebView;
+    CordovaResourceApi resourceApi;
+
+    private CordovaWebViewTestActivity activity;
+    String execPayload;
+    Integer execStatus;
+
+    protected void setUp() throws Exception {
+        super.setUp();
+        activity = this.getActivity();
+        cordovaWebView = activity.cordovaWebView;
+        resourceApi = cordovaWebView.getResourceApi();
+        resourceApi.setThreadCheckingEnabled(false);
+        cordovaWebView.pluginManager.addService(new PluginEntry("CordovaResourceApiTestPlugin1", new CordovaPlugin() {
+            @Override
+            public Uri remapUri(Uri uri) {
+                if (uri.getQuery() != null && uri.getQuery().contains("pluginRewrite")) {
+                    return cordovaWebView.getResourceApi().remapUri(
+                            Uri.parse("data:text/plain;charset=utf-8,pass"));
+                }
+                return null;
+            }
+            public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+                synchronized (CordovaResourceApiTest.this) {
+                    execPayload = args.getString(0);
+                    execStatus = args.getInt(1);
+                    CordovaResourceApiTest.this.notify();
+                }
+                return true;
+            }
+        }));
+    }
+
+    private Uri createTestImageContentUri() {
+        Bitmap imageBitmap = BitmapFactory.decodeResource(activity.getResources(), R.drawable.icon);
+        String stored = MediaStore.Images.Media.insertImage(activity.getContentResolver(),
+                imageBitmap, "app-icon", "desc");
+        return Uri.parse(stored);
+    }
+
+    private void performApiTest(Uri uri, String expectedMimeType, File expectedLocalFile,
+            boolean expectRead, boolean expectWrite) throws IOException {
+        uri = resourceApi.remapUri(uri);
+        assertEquals(expectedLocalFile, resourceApi.mapUriToFile(uri));
+        
+        try {
+            OpenForReadResult readResult = resourceApi.openForRead(uri);
+            String mimeType2 = resourceApi.getMimeType(uri);
+            assertEquals("openForRead mime-type", expectedMimeType, readResult.mimeType);
+            assertEquals("getMimeType mime-type", expectedMimeType, mimeType2);
+            readResult.inputStream.read();
+            if (!expectRead) {
+                fail("Expected getInputStream to throw.");
+            }
+        } catch (IOException e) {
+            if (expectRead) {
+                throw e;
+            }
+        }
+        try {
+            OutputStream outStream = resourceApi.openOutputStream(uri);
+            outStream.write(123);
+            if (!expectWrite) {
+                fail("Expected getOutputStream to throw.");
+            }
+            outStream.close();
+        } catch (IOException e) {
+            if (expectWrite) {
+                throw e;
+            }
+        }
+    }
+
+    public void testValidContentUri() throws IOException
+    {
+        Uri contentUri = createTestImageContentUri();
+        File localFile = resourceApi.mapUriToFile(contentUri);
+        assertNotNull(localFile);
+        performApiTest(contentUri, "image/jpeg", localFile, true, true);
+    }
+
+    public void testInvalidContentUri() throws IOException
+    {
+        Uri contentUri = Uri.parse("content://media/external/images/media/999999999");
+        performApiTest(contentUri, null, null, false, false);
+    }
+
+    public void testValidAssetUri() throws IOException
+    {
+        Uri assetUri = Uri.parse("file:///android_asset/www/index.html?foo#bar"); // Also check for stripping off ? and # correctly.
+        performApiTest(assetUri, "text/html", null, true, false);
+    }
+
+    public void testInvalidAssetUri() throws IOException
+    {
+        Uri assetUri = Uri.parse("file:///android_asset/www/missing.html");
+        performApiTest(assetUri, "text/html", null, false, false);
+    }
+
+    public void testFileUriToExistingFile() throws IOException
+    {
+        File f = File.createTempFile("te s t", ".txt"); // Also check for dealing with spaces.
+        try {
+            Uri fileUri = Uri.parse(f.toURI().toString() + "?foo#bar"); // Also check for stripping off ? and # correctly.
+            performApiTest(fileUri, "text/plain", f, true, true);
+        } finally {
+            f.delete();
+        }
+    }
+
+    public void testFileUriToMissingFile() throws IOException
+    {
+        File f = new File(Environment.getExternalStorageDirectory() + "/somefilethatdoesntexist");
+        Uri fileUri = Uri.parse(f.toURI().toString());
+        try {
+            performApiTest(fileUri, null, f, false, true);
+        } finally {
+            f.delete();
+        }
+    }
+    
+    public void testFileUriToMissingFileWithMissingParent() throws IOException
+    {
+        File f = new File(Environment.getExternalStorageDirectory() + "/somedirthatismissing" + System.currentTimeMillis() + "/somefilethatdoesntexist");
+        Uri fileUri = Uri.parse(f.toURI().toString());
+        performApiTest(fileUri, null, f, false, true);
+    }
+
+    public void testUnrecognizedUri() throws IOException
+    {
+        Uri uri = Uri.parse("somescheme://foo");
+        performApiTest(uri, null, null, false, false);
+    }
+
+    public void testRelativeUri()
+    {
+        try {
+            resourceApi.openForRead(Uri.parse("/foo"));
+            fail("Should have thrown for relative URI 1.");
+        } catch (Throwable t) {
+        }
+        try {
+            resourceApi.openForRead(Uri.parse("//foo/bar"));
+            fail("Should have thrown for relative URI 2.");
+        } catch (Throwable t) {
+        }
+        try {
+            resourceApi.openForRead(Uri.parse("foo.png"));
+            fail("Should have thrown for relative URI 3.");
+        } catch (Throwable t) {
+        }
+    }
+    
+    public void testPluginOverride() throws IOException
+    {
+        Uri uri = Uri.parse("plugin-uri://foohost/android_asset/www/index.html?pluginRewrite=yes");
+        performApiTest(uri, "text/plain", null, true, false);
+    }
+
+    public void testMainThreadUsage() throws IOException
+    {
+        Uri assetUri = Uri.parse("file:///android_asset/www/index.html");
+        resourceApi.setThreadCheckingEnabled(true);
+        try {
+            resourceApi.openForRead(assetUri);
+            fail("Should have thrown for main thread check.");
+        } catch (Throwable t) {
+        }
+    }
+    
+    
+    public void testDataUriPlain() throws IOException
+    {
+        Uri uri = Uri.parse("data:text/plain;charset=utf-8,pa%20ss");
+        OpenForReadResult readResult = resourceApi.openForRead(uri);
+        assertEquals("text/plain", readResult.mimeType);
+        String data = new Scanner(readResult.inputStream, "UTF-8").useDelimiter("\\A").next();
+        assertEquals("pa ss", data);
+    }
+    
+    public void testDataUriBase64() throws IOException
+    {
+        Uri uri = Uri.parse("data:text/js;charset=utf-8;base64,cGFzcw==");
+        OpenForReadResult readResult = resourceApi.openForRead(uri);
+        assertEquals("text/js", readResult.mimeType);
+        String data = new Scanner(readResult.inputStream, "UTF-8").useDelimiter("\\A").next();
+        assertEquals("pass", data);
+    }
+    
+    public void testWebViewRequestIntercept() throws IOException
+    {
+        cordovaWebView.sendJavascript(
+            "var x = new XMLHttpRequest;\n" +
+            "x.open('GET', 'file://foo?pluginRewrite=1', false);\n" + 
+            "x.send();\n" + 
+            "cordova.require('cordova/exec')(null,null,'CordovaResourceApiTestPlugin1', 'foo', [x.responseText, x.status])");
+        
+        execPayload = null;
+        execStatus = null;
+        try {
+            synchronized (this) {
+                this.wait(2000);
+            }
+        } catch (InterruptedException e) {
+        }
+        assertEquals("pass", execPayload);
+        assertEquals(execStatus.intValue(), 200);
+    }
+    
+    public void testWebViewWhiteListRejection() throws IOException
+    {
+        cordovaWebView.sendJavascript(
+            "var x = new XMLHttpRequest;\n" +
+            "x.open('GET', 'http://foo/bar', false);\n" + 
+            "x.send();\n" + 
+            "cordova.require('cordova/exec')(null,null,'CordovaResourceApiTestPlugin1', 'foo', [x.responseText, x.status])");
+        execPayload = null;
+        execStatus = null;
+        try {
+            synchronized (this) {
+                this.wait(2000);
+            }
+        } catch (InterruptedException e) {
+        }
+        assertEquals("", execPayload);
+        assertEquals(execStatus.intValue(), 404);
+    }    
+}
diff --git a/test/src/org/apache/cordova/test/CordovaTest.java b/test/src/org/apache/cordova/test/CordovaTest.java
new file mode 100644
index 0000000..0e11b68
--- /dev/null
+++ b/test/src/org/apache/cordova/test/CordovaTest.java
@@ -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.
+*/
+package org.apache.cordova.test;
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.PluginManager;
+import org.apache.cordova.test.actions.CordovaWebViewTestActivity;
+
+import android.app.Instrumentation;
+import android.test.ActivityInstrumentationTestCase2;
+import android.view.View;
+
+public class CordovaTest extends
+    ActivityInstrumentationTestCase2<CordovaWebViewTestActivity> {
+
+  private static final long TIMEOUT = 1000;
+  private CordovaWebViewTestActivity testActivity;
+  private View testView;
+  private String rString;
+
+  public CordovaTest() {
+    super("org.apache.cordova.test.activities", CordovaWebViewTestActivity.class);
+  }
+
+  protected void setUp() throws Exception {
+    super.setUp();
+    testActivity = this.getActivity();
+    testView = testActivity.findViewById(R.id.cordovaWebView);
+  }
+
+  public void testPreconditions() {
+    assertNotNull(testView);
+  }
+
+  public void testForCordovaView() {
+    //Sleep for no reason!!!!
+    sleep();
+    String className = testView.getClass().getSimpleName();
+    assertTrue(className.equals("CordovaWebView"));
+  }
+
+  /*
+  public void testForPluginManager() {
+    CordovaWebView v = (CordovaWebView) testView;
+    PluginManager p = v.getPluginManager();
+    assertNotNull(p);
+    String className = p.getClass().getSimpleName();
+    assertTrue(className.equals("PluginManager"));
+  }
+
+  public void testBackButton() {
+    CordovaWebView v = (CordovaWebView) testView;
+    assertFalse(v.checkBackKey());
+  }
+
+  public void testLoadUrl() {
+    CordovaWebView v = (CordovaWebView) testView;
+    v.loadUrlIntoView("file:///android_asset/www/index.html");
+    sleep();
+    String url = v.getUrl();
+    boolean result = url.equals("file:///android_asset/www/index.html");
+    assertTrue(result);
+    int visible = v.getVisibility();
+    assertTrue(visible == View.VISIBLE);
+  }
+
+  public void testBackHistoryFalse() {
+    CordovaWebView v = (CordovaWebView) testView;
+    // Move back in the history
+    boolean test = v.backHistory();
+    assertFalse(test);
+  }
+
+  // Make sure that we can go back
+  public void testBackHistoryTrue() {
+    this.testLoadUrl();
+    CordovaWebView v = (CordovaWebView) testView;
+    v.loadUrlIntoView("file:///android_asset/www/compass/index.html");
+    sleep();
+    String url = v.getUrl();
+    assertTrue(url.equals("file:///android_asset/www/compass/index.html"));
+    // Move back in the history
+    boolean test = v.backHistory();
+    assertTrue(test);
+    sleep();
+    url = v.getUrl();
+    assertTrue(url.equals("file:///android_asset/www/index.html"));
+  }
+  */
+  
+
+  private void sleep() {
+    try {
+      Thread.sleep(TIMEOUT);
+    } catch (InterruptedException e) {
+      fail("Unexpected Timeout");
+    }
+  }
+}
diff --git a/test/src/org/apache/cordova/test/ErrorUrlTest.java b/test/src/org/apache/cordova/test/ErrorUrlTest.java
new file mode 100644
index 0000000..35f6a92
--- /dev/null
+++ b/test/src/org/apache/cordova/test/ErrorUrlTest.java
@@ -0,0 +1,79 @@
+package org.apache.cordova.test;
+
+/*
+ *
+ * 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.
+ *
+ */
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.test.actions.errorurl;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+public class ErrorUrlTest extends ActivityInstrumentationTestCase2<errorurl> {
+
+	private int TIMEOUT = 1000;
+	errorurl testActivity;
+	private FrameLayout containerView;
+	private LinearLayout innerContainer;
+	private CordovaWebView testView;
+
+	private AmazonWebViewOnUiThread mUiThread;
+
+	public ErrorUrlTest() {
+		super("org.apache.cordova.test", errorurl.class);
+	}
+
+	protected void setUp() throws Exception {
+		super.setUp();
+		testActivity = this.getActivity();
+		containerView = (FrameLayout) testActivity
+				.findViewById(android.R.id.content);
+		innerContainer = (LinearLayout) containerView.getChildAt(0);
+		testView = (CordovaWebView) innerContainer.getChildAt(0);
+		mUiThread = new AmazonWebViewOnUiThread(this, testView);
+	}
+
+	public void testPreconditions() {
+		assertNotNull(innerContainer);
+		assertNotNull(testView);
+	}
+
+	// This will fail for AWV because the current url on a wrong page will
+	// return the URL used to load
+	public void testUrl() {
+		mUiThread.loadUrl("file:///android_asset/www/htmlnotfound/index.html");
+		sleep();
+		String good_url = "file:///android_asset/www/error.html";
+		String url = mUiThread.getUrl();
+		assertNotNull(url);
+		assertTrue(url.equals(good_url));
+	}
+
+	private void sleep() {
+		try {
+			Thread.sleep(TIMEOUT);
+		} catch (InterruptedException e) {
+			fail("Unexpected Timeout");
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/test/src/org/apache/cordova/test/FixWebView.java b/test/src/org/apache/cordova/test/FixWebView.java
new file mode 100755
index 0000000..ac911ec
--- /dev/null
+++ b/test/src/org/apache/cordova/test/FixWebView.java
@@ -0,0 +1,44 @@
+/*
+       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 org.apache.cordova.test;
+
+import com.amazon.android.webkit.AmazonWebView;
+
+import android.content.Context;
+
+public class FixWebView extends AmazonWebView {
+
+    public FixWebView(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void pauseTimers() {
+        // Do nothing
+    }
+
+    /**
+     * This method is with different signature in order to stop the timers while move application to background
+     * @param realPause
+     */
+    public void pauseTimers(@SuppressWarnings("unused") boolean realPause) {
+        super.pauseTimers();
+    }
+
+}
diff --git a/test/src/org/apache/cordova/test/GapClientTest.java b/test/src/org/apache/cordova/test/GapClientTest.java
new file mode 100644
index 0000000..a3c2a62
--- /dev/null
+++ b/test/src/org/apache/cordova/test/GapClientTest.java
@@ -0,0 +1,68 @@
+/*
+       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 org.apache.cordova.test;
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.CordovaChromeClient;
+import org.apache.cordova.PluginManager;
+import org.apache.cordova.test.actions.CordovaWebViewTestActivity;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.test.ActivityInstrumentationTestCase2;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+public class GapClientTest extends ActivityInstrumentationTestCase2<CordovaWebViewTestActivity> {
+	
+	private CordovaWebViewTestActivity testActivity;
+	private FrameLayout containerView;
+	private LinearLayout innerContainer;
+	private View testView;
+	private String rString;
+	private CordovaChromeClient appCode;
+
+	public GapClientTest() {
+		super("org.apache.cordova.test.activities",CordovaWebViewTestActivity.class);
+	}
+	
+	protected void setUp() throws Exception{
+		super.setUp();
+		testActivity = this.getActivity();
+		containerView = (FrameLayout) testActivity.findViewById(android.R.id.content);
+		innerContainer = (LinearLayout) containerView.getChildAt(0);
+		testView = innerContainer.getChildAt(0);
+		
+	}
+	
+	public void testPreconditions(){
+	    assertNotNull(innerContainer);
+		assertNotNull(testView);
+	}
+	
+	public void testForCordovaView() {
+	    String className = testView.getClass().getSimpleName();
+	    assertTrue(className.equals("CordovaWebView"));
+	}
+	
+	
+}
diff --git a/test/src/org/apache/cordova/test/HtmlNotFoundTest.java b/test/src/org/apache/cordova/test/HtmlNotFoundTest.java
new file mode 100644
index 0000000..daf6af2
--- /dev/null
+++ b/test/src/org/apache/cordova/test/HtmlNotFoundTest.java
@@ -0,0 +1,76 @@
+package org.apache.cordova.test;
+/*
+ *
+ * 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.
+ *
+*/
+
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.test.actions.htmlnotfound;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+public class HtmlNotFoundTest extends ActivityInstrumentationTestCase2<htmlnotfound> {
+
+  private int TIMEOUT = 1000;
+  private htmlnotfound testActivity;
+  private FrameLayout containerView;
+  private LinearLayout innerContainer;
+  private CordovaWebView testView;
+  private AmazonWebViewOnUiThread mUiThread;
+
+  public HtmlNotFoundTest() {
+    super("org.apache.cordova.test",htmlnotfound.class);
+  }
+  
+  
+  protected void setUp() throws Exception {
+    super.setUp();
+    testActivity = this.getActivity();
+    containerView = (FrameLayout) testActivity.findViewById(android.R.id.content);
+    innerContainer = (LinearLayout) containerView.getChildAt(0);
+    testView = (CordovaWebView) innerContainer.getChildAt(0);
+    mUiThread = new AmazonWebViewOnUiThread(this, testView);
+  }
+
+  public void testPreconditions(){
+    assertNotNull(innerContainer);
+    assertNotNull(testView);
+  }
+
+  public void testUrl()
+  {
+      sleep();
+      String good_url = "file:///android_asset/www/htmlnotfound/error.html";
+      String url = mUiThread.getUrl();
+      assertNotNull(url);
+      assertFalse(url.equals(good_url));
+  }
+
+  private void sleep() {
+      try {
+        Thread.sleep(TIMEOUT);
+      } catch (InterruptedException e) {
+        fail("Unexpected Timeout");
+      }
+    }
+
+}
diff --git a/test/src/org/apache/cordova/test/IFrameTest.java b/test/src/org/apache/cordova/test/IFrameTest.java
new file mode 100644
index 0000000..d8adcfc
--- /dev/null
+++ b/test/src/org/apache/cordova/test/IFrameTest.java
@@ -0,0 +1,94 @@
+package org.apache.cordova.test;
+/*
+ *
+ * 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.
+ *
+*/
+
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.test.util.Purity;
+import org.apache.cordova.test.actions.iframe;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.TouchUtils;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+public class IFrameTest extends ActivityInstrumentationTestCase2 {
+  
+    
+    private Instrumentation mInstr;
+    private Activity testActivity;
+    private FrameLayout containerView;
+    private LinearLayout innerContainer;
+    private CordovaWebView testView;
+    private AmazonWebViewOnUiThread mUiThread;
+    private TouchUtils touch;
+    private Purity touchTool;
+    
+    public IFrameTest() {
+        super("org.apache.cordova.test",iframe.class);
+    }
+
+  
+    protected void setUp() throws Exception {
+      super.setUp();
+      mInstr = this.getInstrumentation();
+      testActivity = this.getActivity();
+      containerView = (FrameLayout) testActivity.findViewById(android.R.id.content);
+      innerContainer = (LinearLayout) containerView.getChildAt(0);
+      testView = (CordovaWebView) innerContainer.getChildAt(0);
+      mUiThread = new AmazonWebViewOnUiThread(this, testView);
+      touch = new TouchUtils();
+      touchTool = new Purity(testActivity, getInstrumentation());
+    }
+  
+  
+    public void testIframeDest()
+    {
+        testView.sendJavascript("loadUrl('http://maps.google.com/maps?output=embed');");
+        sleep(3000);
+        testView.sendJavascript("loadUrl('index2.html')");
+        sleep(1000);
+        String url = mUiThread.getUrl();
+        assertTrue(url.endsWith("index.html"));
+    }
+    
+    public void testIframeHistory()
+    {
+        testView.sendJavascript("loadUrl('http://maps.google.com/maps?output=embed');");
+        sleep(3000);
+        testView.sendJavascript("loadUrl('index2.html')");
+        sleep(1000);
+        String url = mUiThread.getUrl();
+        mUiThread.backHistory();
+        sleep(1000);
+        assertTrue(url.endsWith("index.html"));
+    }
+    
+    private void sleep(int timeout) {
+        try {
+          Thread.sleep(timeout);
+        } catch (InterruptedException e) {
+          fail("Unexpected Timeout");
+        }
+    }
+}
diff --git a/test/src/org/apache/cordova/test/LifecycleTest.java b/test/src/org/apache/cordova/test/LifecycleTest.java
new file mode 100644
index 0000000..396036d
--- /dev/null
+++ b/test/src/org/apache/cordova/test/LifecycleTest.java
@@ -0,0 +1,34 @@
+package org.apache.cordova.test;
+/*
+ *
+ * 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.
+ *
+*/
+
+
+import org.apache.cordova.test.actions.lifecycle;
+
+import android.test.ActivityInstrumentationTestCase2;
+
+public class LifecycleTest extends ActivityInstrumentationTestCase2<lifecycle> {
+  
+  public LifecycleTest()
+  {
+    super("org.apache.cordova.test",lifecycle.class);
+  }
+}
diff --git a/test/src/org/apache/cordova/test/MenuTest.java b/test/src/org/apache/cordova/test/MenuTest.java
new file mode 100644
index 0000000..1486cca
--- /dev/null
+++ b/test/src/org/apache/cordova/test/MenuTest.java
@@ -0,0 +1,32 @@
+/*
+       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 org.apache.cordova.test;
+
+import org.apache.cordova.test.actions.menus;
+
+import android.test.ActivityInstrumentationTestCase2;
+
+public class MenuTest extends ActivityInstrumentationTestCase2<menus> {
+
+    public MenuTest() {
+      super("org.apache.cordova.test", menus.class);
+    }
+    
+}
diff --git a/test/src/org/apache/cordova/test/PluginManagerTest.java b/test/src/org/apache/cordova/test/PluginManagerTest.java
new file mode 100644
index 0000000..c767e7d
--- /dev/null
+++ b/test/src/org/apache/cordova/test/PluginManagerTest.java
@@ -0,0 +1,70 @@
+/*
+       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 org.apache.cordova.test;
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.PluginManager;
+import org.apache.cordova.test.actions.CordovaWebViewTestActivity;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+public class PluginManagerTest extends ActivityInstrumentationTestCase2<CordovaWebViewTestActivity> {
+	
+	private CordovaWebViewTestActivity testActivity;
+	private FrameLayout containerView;
+	private LinearLayout innerContainer;
+	private View testView;
+	private String rString;
+	private PluginManager pMan;
+
+	public PluginManagerTest() {
+		super("org.apache.cordova.test.activities",CordovaWebViewTestActivity.class);
+	}
+	
+	protected void setUp() throws Exception{
+		super.setUp();
+		testActivity = this.getActivity();
+		containerView = (FrameLayout) testActivity.findViewById(android.R.id.content);
+		innerContainer = (LinearLayout) containerView.getChildAt(0);
+		testView = innerContainer.getChildAt(0);
+		
+	}
+	
+	public void testPreconditions(){
+	    assertNotNull(innerContainer);
+	    assertNotNull(testView);
+	}
+	
+	
+	public void testForPluginManager() {
+	  /*
+	    CordovaWebView v = (CordovaWebView) testView;
+	    pMan = v.getPluginManager();
+	    assertNotNull(pMan);
+	    String className = pMan.getClass().getSimpleName();
+	    assertTrue(className.equals("PluginManager"));
+	    */
+	}
+	
+
+}
diff --git a/test/src/org/apache/cordova/test/SplashscreenTest.java b/test/src/org/apache/cordova/test/SplashscreenTest.java
new file mode 100644
index 0000000..edb8758
--- /dev/null
+++ b/test/src/org/apache/cordova/test/SplashscreenTest.java
@@ -0,0 +1,49 @@
+package org.apache.cordova.test;
+/*
+ *
+ * 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.
+ *
+*/
+
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.test.actions.splashscreen;
+
+import android.app.Dialog;
+import android.test.ActivityInstrumentationTestCase2;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+public class SplashscreenTest extends ActivityInstrumentationTestCase2<splashscreen> {
+  
+  private splashscreen testActivity;
+  private Dialog containerView;
+
+  public SplashscreenTest()
+  {
+      super("org.apache.cordova.test",splashscreen.class);
+  }
+  
+  protected void setUp() throws Exception {
+      super.setUp();
+      testActivity = this.getActivity();
+      //containerView = (FrameLayout) testActivity.findViewById(android.R.id.content);
+      //containerView = (Dialog) testActivity.findViewById(id);
+    }
+
+}
diff --git a/test/src/org/apache/cordova/test/UserWebViewTest.java b/test/src/org/apache/cordova/test/UserWebViewTest.java
new file mode 100644
index 0000000..e7a240a
--- /dev/null
+++ b/test/src/org/apache/cordova/test/UserWebViewTest.java
@@ -0,0 +1,76 @@
+package org.apache.cordova.test;
+/*
+ *
+ * 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.
+ *
+*/
+
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.CordovaWebViewClient;
+import org.apache.cordova.CordovaChromeClient;
+import org.apache.cordova.test.actions.userwebview;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+public class UserWebViewTest extends ActivityInstrumentationTestCase2<userwebview> {
+
+  public UserWebViewTest ()
+  {
+    super(userwebview.class);
+  }
+  
+  private int TIMEOUT = 1000;
+  userwebview testActivity;
+  private FrameLayout containerView;
+  private LinearLayout innerContainer;
+  private CordovaWebView testView;
+  
+
+  protected void setUp() throws Exception {
+      super.setUp();
+      testActivity = this.getActivity();
+      containerView = (FrameLayout) testActivity.findViewById(android.R.id.content);
+      innerContainer = (LinearLayout) containerView.getChildAt(0);
+      testView = (CordovaWebView) innerContainer.getChildAt(0);
+  }
+  
+  public void testPreconditions(){
+      assertNotNull(innerContainer);
+      assertNotNull(testView);
+  }
+  
+  public void testCustom()
+  {
+    assertTrue(CordovaWebView.class.isInstance(testView));
+    assertTrue(CordovaWebViewClient.class.isInstance(testActivity.testViewClient));
+    assertTrue(CordovaChromeClient.class.isInstance(testActivity.testChromeClient));
+  }
+  
+
+  private void sleep() {
+      try {
+        Thread.sleep(TIMEOUT);
+      } catch (InterruptedException e) {
+        fail("Unexpected Timeout");
+      }
+    }
+
+}
diff --git a/test/src/org/apache/cordova/test/XhrTest.java b/test/src/org/apache/cordova/test/XhrTest.java
new file mode 100644
index 0000000..7c3ed9f
--- /dev/null
+++ b/test/src/org/apache/cordova/test/XhrTest.java
@@ -0,0 +1,34 @@
+package org.apache.cordova.test;
+/*
+ *
+ * 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.
+ *
+*/
+
+
+import org.apache.cordova.test.actions.xhr;
+
+import android.test.ActivityInstrumentationTestCase2;
+
+public class XhrTest extends ActivityInstrumentationTestCase2<xhr> {
+
+  public XhrTest()
+  {
+    super(xhr.class);
+  }
+}
diff --git a/test/src/org/apache/cordova/test/actions/CordovaActivity.java b/test/src/org/apache/cordova/test/actions/CordovaActivity.java
new file mode 100644
index 0000000..f709737
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/CordovaActivity.java
@@ -0,0 +1,33 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import org.apache.cordova.DroidGap;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class CordovaActivity extends DroidGap {
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.loadUrl("file:///android_asset/www/index.html");
+    }
+}
diff --git a/test/src/org/apache/cordova/test/actions/CordovaDriverAction.java b/test/src/org/apache/cordova/test/actions/CordovaDriverAction.java
new file mode 100644
index 0000000..2021346
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/CordovaDriverAction.java
@@ -0,0 +1,87 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import java.util.concurrent.ExecutorService;
+
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+
+import com.amazon.android.webkit.AmazonWebKitFactories;
+import com.amazon.android.webkit.AmazonWebKitFactory;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+
+public class CordovaDriverAction extends Activity implements CordovaInterface {
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+    public void startActivityForResult(CordovaPlugin command, Intent intent,
+            int requestCode) {
+        // TODO Auto-generated method stub
+        
+    }
+
+    public void setActivityResultCallback(CordovaPlugin plugin) {
+        // TODO Auto-generated method stub
+        
+    }
+
+    public Activity getActivity() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Deprecated
+    public Context getContext() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Deprecated
+    public void cancelLoadUrl() {
+        // TODO Auto-generated method stub
+        
+    }
+
+    public Object onMessage(String id, Object data) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public ExecutorService getThreadPool() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public AmazonWebKitFactory getFactory() {
+        return AmazonWebKitFactories.getDefaultFactory();
+    }
+
+}
diff --git a/test/src/org/apache/cordova/test/actions/CordovaWebViewTestActivity.java b/test/src/org/apache/cordova/test/actions/CordovaWebViewTestActivity.java
new file mode 100644
index 0000000..b96e48f
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/CordovaWebViewTestActivity.java
@@ -0,0 +1,112 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.LOG;
+import org.apache.cordova.test.R;
+import org.apache.cordova.test.R.id;
+import org.apache.cordova.test.R.layout;
+
+import com.amazon.android.webkit.AmazonWebKitFactories;
+import com.amazon.android.webkit.AmazonWebKitFactory;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+public class CordovaWebViewTestActivity extends Activity implements CordovaInterface {
+    public CordovaWebView cordovaWebView;
+
+    private final ExecutorService threadPool = Executors.newCachedThreadPool();
+    
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.main);
+
+        cordovaWebView = (CordovaWebView) findViewById(R.id.cordovaWebView);
+
+        cordovaWebView.loadUrl("file:///android_asset/www/index.html");
+
+    }
+
+    public Context getContext() {
+        return this;
+    }
+
+    public void startActivityForResult(CordovaPlugin command, Intent intent,
+            int requestCode) {
+        // TODO Auto-generated method stub
+        
+    }
+
+    public void setActivityResultCallback(CordovaPlugin plugin) {
+        // TODO Auto-generated method stub
+        
+    }
+
+    //Note: This must always return an activity!
+    public Activity getActivity() {
+        return this;
+    }
+
+    @Deprecated
+    public void cancelLoadUrl() {
+        // TODO Auto-generated method stub
+        
+    }
+
+    public Object onMessage(String id, Object data) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public ExecutorService getThreadPool() {
+        // TODO Auto-generated method stub
+        return threadPool;
+    }
+    
+    @Override
+    /**
+     * The final call you receive before your activity is destroyed.
+     */
+    public void onDestroy() {
+        super.onDestroy();
+        if (cordovaWebView != null) {
+            // Send destroy event to JavaScript
+            cordovaWebView.handleDestroy();
+        }
+    }
+
+    @Override
+    public AmazonWebKitFactory getFactory() {
+        return AmazonWebKitFactories.getDefaultFactory();
+    }
+}
\ No newline at end of file
diff --git a/test/src/org/apache/cordova/test/actions/backbuttonmultipage.java b/test/src/org/apache/cordova/test/actions/backbuttonmultipage.java
new file mode 100755
index 0000000..3b90484
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/backbuttonmultipage.java
@@ -0,0 +1,31 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class backbuttonmultipage extends DroidGap {
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.loadUrl("file:///android_asset/www/backbuttonmultipage/index.html");
+    }
+}
\ No newline at end of file
diff --git a/test/src/org/apache/cordova/test/actions/background.java b/test/src/org/apache/cordova/test/actions/background.java
new file mode 100755
index 0000000..47c6bc7
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/background.java
@@ -0,0 +1,33 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+
+import org.apache.cordova.*;
+
+public class background extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        //super.init(new FixWebView(this), new CordovaWebViewClient(this), new CordovaChromeClient(this));
+        super.setBooleanProperty("keepRunning", false);
+        super.loadUrl("file:///android_asset/www/background/index.html");
+    }
+}
diff --git a/test/src/org/apache/cordova/test/actions/backgroundcolor.java b/test/src/org/apache/cordova/test/actions/backgroundcolor.java
new file mode 100755
index 0000000..d501824
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/backgroundcolor.java
@@ -0,0 +1,40 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class backgroundcolor extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Properties must be set before init() is called, since some are processed during init().
+
+        // backgroundColor can also be set in cordova.xml, but you must use the number equivalent of the color.  For example, Color.RED is
+        //      <preference name="backgroundColor" value="-65536" />
+        super.setIntegerProperty("backgroundColor", Color.GREEN);
+
+        super.init();
+        super.loadUrl("file:///android_asset/www/backgroundcolor/index.html");
+    }
+
+}
diff --git a/test/src/org/apache/cordova/test/actions/basicauth.java b/test/src/org/apache/cordova/test/actions/basicauth.java
new file mode 100755
index 0000000..ad739c9
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/basicauth.java
@@ -0,0 +1,44 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class basicauth extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.init();
+
+        // LogCat: onReceivedHttpAuthRequest(browserspy.dk:80,BrowserSpy.dk - HTTP Password Test)
+        AuthenticationToken token = new AuthenticationToken();
+        token.setUserName("test");
+        token.setPassword("test");
+        super.setAuthenticationToken(token, "browserspy.dk:80", "BrowserSpy.dk - HTTP Password Test");
+
+        // Add web site to whitelist
+        Config.init();
+        Config.addWhiteListEntry("http://browserspy.dk*", true);
+
+        // Load test
+        super.loadUrl("file:///android_asset/www/basicauth/index.html");
+    }
+
+}
diff --git a/test/src/org/apache/cordova/test/actions/errorurl.java b/test/src/org/apache/cordova/test/actions/errorurl.java
new file mode 100755
index 0000000..8015c4f
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/errorurl.java
@@ -0,0 +1,33 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class errorurl extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.init();
+        this.setStringProperty("errorUrl", "file:///android_asset/www/htmlnotfound/error.html");
+        super.loadUrl("file:///android_asset/www/htmlnotfound/index.html");
+    }
+
+}
diff --git a/test/src/org/apache/cordova/test/actions/fullscreen.java b/test/src/org/apache/cordova/test/actions/fullscreen.java
new file mode 100755
index 0000000..60726ad
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/fullscreen.java
@@ -0,0 +1,39 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class fullscreen extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Properties must be set before init() is called, since some are processed during init(). 
+
+        // fullscreen can also be set in cordova.xml.  For example, 
+        //      <preference name="fullscreen" value="true" />
+        super.setBooleanProperty("fullscreen", true);
+
+        super.init();
+        super.loadUrl("file:///android_asset/www/fullscreen/index.html");
+    }
+
+}
diff --git a/test/src/org/apache/cordova/test/actions/htmlnotfound.java b/test/src/org/apache/cordova/test/actions/htmlnotfound.java
new file mode 100755
index 0000000..da849e0
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/htmlnotfound.java
@@ -0,0 +1,31 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class htmlnotfound extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.init();
+        super.loadUrl("file:///android_asset/www/htmlnotfound/index.html");
+    }
+}
diff --git a/test/src/org/apache/cordova/test/actions/iframe.java b/test/src/org/apache/cordova/test/actions/iframe.java
new file mode 100755
index 0000000..3398aa4
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/iframe.java
@@ -0,0 +1,30 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class iframe extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.loadUrl("file:///android_asset/www/iframe/index.html");
+    }
+}
diff --git a/test/src/org/apache/cordova/test/actions/lifecycle.java b/test/src/org/apache/cordova/test/actions/lifecycle.java
new file mode 100755
index 0000000..8419b0e
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/lifecycle.java
@@ -0,0 +1,30 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class lifecycle extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.loadUrl("file:///android_asset/www/lifecycle/index.html");
+    }
+}
diff --git a/test/src/org/apache/cordova/test/actions/loading.java b/test/src/org/apache/cordova/test/actions/loading.java
new file mode 100755
index 0000000..4c52c9f
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/loading.java
@@ -0,0 +1,31 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class loading extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.setStringProperty("loadingDialog", "Testing,Loading...");
+        super.loadUrl("http://www.google.com");
+    }
+}
diff --git a/test/src/org/apache/cordova/test/actions/menus.java b/test/src/org/apache/cordova/test/actions/menus.java
new file mode 100755
index 0000000..c4cb674
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/menus.java
@@ -0,0 +1,80 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+
+import org.apache.cordova.*;
+import org.apache.cordova.LOG;
+
+public class menus extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.init();
+        super.registerForContextMenu(super.appView);
+        super.loadUrl("file:///android_asset/www/menus/index.html");
+    }
+
+    // Demonstrate how to add your own menus to app
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        int base = Menu.FIRST;
+        // Group, item id, order, title
+        menu.add(base, base, base, "Item1");
+        menu.add(base, base + 1, base + 1, "Item2");
+        menu.add(base, base + 2, base + 2, "Item3");
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        LOG.d("menus", "Item " + item.getItemId() + " pressed.");
+        this.appView.loadUrl("javascript:alert('Menu " + item.getItemId() + " pressed.')");
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        LOG.d("menus", "onPrepareOptionsMenu()");
+        // this.appView.loadUrl("javascript:alert('onPrepareOptionsMenu()')");
+        return true;
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info) {
+        LOG.d("menus", "onCreateContextMenu()");
+        menu.setHeaderTitle("Test Context Menu");
+        menu.add(200, 200, 200, "Context Item1");
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        this.appView.loadUrl("javascript:alert('Context Menu " + item.getItemId() + " pressed.')");
+        return true;
+    }
+
+}
diff --git a/test/src/org/apache/cordova/test/actions/splashscreen.java b/test/src/org/apache/cordova/test/actions/splashscreen.java
new file mode 100755
index 0000000..3d924e0
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/splashscreen.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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+import org.apache.cordova.test.R;
+import org.apache.cordova.test.R.drawable;
+
+public class splashscreen extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.init();
+
+        // Show splashscreen
+        this.setIntegerProperty("splashscreen", R.drawable.sandy);
+
+        super.loadUrl("file:///android_asset/www/splashscreen/index.html", 2000);
+    }
+}
diff --git a/test/src/org/apache/cordova/test/actions/tests.java b/test/src/org/apache/cordova/test/actions/tests.java
new file mode 100755
index 0000000..bc326e3
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/tests.java
@@ -0,0 +1,32 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class tests extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.init();
+        //super.pluginManager.addService("Activity", "org.apache.cordova.test.ActivityPlugin");
+        super.loadUrl("file:///android_asset/www/index.html");
+    }
+}
diff --git a/test/src/org/apache/cordova/test/actions/timeout.java b/test/src/org/apache/cordova/test/actions/timeout.java
new file mode 100755
index 0000000..22c0881
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/timeout.java
@@ -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.
+*/
+package org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class timeout extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.init();
+
+        // Short timeout to cause error
+        this.setIntegerProperty("loadUrlTimeoutValue", 10);
+        super.loadUrl("http://www.google.com");
+    }
+}
diff --git a/test/src/org/apache/cordova/test/actions/userwebview.java b/test/src/org/apache/cordova/test/actions/userwebview.java
new file mode 100755
index 0000000..2beb793
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/userwebview.java
@@ -0,0 +1,76 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import com.amazon.android.webkit.AmazonGeolocationPermissions.Callback;
+import org.apache.cordova.*;
+
+import com.amazon.android.webkit.AmazonWebView;
+
+public class userwebview extends DroidGap {
+    
+    public TestViewClient testViewClient;
+    public TestChromeClient testChromeClient;
+    
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        testViewClient = new TestViewClient(this);
+        testChromeClient = new TestChromeClient(this);
+        super.init(new CordovaWebView(this), new TestViewClient(this), new TestChromeClient(this));
+        super.loadUrl("file:///android_asset/www/userwebview/index.html");
+    }
+
+    public class TestChromeClient extends CordovaChromeClient {
+        public TestChromeClient(DroidGap arg0) {
+            super(arg0);
+            LOG.d("userwebview", "TestChromeClient()");
+        }
+
+        public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) {
+            LOG.d("userwebview", "onGeolocationPermissionsShowPrompt(" + origin + ")");
+            super.onGeolocationPermissionsShowPrompt(origin, callback);
+            callback.invoke(origin, true, false);
+        }
+    }
+
+    /**
+     * This class can be used to override the GapViewClient and receive notification of webview events.
+     */
+    public class TestViewClient extends CordovaWebViewClient {
+        public TestViewClient(DroidGap arg0) {
+            super(arg0);
+            LOG.d("userwebview", "TestViewClient()");
+        }
+        
+        @Override
+        public boolean shouldOverrideUrlLoading(AmazonWebView view, String url) {
+            LOG.d("userwebview", "shouldOverrideUrlLoading(" + url + ")");
+            return super.shouldOverrideUrlLoading(view, url);
+        }
+        
+        @Override
+        public void onReceivedError(AmazonWebView view, int errorCode, String description, String failingUrl) {
+            LOG.d("userwebview", "onReceivedError: Error code=" + errorCode + " Description=" + description + " URL=" + failingUrl);
+            super.onReceivedError(view, errorCode, description, failingUrl);
+        }
+    }
+
+}
diff --git a/test/src/org/apache/cordova/test/actions/whitelist.java b/test/src/org/apache/cordova/test/actions/whitelist.java
new file mode 100755
index 0000000..85a4dcb
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/whitelist.java
@@ -0,0 +1,51 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+
+import org.apache.cordova.*;
+
+import com.amazon.android.webkit.AmazonWebView;
+
+public class whitelist extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.init(new CordovaWebView(this), new TestViewClient(this), new CordovaChromeClient(this));
+        super.loadUrl("file:///android_asset/www/whitelist/index.html");
+    }
+
+    /**
+     * This class can be used to override the GapViewClient and receive notification of webview events.
+     */
+    public class TestViewClient extends CordovaWebViewClient {
+
+        public TestViewClient(DroidGap arg0) {
+            super(arg0);
+        }
+        
+        @Override
+        public boolean shouldOverrideUrlLoading(AmazonWebView view, String url) {
+            LOG.d("whitelist", "shouldOverrideUrlLoading(" + url + ")");
+            LOG.d("whitelist", "originalUrl=" + view.getOriginalUrl());
+            return super.shouldOverrideUrlLoading(view, url);
+        }
+    }
+}
diff --git a/test/src/org/apache/cordova/test/actions/xhr.java b/test/src/org/apache/cordova/test/actions/xhr.java
new file mode 100755
index 0000000..3695c11
--- /dev/null
+++ b/test/src/org/apache/cordova/test/actions/xhr.java
@@ -0,0 +1,30 @@
+/*
+       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 org.apache.cordova.test.actions;
+
+import android.os.Bundle;
+import org.apache.cordova.*;
+
+public class xhr extends DroidGap {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        super.loadUrl("file:///android_asset/www/xhr/index.html");
+    }
+}
diff --git a/test/src/org/apache/cordova/test/util/PollingCheck.java b/test/src/org/apache/cordova/test/util/PollingCheck.java
new file mode 100644
index 0000000..9d4d559
--- /dev/null
+++ b/test/src/org/apache/cordova/test/util/PollingCheck.java
@@ -0,0 +1,72 @@
+/*

+ * 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 org.apache.cordova.test.util;

+

+import java.util.concurrent.Callable;

+

+import junit.framework.Assert;

+

+public abstract class PollingCheck {

+	private static final long TIME_SLICE = 50;

+	private long mTimeout = 3000;

+

+	public PollingCheck() {

+	}

+

+	public PollingCheck(long timeout) {

+		mTimeout = timeout;

+	}

+

+	protected abstract boolean check();

+

+	public void run() {

+		if (check()) {

+			return;

+		}

+

+		long timeout = mTimeout;

+		while (timeout > 0) {

+			try {

+				Thread.sleep(TIME_SLICE);

+			} catch (InterruptedException e) {

+				Assert.fail("unexpected InterruptedException");

+			}

+

+			if (check()) {

+				return;

+			}

+

+			timeout -= TIME_SLICE;

+		}

+

+		Assert.fail("unexpected timeout");

+	}

+

+	public static void check(CharSequence message, long timeout,

+			Callable<Boolean> condition) throws Exception {

+		while (timeout > 0) {

+			if (condition.call()) {

+				return;

+			}

+

+			Thread.sleep(TIME_SLICE);

+			timeout -= TIME_SLICE;

+		}

+

+		Assert.fail(message.toString());

+	}

+}

diff --git a/test/src/org/apache/cordova/test/util/Purity.java b/test/src/org/apache/cordova/test/util/Purity.java
new file mode 100644
index 0000000..ec6ac79
--- /dev/null
+++ b/test/src/org/apache/cordova/test/util/Purity.java
@@ -0,0 +1,171 @@
+/*
+       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.
+*/
+
+/*
+ * Purity is a small set of Android utility methods that allows us to simulate touch events on 
+ * Android applications.  This is important for simulating some of the most annoying tests.
+ */
+
+package org.apache.cordova.test.util;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Picture;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+
+import com.amazon.android.webkit.AmazonWebView;
+
+public class Purity {
+
+    Instrumentation inst;
+    int width, height;
+    float density;
+    Bitmap state;
+    boolean fingerDown = false;
+   
+    public Purity(Context ctx, Instrumentation i)
+    {
+        inst = i;
+        DisplayMetrics display = ctx.getResources().getDisplayMetrics();
+        density = display.density;
+        width = display.widthPixels;
+        height = display.heightPixels;
+        
+    }
+    
+    /*
+     * WebKit doesn't give you real pixels anymore, this is done for subpixel fonts to appear on
+     * iOS and Android.  However, Android automation requires real pixels
+     */
+    private int getRealCoord(int coord)
+    {
+        return (int) (coord * density);
+    }
+
+    public int getViewportWidth()
+    {
+        return (int) (width/density);
+    }
+    
+    public int getViewportHeight()
+    {
+        return (int) (height/density);
+    }
+    
+    public void touch(int x, int y)
+    {
+        int realX = getRealCoord(x);
+        int realY = getRealCoord(y);
+        long downTime = SystemClock.uptimeMillis();
+        // event time MUST be retrieved only by this way!
+        long eventTime = SystemClock.uptimeMillis();
+        if(!fingerDown)
+        {
+            MotionEvent downEvent = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, realX, realY, 0);
+            inst.sendPointerSync(downEvent);
+        }
+        MotionEvent upEvent = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, realX, realY, 0);
+        inst.sendPointerSync(upEvent);
+    }
+    
+    public void touchStart(int x, int y)
+    {
+        int realX = getRealCoord(x);
+        int realY = getRealCoord(y);
+        long downTime = SystemClock.uptimeMillis();
+        // event time MUST be retrieved only by this way!
+        long eventTime = SystemClock.uptimeMillis();
+        MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, realX, realY, 0);
+        inst.sendPointerSync(event);
+        fingerDown = true;
+    }
+    
+    //Move from the touch start
+    public void touchMove(int x, int y)
+    {
+        if(!fingerDown)
+            touchStart(x,y);
+        else
+        {
+            int realX = getRealCoord(x);
+            int realY = getRealCoord(y);
+            long downTime = SystemClock.uptimeMillis();
+            // event time MUST be retrieved only by this way!
+            long eventTime = SystemClock.uptimeMillis();
+            MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, realX, realY, 0);
+            inst.sendPointerSync(event);
+        }
+    }
+    
+    public void touchEnd(int x, int y)
+    {
+        if(!fingerDown)
+        {
+            touch(x, y);
+        }
+        else
+        {
+            int realX = getRealCoord(x);
+            int realY = getRealCoord(y);
+            long downTime = SystemClock.uptimeMillis();
+            // event time MUST be retrieved only by this way!
+            long eventTime = SystemClock.uptimeMillis();
+            MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, realX, realY, 0);
+            inst.sendPointerSync(event);
+            fingerDown = false;
+        }
+    }
+    
+    public void setBitmap(AmazonWebView view)
+    {
+        Picture p = view.capturePicture();
+        state = Bitmap.createBitmap(p.getWidth(), p.getHeight(), Bitmap.Config.ARGB_8888);
+    }
+    
+    public boolean checkRenderView(AmazonWebView view)
+    {
+        if(state == null)
+        {
+            setBitmap(view);
+            return false;
+        }
+        else
+        {
+            Picture p = view.capturePicture();
+            Bitmap newState = Bitmap.createBitmap(p.getWidth(), p.getHeight(), Bitmap.Config.ARGB_8888);
+            boolean result = newState.equals(state);
+            newState.recycle();
+            return result;
+        }
+    }
+    
+    public void clearBitmap()
+    {
+        if(state != null)
+            state.recycle();
+    }
+    
+    protected void finalize()
+    {
+            clearBitmap();
+    }
+}