Browse Source

cordoba project update

x 5 years ago
parent
commit
07da2a093d
100 changed files with 11120 additions and 647 deletions
  1. 28 1
      cordova/config.xml
  2. 24 0
      cordova/package-lock.json
  3. 10 1
      cordova/package.json
  4. 2 1
      cordova/platforms/android/CordovaLib/src/org/apache/cordova/CoreAndroid.java
  5. 257 2
      cordova/platforms/android/android.json
  6. 2 0
      cordova/platforms/android/app/build.gradle
  7. 6 2
      cordova/platforms/android/app/src/main/AndroidManifest.xml
  8. 210 1
      cordova/platforms/android/app/src/main/assets/www/cordova_plugins.js
  9. 0 0
      cordova/platforms/android/app/src/main/assets/www/h5/css/chunk-493fc81d.c05350da.css
  10. 1 1
      cordova/platforms/android/app/src/main/assets/www/index.html
  11. 199 0
      cordova/platforms/android/app/src/main/java/io/github/pwlin/cordova/plugins/fileopener2/FileOpener2.java
  12. 29 0
      cordova/platforms/android/app/src/main/java/io/github/pwlin/cordova/plugins/fileopener2/FileProvider.java
  13. 294 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/AssetFilesystem.java
  14. 223 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/ContentFilesystem.java
  15. 134 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/DirectoryManager.java
  16. 29 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/EncodingException.java
  17. 29 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/FileExistsException.java
  18. 1225 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/FileUtils.java
  19. 331 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/Filesystem.java
  20. 30 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/InvalidModificationException.java
  21. 513 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/LocalFilesystem.java
  22. 64 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/LocalFilesystemURL.java
  23. 29 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/NoModificationAllowedException.java
  24. 94 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/PendingRequests.java
  25. 30 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/file/TypeMismatchException.java
  26. 63 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileProgressResult.java
  27. 973 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileTransfer.java
  28. 73 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileUploadResult.java
  29. 413 0
      cordova/platforms/android/app/src/main/java/org/apache/cordova/splashscreen/SplashScreen.java
  30. BIN
      cordova/platforms/android/app/src/main/res/drawable-land-hdpi/screen.png
  31. BIN
      cordova/platforms/android/app/src/main/res/drawable-land-ldpi/screen.png
  32. BIN
      cordova/platforms/android/app/src/main/res/drawable-land-mdpi/screen.png
  33. BIN
      cordova/platforms/android/app/src/main/res/drawable-land-xhdpi/screen.png
  34. BIN
      cordova/platforms/android/app/src/main/res/drawable-land-xxhdpi/screen.png
  35. BIN
      cordova/platforms/android/app/src/main/res/drawable-land-xxxhdpi/screen.png
  36. BIN
      cordova/platforms/android/app/src/main/res/drawable-port-hdpi/screen.png
  37. BIN
      cordova/platforms/android/app/src/main/res/drawable-port-ldpi/screen.png
  38. BIN
      cordova/platforms/android/app/src/main/res/drawable-port-mdpi/screen.png
  39. BIN
      cordova/platforms/android/app/src/main/res/drawable-port-xhdpi/screen.png
  40. BIN
      cordova/platforms/android/app/src/main/res/drawable-port-xxhdpi/screen.png
  41. BIN
      cordova/platforms/android/app/src/main/res/drawable-port-xxxhdpi/screen.png
  42. 27 1
      cordova/platforms/android/app/src/main/res/xml/config.xml
  43. 14 0
      cordova/platforms/android/app/src/main/res/xml/opener_paths.xml
  44. 210 1
      cordova/platforms/android/platform_www/cordova_plugins.js
  45. 2 1
      cordova/platforms/android/project.properties
  46. 2 2
      cordova/platforms/ios/CordovaLib/CordovaLib.xcodeproj/xcuserdata/x.xcuserdatad/xcschemes/xcschememanagement.plist
  47. 226 1
      cordova/platforms/ios/ios.json
  48. 202 1
      cordova/platforms/ios/platform_www/cordova_plugins.js
  49. 3 3
      cordova/platforms/ios/美天旺.xcarchive/Info.plist
  50. BIN
      cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/Assets.car
  51. BIN
      cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/CDVLaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib/objects-13.0+.nib
  52. BIN
      cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/CDVLaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib/runtime.nib
  53. BIN
      cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/CDVLaunchScreen.storyboardc/UIViewController-01J-lp-oVM.nib/objects-13.0+.nib
  54. BIN
      cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/CDVLaunchScreen.storyboardc/UIViewController-01J-lp-oVM.nib/runtime.nib
  55. BIN
      cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/Info.plist
  56. BIN
      cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/JPushConfig.plist
  57. BIN
      cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/MainViewController.nib
  58. 322 606
      cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/_CodeSignature/CodeResources
  59. 18 1
      cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/config.xml
  60. BIN
      cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/美天旺
  61. 38 4
      cordova/platforms/ios/美天旺.xcodeproj/project.pbxproj
  62. BIN
      cordova/platforms/ios/美天旺.xcworkspace/xcuserdata/x.xcuserdatad/UserInterfaceState.xcuserstate
  63. 3 3
      cordova/platforms/ios/美天旺/CDVLaunchScreen.storyboard
  64. 2 2
      cordova/platforms/ios/美天旺/Entitlements-Debug.plist
  65. 2 2
      cordova/platforms/ios/美天旺/Entitlements-Release.plist
  66. 6 3
      cordova/platforms/ios/美天旺/Images.xcassets/LaunchStoryboard.imageset/Contents.json
  67. BIN
      cordova/platforms/ios/美天旺/Images.xcassets/LaunchStoryboard.imageset/Default@2x~universal~anyany.png
  68. BIN
      cordova/platforms/ios/美天旺/Images.xcassets/LaunchStoryboard.imageset/Default@2x~universal~comany.png
  69. BIN
      cordova/platforms/ios/美天旺/Images.xcassets/LaunchStoryboard.imageset/Default@3x~universal~comany.png
  70. 35 0
      cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file-opener2/FileOpener2.h
  71. 151 0
      cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file-opener2/FileOpener2.m
  72. 89 0
      cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file-transfer/CDVFileTransfer.h
  73. 861 0
      cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file-transfer/CDVFileTransfer.m
  74. 30 0
      cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file/CDVAssetLibraryFilesystem.h
  75. 253 0
      cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file/CDVAssetLibraryFilesystem.m
  76. 157 0
      cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file/CDVFile.h
  77. 1119 0
      cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file/CDVFile.m
  78. 32 0
      cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file/CDVLocalFilesystem.h
  79. 750 0
      cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file/CDVLocalFilesystem.m
  80. 20 2
      cordova/platforms/ios/美天旺/config.xml
  81. 5 5
      cordova/platforms/ios/美天旺/美天旺-Info.plist
  82. 13 0
      cordova/plugins/android.json
  83. 44 0
      cordova/plugins/cordova-plugin-file-opener2/.github/ISSUE_TEMPLATE/bug_report.md
  84. 20 0
      cordova/plugins/cordova-plugin-file-opener2/LICENSE
  85. 213 0
      cordova/plugins/cordova-plugin-file-opener2/README.md
  86. 62 0
      cordova/plugins/cordova-plugin-file-opener2/package.json
  87. 97 0
      cordova/plugins/cordova-plugin-file-opener2/plugin.xml
  88. 199 0
      cordova/plugins/cordova-plugin-file-opener2/src/android/io/github/pwlin/cordova/plugins/fileopener2/FileOpener2.java
  89. 29 0
      cordova/plugins/cordova-plugin-file-opener2/src/android/io/github/pwlin/cordova/plugins/fileopener2/FileProvider.java
  90. 14 0
      cordova/plugins/cordova-plugin-file-opener2/src/android/res/xml/opener_paths.xml
  91. 125 0
      cordova/plugins/cordova-plugin-file-opener2/src/browser/FileOpener2.js
  92. 0 0
      cordova/plugins/cordova-plugin-file-opener2/src/browser/FileSaver.min.js
  93. 35 0
      cordova/plugins/cordova-plugin-file-opener2/src/electron/FileOpener2.js
  94. 35 0
      cordova/plugins/cordova-plugin-file-opener2/src/ios/FileOpener2.h
  95. 151 0
      cordova/plugins/cordova-plugin-file-opener2/src/ios/FileOpener2.m
  96. 97 0
      cordova/plugins/cordova-plugin-file-opener2/src/windows/fileOpener2Proxy.js
  97. 28 0
      cordova/plugins/cordova-plugin-file-transfer/.appveyor.yml
  98. 22 0
      cordova/plugins/cordova-plugin-file-transfer/.github/PULL_REQUEST_TEMPLATE.md
  99. 23 0
      cordova/plugins/cordova-plugin-file-transfer/.jscsrc
  100. 19 0
      cordova/plugins/cordova-plugin-file-transfer/.jshintrc

+ 28 - 1
cordova/config.xml

@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='utf-8'?>
-<widget id="com.shotshock.twong" version="1.2.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+<widget id="com.shotshock.twong" version="1.2.8" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
     <name>美天旺</name>
     <description>
         美天旺是完璧时空旗下新型垂直电商App,专注于新经济下的购物服务!
@@ -25,6 +25,15 @@
         <icon src="res/android/mipmap-xxxhdpi/ic_launcher.png" density="xxxhdpi" />
         <preference name="CodePushDeploymentKey" value="YOUR-ANDROID-DEPLOYMENT-KEY" />
         <preference name="CodePushPublicKey" value="YOUR-PUBLIC-KEY" />
+        <preference name="AndroidPersistentFileLocation" value="Compatibility" />
+
+        <splash src="res/screen/android/splash-port-mdpi.png" density="port-mdpi"/>
+        <splash src="res/screen/android/splash-port-hdpi.png" density="port-hdpi"/>
+        <splash src="res/screen/android/splash-port-xhdpi.png" density="port-xhdpi"/>
+        <splash src="res/screen/android/splash-port-xxhdpi.png" density="port-xxhdpi"/>
+        <splash src="res/screen/android/splash-port-xxxhdpi.png" density="port-xxxhdpi"/>
+        <!--设置启动画面时间-->
+        <preference name="SplashScreenDelay" value="3000" />
     </platform>
 	<preference name="Fullscreen" value="true" />
 	<preference name="WebViewBounce" value="false" />
@@ -62,5 +71,23 @@
         <icon src="res/ios/ios-marketing-1024x1024" width="024" height="1024" />
         <preference name="CodePushDeploymentKey" value="YOUR-ANDROID-DEPLOYMENT-KEY" />
         <preference name="CodePushPublicKey" value="YOUR-PUBLIC-KEY" />
+
+        <!-- Storyboard (supports all devices): -->
+        <!-- Note: images are determined by scale, idiom, and size traits. The following -->
+        <!-- are suggested based on current device form factors -->
+        <splash src="res/screen/ios/Default@2x~universal~anyany.png" />
+        <splash src="res/screen/ios/Default@2x~universal~comany.png" />
+        <splash src="res/screen/ios/Default@3x~universal~comany.png" />
+        <!--设置启动画面时间-->
+        <preference name="SplashScreenDelay" value="1500" />
+        <preference name="AllowInlineMediaPlayback" value="true" />
     </platform>
+
+    <!--隐藏淡入淡出-->
+    <preference name="FadeSplashScreen" value="false"/>
+    <!--隐藏启动画面那个laoding图片-->
+    <preference name="ShowSplashScreenSpinner" value="false"/>
+    <!--拉伸-->
+    <preference name="SplashMaintainAspectRatio" value="true" />
+    <preference name="SplashScreenSpinnerColor" value="white" />
 </widget>

+ 24 - 0
cordova/package-lock.json

@@ -228,6 +228,24 @@
       "integrity": "sha1-wrQbfv0EVd0Jf4k1bYW/3V2t6w8=",
       "dev": true
     },
+    "cordova-plugin-file": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npm.taobao.org/cordova-plugin-file/download/cordova-plugin-file-6.0.2.tgz",
+      "integrity": "sha1-85EUefg1fpqstVdmdPjZWzGh+yA=",
+      "dev": true
+    },
+    "cordova-plugin-file-opener2": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npm.taobao.org/cordova-plugin-file-opener2/download/cordova-plugin-file-opener2-3.0.5.tgz",
+      "integrity": "sha1-O/SBpyS87Jz6IlenGRUwnfXqW00=",
+      "dev": true
+    },
+    "cordova-plugin-file-transfer": {
+      "version": "1.7.1",
+      "resolved": "https://registry.npm.taobao.org/cordova-plugin-file-transfer/download/cordova-plugin-file-transfer-1.7.1.tgz",
+      "integrity": "sha1-p12L4uvDu5sjxbG70ZkhTsJnWGs=",
+      "dev": true
+    },
     "cordova-plugin-fullscreen": {
       "version": "1.3.0",
       "resolved": "https://registry.npm.taobao.org/cordova-plugin-fullscreen/download/cordova-plugin-fullscreen-1.3.0.tgz",
@@ -246,6 +264,12 @@
       "integrity": "sha1-2VcRt6HiAEduQiTaw00ork594B8=",
       "dev": true
     },
+    "cordova-plugin-splashscreen": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npm.taobao.org/cordova-plugin-splashscreen/download/cordova-plugin-splashscreen-6.0.0.tgz",
+      "integrity": "sha1-Z0cY+HiUzXVhXaK1WQXrTMcZzwE=",
+      "dev": true
+    },
     "cordova-plugin-statusbar": {
       "version": "2.4.3",
       "resolved": "https://registry.npm.taobao.org/cordova-plugin-statusbar/download/cordova-plugin-statusbar-2.4.3.tgz",

+ 10 - 1
cordova/package.json

@@ -20,9 +20,13 @@
     "cordova-plugin-console": "^1.1.0",
     "cordova-plugin-customurlscheme": "^5.0.2",
     "cordova-plugin-device": "^2.0.3",
+    "cordova-plugin-file": "^6.0.2",
+    "cordova-plugin-file-opener2": "^3.0.5",
+    "cordova-plugin-file-transfer": "^1.7.1",
     "cordova-plugin-fullscreen": "^1.3.0",
     "cordova-plugin-inappbrowser": "^4.0.0",
     "cordova-plugin-jcore": "^1.3.3",
+    "cordova-plugin-splashscreen": "^6.0.0",
     "cordova-plugin-statusbar": "^2.4.3",
     "cordova-plugin-wechat": "git+https://github.com/xu-li/cordova-plugin-wechat.git#develop",
     "cordova-plugin-whitelist": "^1.3.4",
@@ -53,7 +57,12 @@
       "cordova-plugin-alipay-v2": {
         "APP_ID": "2021001196660263"
       },
-      "ionic-plugin-keyboard": {}
+      "ionic-plugin-keyboard": {},
+      "cordova-plugin-file-transfer": {},
+      "cordova-plugin-file-opener2": {
+        "ANDROID_SUPPORT_V4_VERSION": "27.+"
+      },
+      "cordova-plugin-splashscreen": {}
     },
     "platforms": [
       "android",

+ 2 - 1
cordova/platforms/android/CordovaLib/src/org/apache/cordova/CoreAndroid.java

@@ -276,7 +276,8 @@ public class CoreAndroid extends CordovaPlugin {
      * Exit the Android application.
      */
     public void exitApp() {
-        this.webView.getPluginManager().postMessage("exit", null);
+        System.exit(0);
+        // this.webView.getPluginManager().postMessage("exit", null);
     }
 
 

+ 257 - 2
cordova/platforms/android/android.json

@@ -55,6 +55,26 @@
             {
               "xml": "<feature name=\"Keyboard\"><param name=\"android-package\" value=\"io.ionic.keyboard.IonicKeyboard\" /><param name=\"onload\" value=\"true\" /></feature>",
               "count": 1
+            },
+            {
+              "xml": "<feature name=\"File\"><param name=\"android-package\" value=\"org.apache.cordova.file.FileUtils\" /><param name=\"onload\" value=\"true\" /></feature>",
+              "count": 1
+            },
+            {
+              "xml": "<allow-navigation href=\"cdvfile:*\" />",
+              "count": 1
+            },
+            {
+              "xml": "<feature name=\"FileTransfer\"><param name=\"android-package\" value=\"org.apache.cordova.filetransfer.FileTransfer\" /></feature>",
+              "count": 1
+            },
+            {
+              "xml": "<feature name=\"FileOpener2\"><param name=\"android-package\" value=\"io.github.pwlin.cordova.plugins.fileopener2.FileOpener2\" /></feature>",
+              "count": 1
+            },
+            {
+              "xml": "<feature name=\"SplashScreen\"><param name=\"android-package\" value=\"org.apache.cordova.splashscreen.SplashScreen\" /><param name=\"onload\" value=\"true\" /></feature>",
+              "count": 1
             }
           ],
           "/widget": []
@@ -237,6 +257,14 @@
             },
             {
               "xml": "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />",
+              "count": 3
+            },
+            {
+              "xml": "<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />",
+              "count": 1
+            },
+            {
+              "xml": "<uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\" />",
               "count": 1
             }
           ],
@@ -250,7 +278,12 @@
               "count": 1
             }
           ],
-          "application": []
+          "application": [
+            {
+              "xml": "<provider android:authorities=\"${applicationId}.fileOpener2.provider\" android:exported=\"false\" android:grantUriPermissions=\"true\" android:name=\"io.github.pwlin.cordova.plugins.fileopener2.FileProvider\"><meta-data android:name=\"android.support.FILE_PROVIDER_PATHS\" android:resource=\"@xml/opener_paths\" /></provider>",
+              "count": 1
+            }
+          ]
         }
       },
       "config.xml": {
@@ -308,6 +341,19 @@
     },
     "ionic-plugin-keyboard": {
       "PACKAGE_NAME": "com.shotshock.twong"
+    },
+    "cordova-plugin-file": {
+      "PACKAGE_NAME": "com.shotshock.twong"
+    },
+    "cordova-plugin-file-transfer": {
+      "PACKAGE_NAME": "com.shotshock.twong"
+    },
+    "cordova-plugin-file-opener2": {
+      "ANDROID_SUPPORT_V4_VERSION": "27.+",
+      "PACKAGE_NAME": "com.shotshock.twong"
+    },
+    "cordova-plugin-splashscreen": {
+      "PACKAGE_NAME": "com.shotshock.twong"
     }
   },
   "dependent_plugins": {},
@@ -392,6 +438,211 @@
         "cordova.plugins.Keyboard"
       ],
       "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.DirectoryEntry",
+      "file": "plugins/cordova-plugin-file/www/DirectoryEntry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.DirectoryEntry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.DirectoryReader",
+      "file": "plugins/cordova-plugin-file/www/DirectoryReader.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.DirectoryReader"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Entry",
+      "file": "plugins/cordova-plugin-file/www/Entry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Entry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.File",
+      "file": "plugins/cordova-plugin-file/www/File.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.File"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileEntry",
+      "file": "plugins/cordova-plugin-file/www/FileEntry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileEntry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileError",
+      "file": "plugins/cordova-plugin-file/www/FileError.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileError"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileReader",
+      "file": "plugins/cordova-plugin-file/www/FileReader.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileReader"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileSystem",
+      "file": "plugins/cordova-plugin-file/www/FileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileUploadOptions",
+      "file": "plugins/cordova-plugin-file/www/FileUploadOptions.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileUploadOptions"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileUploadResult",
+      "file": "plugins/cordova-plugin-file/www/FileUploadResult.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileUploadResult"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileWriter",
+      "file": "plugins/cordova-plugin-file/www/FileWriter.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileWriter"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Flags",
+      "file": "plugins/cordova-plugin-file/www/Flags.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Flags"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.LocalFileSystem",
+      "file": "plugins/cordova-plugin-file/www/LocalFileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.LocalFileSystem"
+      ],
+      "merges": [
+        "window"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Metadata",
+      "file": "plugins/cordova-plugin-file/www/Metadata.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Metadata"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.ProgressEvent",
+      "file": "plugins/cordova-plugin-file/www/ProgressEvent.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.ProgressEvent"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.fileSystems",
+      "file": "plugins/cordova-plugin-file/www/fileSystems.js",
+      "pluginId": "cordova-plugin-file"
+    },
+    {
+      "id": "cordova-plugin-file.requestFileSystem",
+      "file": "plugins/cordova-plugin-file/www/requestFileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.requestFileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.resolveLocalFileSystemURI",
+      "file": "plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "window"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.isChrome",
+      "file": "plugins/cordova-plugin-file/www/browser/isChrome.js",
+      "pluginId": "cordova-plugin-file",
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.androidFileSystem",
+      "file": "plugins/cordova-plugin-file/www/android/FileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "FileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.fileSystems-roots",
+      "file": "plugins/cordova-plugin-file/www/fileSystems-roots.js",
+      "pluginId": "cordova-plugin-file",
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.fileSystemPaths",
+      "file": "plugins/cordova-plugin-file/www/fileSystemPaths.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "cordova"
+      ],
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file-transfer.FileTransferError",
+      "file": "plugins/cordova-plugin-file-transfer/www/FileTransferError.js",
+      "pluginId": "cordova-plugin-file-transfer",
+      "clobbers": [
+        "window.FileTransferError"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file-transfer.FileTransfer",
+      "file": "plugins/cordova-plugin-file-transfer/www/FileTransfer.js",
+      "pluginId": "cordova-plugin-file-transfer",
+      "clobbers": [
+        "window.FileTransfer"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file-opener2.FileOpener2",
+      "file": "plugins/cordova-plugin-file-opener2/www/plugins.FileOpener2.js",
+      "pluginId": "cordova-plugin-file-opener2",
+      "clobbers": [
+        "cordova.plugins.fileOpener2"
+      ]
+    },
+    {
+      "id": "cordova-plugin-splashscreen.SplashScreen",
+      "file": "plugins/cordova-plugin-splashscreen/www/splashscreen.js",
+      "pluginId": "cordova-plugin-splashscreen",
+      "clobbers": [
+        "navigator.splashscreen"
+      ]
     }
   ],
   "plugin_metadata": {
@@ -407,6 +658,10 @@
     "cordova-plugin-app-version": "0.1.9",
     "cordova-plugin-console": "1.1.0",
     "cordova-plugin-alipay-v2": "2.0.0",
-    "ionic-plugin-keyboard": "2.2.1"
+    "ionic-plugin-keyboard": "2.2.1",
+    "cordova-plugin-file": "6.0.2",
+    "cordova-plugin-file-transfer": "1.7.1",
+    "cordova-plugin-file-opener2": "3.0.5",
+    "cordova-plugin-splashscreen": "6.0.0"
   }
 }

+ 2 - 0
cordova/platforms/android/app/build.gradle

@@ -345,6 +345,7 @@ android {
 
 
 dependencies {
+    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
     implementation fileTree(dir: 'libs', include: '*.jar')
 
     if (cdvHelpers.getConfigPreference('GradlePluginKotlinEnabled', 'false').toBoolean()) {
@@ -353,6 +354,7 @@ dependencies {
 
     // SUB-PROJECT DEPENDENCIES START
     implementation(project(path: ":CordovaLib"))
+    implementation "com.android.support:support-v4:27.+"
     // SUB-PROJECT DEPENDENCIES END
 }
 

+ 6 - 2
cordova/platforms/android/app/src/main/AndroidManifest.xml

@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='utf-8'?>
-<manifest android:hardwareAccelerated="true" android:versionCode="10201" android:versionName="1.2.1" package="com.shotshock.twong" xmlns:android="http://schemas.android.com/apk/res/android">
+<manifest android:hardwareAccelerated="true" android:versionCode="10208" android:versionName="1.2.8" package="com.shotshock.twong" xmlns:android="http://schemas.android.com/apk/res/android">
     <supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:resizeable="true" android:smallScreens="true" android:xlargeScreens="true" />
     <uses-permission android:name="android.permission.INTERNET" />
     <application android:hardwareAccelerated="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:usesCleartextTraffic="true">
@@ -118,13 +118,15 @@
             </intent-filter>
         </activity>
         <meta-data android:name="android.max_aspect" android:value="2.16" />
+        <provider android:authorities="${applicationId}.fileOpener2.provider" android:exported="false" android:grantUriPermissions="true" android:name="io.github.pwlin.cordova.plugins.fileopener2.FileProvider">
+            <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/opener_paths" />
+        </provider>
     </application>
     <permission android:name="com.shotshock.twong.permission.JPUSH_MESSAGE" android:protectionLevel="signature" />
     <uses-permission android:name="com.shotshock.twong.permission.JPUSH_MESSAGE" />
     <uses-permission android:name="android.permission.RECEIVE_USER_PRESENT" />
     <uses-permission android:name="android.permission.READ_PHONE_STATE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
@@ -138,4 +140,6 @@
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
     <uses-permission android:name="android.permission.GET_TASKS" />
     <uses-permission android:name="com.huawei.android.launcher.permission.CHANGE_BADGE " />
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 </manifest>

+ 210 - 1
cordova/platforms/android/app/src/main/assets/www/cordova_plugins.js

@@ -80,6 +80,211 @@ cordova.define('cordova/plugin_list', function(require, exports, module) {
         "cordova.plugins.Keyboard"
       ],
       "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.DirectoryEntry",
+      "file": "plugins/cordova-plugin-file/www/DirectoryEntry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.DirectoryEntry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.DirectoryReader",
+      "file": "plugins/cordova-plugin-file/www/DirectoryReader.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.DirectoryReader"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Entry",
+      "file": "plugins/cordova-plugin-file/www/Entry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Entry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.File",
+      "file": "plugins/cordova-plugin-file/www/File.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.File"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileEntry",
+      "file": "plugins/cordova-plugin-file/www/FileEntry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileEntry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileError",
+      "file": "plugins/cordova-plugin-file/www/FileError.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileError"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileReader",
+      "file": "plugins/cordova-plugin-file/www/FileReader.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileReader"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileSystem",
+      "file": "plugins/cordova-plugin-file/www/FileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileUploadOptions",
+      "file": "plugins/cordova-plugin-file/www/FileUploadOptions.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileUploadOptions"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileUploadResult",
+      "file": "plugins/cordova-plugin-file/www/FileUploadResult.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileUploadResult"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileWriter",
+      "file": "plugins/cordova-plugin-file/www/FileWriter.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileWriter"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Flags",
+      "file": "plugins/cordova-plugin-file/www/Flags.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Flags"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.LocalFileSystem",
+      "file": "plugins/cordova-plugin-file/www/LocalFileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.LocalFileSystem"
+      ],
+      "merges": [
+        "window"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Metadata",
+      "file": "plugins/cordova-plugin-file/www/Metadata.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Metadata"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.ProgressEvent",
+      "file": "plugins/cordova-plugin-file/www/ProgressEvent.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.ProgressEvent"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.fileSystems",
+      "file": "plugins/cordova-plugin-file/www/fileSystems.js",
+      "pluginId": "cordova-plugin-file"
+    },
+    {
+      "id": "cordova-plugin-file.requestFileSystem",
+      "file": "plugins/cordova-plugin-file/www/requestFileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.requestFileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.resolveLocalFileSystemURI",
+      "file": "plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "window"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.isChrome",
+      "file": "plugins/cordova-plugin-file/www/browser/isChrome.js",
+      "pluginId": "cordova-plugin-file",
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.androidFileSystem",
+      "file": "plugins/cordova-plugin-file/www/android/FileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "FileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.fileSystems-roots",
+      "file": "plugins/cordova-plugin-file/www/fileSystems-roots.js",
+      "pluginId": "cordova-plugin-file",
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.fileSystemPaths",
+      "file": "plugins/cordova-plugin-file/www/fileSystemPaths.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "cordova"
+      ],
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file-transfer.FileTransferError",
+      "file": "plugins/cordova-plugin-file-transfer/www/FileTransferError.js",
+      "pluginId": "cordova-plugin-file-transfer",
+      "clobbers": [
+        "window.FileTransferError"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file-transfer.FileTransfer",
+      "file": "plugins/cordova-plugin-file-transfer/www/FileTransfer.js",
+      "pluginId": "cordova-plugin-file-transfer",
+      "clobbers": [
+        "window.FileTransfer"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file-opener2.FileOpener2",
+      "file": "plugins/cordova-plugin-file-opener2/www/plugins.FileOpener2.js",
+      "pluginId": "cordova-plugin-file-opener2",
+      "clobbers": [
+        "cordova.plugins.fileOpener2"
+      ]
+    },
+    {
+      "id": "cordova-plugin-splashscreen.SplashScreen",
+      "file": "plugins/cordova-plugin-splashscreen/www/splashscreen.js",
+      "pluginId": "cordova-plugin-splashscreen",
+      "clobbers": [
+        "navigator.splashscreen"
+      ]
     }
   ];
   module.exports.metadata = {
@@ -95,6 +300,10 @@ cordova.define('cordova/plugin_list', function(require, exports, module) {
     "cordova-plugin-app-version": "0.1.9",
     "cordova-plugin-console": "1.1.0",
     "cordova-plugin-alipay-v2": "2.0.0",
-    "ionic-plugin-keyboard": "2.2.1"
+    "ionic-plugin-keyboard": "2.2.1",
+    "cordova-plugin-file": "6.0.2",
+    "cordova-plugin-file-transfer": "1.7.1",
+    "cordova-plugin-file-opener2": "3.0.5",
+    "cordova-plugin-splashscreen": "6.0.0"
   };
 });

File diff suppressed because it is too large
+ 0 - 0
cordova/platforms/android/app/src/main/assets/www/h5/css/chunk-493fc81d.c05350da.css


File diff suppressed because it is too large
+ 1 - 1
cordova/platforms/android/app/src/main/assets/www/index.html


+ 199 - 0
cordova/platforms/android/app/src/main/java/io/github/pwlin/cordova/plugins/fileopener2/FileOpener2.java

@@ -0,0 +1,199 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2013 pwlin - pwlin05@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+package io.github.pwlin.cordova.plugins.fileopener2;
+
+import java.io.File;
+import java.util.List;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.webkit.MimeTypeMap;
+
+import io.github.pwlin.cordova.plugins.fileopener2.FileProvider;
+
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.PluginResult;
+import org.apache.cordova.CordovaResourceApi;
+
+public class FileOpener2 extends CordovaPlugin {
+
+	/**
+	 * Executes the request and returns a boolean.
+	 *
+	 * @param action
+	 *            The action to execute.
+	 * @param args
+	 *            JSONArry of arguments for the plugin.
+	 * @param callbackContext
+	 *            The callback context used when calling back into JavaScript.
+	 * @return boolean.
+	 */
+	public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+		if (action.equals("open")) {
+			String fileUrl = args.getString(0);
+			String contentType = args.getString(1);
+			Boolean openWithDefault = true;
+			if(args.length() > 2){
+				openWithDefault = args.getBoolean(2);
+			}
+			this._open(fileUrl, contentType, openWithDefault, callbackContext);
+		}
+		else if (action.equals("uninstall")) {
+			this._uninstall(args.getString(0), callbackContext);
+		}
+		else if (action.equals("appIsInstalled")) {
+			JSONObject successObj = new JSONObject();
+			if (this._appIsInstalled(args.getString(0))) {
+				successObj.put("status", PluginResult.Status.OK.ordinal());
+				successObj.put("message", "Installed");
+			}
+			else {
+				successObj.put("status", PluginResult.Status.NO_RESULT.ordinal());
+				successObj.put("message", "Not installed");
+			}
+			callbackContext.success(successObj);
+		}
+		else {
+			JSONObject errorObj = new JSONObject();
+			errorObj.put("status", PluginResult.Status.INVALID_ACTION.ordinal());
+			errorObj.put("message", "Invalid action");
+			callbackContext.error(errorObj);
+		}
+		return true;
+	}
+
+	private void _open(String fileArg, String contentType, Boolean openWithDefault, CallbackContext callbackContext) throws JSONException {
+		String fileName = "";
+		try {
+			CordovaResourceApi resourceApi = webView.getResourceApi();
+			Uri fileUri = resourceApi.remapUri(Uri.parse(fileArg));
+			fileName = fileUri.getPath();
+		} catch (Exception e) {
+			fileName = fileArg;
+		}
+		File file = new File(fileName);
+		if (file.exists()) {
+			try {
+				if (contentType == null || contentType.trim().equals("")) {
+				    contentType = _getMimeType(fileName);
+				}
+
+				Intent intent;
+				if (contentType.equals("application/vnd.android.package-archive")) {
+					// https://stackoverflow.com/questions/9637629/can-we-install-an-apk-from-a-contentprovider/9672282#9672282
+					intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
+					Uri path;
+					if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+						path = Uri.fromFile(file);
+					} else {
+						Context context = cordova.getActivity().getApplicationContext();
+						path = FileProvider.getUriForFile(context, cordova.getActivity().getPackageName() + ".fileOpener2.provider", file);
+					}
+					intent.setDataAndType(path, contentType);
+					intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);
+
+				} else {
+					intent = new Intent(Intent.ACTION_VIEW);
+					Context context = cordova.getActivity().getApplicationContext();
+					Uri path = FileProvider.getUriForFile(context, cordova.getActivity().getPackageName() + ".fileOpener2.provider", file);
+					intent.setDataAndType(path, contentType);
+					intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
+				}
+
+				/*
+				 * @see
+				 * http://stackoverflow.com/questions/14321376/open-an-activity-from-a-cordovaplugin
+				 */
+				 if(openWithDefault){
+					 cordova.getActivity().startActivity(intent);
+				 }
+				 else{
+					 cordova.getActivity().startActivity(Intent.createChooser(intent, "Open File in..."));
+				 }
+
+				callbackContext.success();
+			} catch (android.content.ActivityNotFoundException e) {
+				JSONObject errorObj = new JSONObject();
+				errorObj.put("status", PluginResult.Status.ERROR.ordinal());
+				errorObj.put("message", "Activity not found: " + e.getMessage());
+				callbackContext.error(errorObj);
+			}
+		} else {
+			JSONObject errorObj = new JSONObject();
+			errorObj.put("status", PluginResult.Status.ERROR.ordinal());
+			errorObj.put("message", "File not found");
+			callbackContext.error(errorObj);
+		}
+	}
+
+	private String _getMimeType(String url) {
+	    String mimeType = "*/*";
+	    int extensionIndex = url.lastIndexOf('.');
+	    if (extensionIndex > 0) {
+		String extMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(url.substring(extensionIndex+1));
+		if (extMimeType != null) {
+		    mimeType = extMimeType;
+		}
+	    }
+	    return mimeType;
+	}
+
+	private void _uninstall(String packageId, CallbackContext callbackContext) throws JSONException {
+		if (this._appIsInstalled(packageId)) {
+			Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE);
+			intent.setData(Uri.parse("package:" + packageId));
+			cordova.getActivity().startActivity(intent);
+			callbackContext.success();
+		}
+		else {
+			JSONObject errorObj = new JSONObject();
+			errorObj.put("status", PluginResult.Status.ERROR.ordinal());
+			errorObj.put("message", "This package is not installed");
+			callbackContext.error(errorObj);
+		}
+	}
+
+	private boolean _appIsInstalled(String packageId) {
+		PackageManager pm = cordova.getActivity().getPackageManager();
+        boolean appInstalled = false;
+        try {
+            pm.getPackageInfo(packageId, PackageManager.GET_ACTIVITIES);
+            appInstalled = true;
+        } catch (PackageManager.NameNotFoundException e) {
+            appInstalled = false;
+        }
+        return appInstalled;
+	}
+
+}
+

+ 29 - 0
cordova/platforms/android/app/src/main/java/io/github/pwlin/cordova/plugins/fileopener2/FileProvider.java

@@ -0,0 +1,29 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2013 pwlin - pwlin05@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+package io.github.pwlin.cordova.plugins.fileopener2;
+
+/*
+ * http://stackoverflow.com/questions/40746144/error-with-duplicated-fileprovider-in-manifest-xml-with-cordova/41550634#41550634
+ */
+public class FileProvider extends android.support.v4.content.FileProvider {
+}

+ 294 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/AssetFilesystem.java

@@ -0,0 +1,294 @@
+/*
+       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.file;
+
+import android.content.res.AssetManager;
+import android.net.Uri;
+
+import org.apache.cordova.CordovaResourceApi;
+import org.apache.cordova.LOG;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+public class AssetFilesystem extends Filesystem {
+
+    private final AssetManager assetManager;
+
+    // A custom gradle hook creates the cdvasset.manifest file, which speeds up asset listing a tonne.
+    // See: http://stackoverflow.com/questions/16911558/android-assetmanager-list-incredibly-slow
+    private static Object listCacheLock = new Object();
+    private static boolean listCacheFromFile;
+    private static Map<String, String[]> listCache;
+    private static Map<String, Long> lengthCache;
+
+    private static final String LOG_TAG = "AssetFilesystem";
+
+    private void lazyInitCaches() {
+        synchronized (listCacheLock) {
+            if (listCache == null) {
+                ObjectInputStream ois = null;
+                try {
+                    ois = new ObjectInputStream(assetManager.open("cdvasset.manifest"));
+                    listCache = (Map<String, String[]>) ois.readObject();
+                    lengthCache = (Map<String, Long>) ois.readObject();
+                    listCacheFromFile = true;
+                } catch (ClassNotFoundException e) {
+                    e.printStackTrace();
+                } catch (IOException e) {
+                    // Asset manifest won't exist if the gradle hook isn't set up correctly.
+                } finally {
+                    if (ois != null) {
+                        try {
+                            ois.close();
+                        } catch (IOException e) {
+                            LOG.d(LOG_TAG, e.getLocalizedMessage());
+                        }
+                    }
+                }
+                if (listCache == null) {
+                    LOG.w("AssetFilesystem", "Asset manifest not found. Recursive copies and directory listing will be slow.");
+                    listCache = new HashMap<String, String[]>();
+                }
+            }
+        }
+    }
+
+    private String[] listAssets(String assetPath) throws IOException {
+        if (assetPath.startsWith("/")) {
+            assetPath = assetPath.substring(1);
+        }
+        if (assetPath.endsWith("/")) {
+            assetPath = assetPath.substring(0, assetPath.length() - 1);
+        }
+        lazyInitCaches();
+        String[] ret = listCache.get(assetPath);
+        if (ret == null) {
+            if (listCacheFromFile) {
+                ret = new String[0];
+            } else {
+                ret = assetManager.list(assetPath);
+                listCache.put(assetPath, ret);
+            }
+        }
+        return ret;
+    }
+
+    private long getAssetSize(String assetPath) throws FileNotFoundException {
+        if (assetPath.startsWith("/")) {
+            assetPath = assetPath.substring(1);
+        }
+        lazyInitCaches();
+        if (lengthCache != null) {
+            Long ret = lengthCache.get(assetPath);
+            if (ret == null) {
+                throw new FileNotFoundException("Asset not found: " + assetPath);
+            }
+            return ret;
+        }
+        CordovaResourceApi.OpenForReadResult offr = null;
+        try {
+            offr = resourceApi.openForRead(nativeUriForFullPath(assetPath));
+            long length = offr.length;
+            if (length < 0) {
+                // available() doesn't always yield the file size, but for assets it does.
+                length = offr.inputStream.available();
+            }
+            return length;
+        } catch (IOException e) {
+            FileNotFoundException fnfe = new FileNotFoundException("File not found: " + assetPath);
+            fnfe.initCause(e);
+            throw fnfe;
+        } finally {
+            if (offr != null) {
+                try {
+                    offr.inputStream.close();
+                } catch (IOException e) {
+                    LOG.d(LOG_TAG, e.getLocalizedMessage());
+                }
+            }
+        }
+    }
+
+    public AssetFilesystem(AssetManager assetManager, CordovaResourceApi resourceApi) {
+        super(Uri.parse("file:///android_asset/"), "assets", resourceApi);
+        this.assetManager = assetManager;
+	}
+
+    @Override
+    public Uri toNativeUri(LocalFilesystemURL inputURL) {
+        return nativeUriForFullPath(inputURL.path);
+    }
+
+    @Override
+    public LocalFilesystemURL toLocalUri(Uri inputURL) {
+        if (!"file".equals(inputURL.getScheme())) {
+            return null;
+        }
+        File f = new File(inputURL.getPath());
+        // Removes and duplicate /s (e.g. file:///a//b/c)
+        Uri resolvedUri = Uri.fromFile(f);
+        String rootUriNoTrailingSlash = rootUri.getEncodedPath();
+        rootUriNoTrailingSlash = rootUriNoTrailingSlash.substring(0, rootUriNoTrailingSlash.length() - 1);
+        if (!resolvedUri.getEncodedPath().startsWith(rootUriNoTrailingSlash)) {
+            return null;
+        }
+        String subPath = resolvedUri.getEncodedPath().substring(rootUriNoTrailingSlash.length());
+        // Strip leading slash
+        if (!subPath.isEmpty()) {
+            subPath = subPath.substring(1);
+        }
+        Uri.Builder b = new Uri.Builder()
+            .scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
+            .authority("localhost")
+            .path(name);
+        if (!subPath.isEmpty()) {
+            b.appendEncodedPath(subPath);
+        }
+        if (isDirectory(subPath) || inputURL.getPath().endsWith("/")) {
+            // Add trailing / for directories.
+            b.appendEncodedPath("");
+        }
+        return LocalFilesystemURL.parse(b.build());
+    }
+
+    private boolean isDirectory(String assetPath) {
+        try {
+            return listAssets(assetPath).length != 0;
+        } catch (IOException e) {
+            return false;
+        }
+    }
+
+    @Override
+    public LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException {
+        String pathNoSlashes = inputURL.path.substring(1);
+        if (pathNoSlashes.endsWith("/")) {
+            pathNoSlashes = pathNoSlashes.substring(0, pathNoSlashes.length() - 1);
+        }
+
+        String[] files;
+        try {
+            files = listAssets(pathNoSlashes);
+        } catch (IOException e) {
+            FileNotFoundException fnfe = new FileNotFoundException();
+            fnfe.initCause(e);
+            throw fnfe;
+        }
+
+        LocalFilesystemURL[] entries = new LocalFilesystemURL[files.length];
+        for (int i = 0; i < files.length; ++i) {
+            entries[i] = localUrlforFullPath(new File(inputURL.path, files[i]).getPath());
+        }
+        return entries;
+	}
+
+    @Override
+    public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL,
+                                         String path, JSONObject options, boolean directory)
+            throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+        if (options != null && options.optBoolean("create")) {
+            throw new UnsupportedOperationException("Assets are read-only");
+        }
+
+        // Check whether the supplied path is absolute or relative
+        if (directory && !path.endsWith("/")) {
+            path += "/";
+        }
+
+        LocalFilesystemURL requestedURL;
+        if (path.startsWith("/")) {
+            requestedURL = localUrlforFullPath(normalizePath(path));
+        } else {
+            requestedURL = localUrlforFullPath(normalizePath(inputURL.path + "/" + path));
+        }
+
+        // Throws a FileNotFoundException if it doesn't exist.
+        getFileMetadataForLocalURL(requestedURL);
+
+        boolean isDir = isDirectory(requestedURL.path);
+        if (directory && !isDir) {
+            throw new TypeMismatchException("path doesn't exist or is file");
+        } else if (!directory && isDir) {
+            throw new TypeMismatchException("path doesn't exist or is directory");
+        }
+
+        // Return the directory
+        return makeEntryForURL(requestedURL);
+    }
+
+    @Override
+	public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
+        JSONObject metadata = new JSONObject();
+        long size = inputURL.isDirectory ? 0 : getAssetSize(inputURL.path);
+        try {
+        	metadata.put("size", size);
+        	metadata.put("type", inputURL.isDirectory ? "text/directory" : resourceApi.getMimeType(toNativeUri(inputURL)));
+        	metadata.put("name", new File(inputURL.path).getName());
+        	metadata.put("fullPath", inputURL.path);
+        	metadata.put("lastModifiedDate", 0);
+        } catch (JSONException e) {
+            return null;
+        }
+        return metadata;
+	}
+
+	@Override
+	public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) {
+		return false;
+	}
+
+    @Override
+    long writeToFileAtURL(LocalFilesystemURL inputURL, String data, int offset, boolean isBinary) throws NoModificationAllowedException, IOException {
+        throw new NoModificationAllowedException("Assets are read-only");
+    }
+
+    @Override
+    long truncateFileAtURL(LocalFilesystemURL inputURL, long size) throws IOException, NoModificationAllowedException {
+        throw new NoModificationAllowedException("Assets are read-only");
+    }
+
+    @Override
+    String filesystemPathForURL(LocalFilesystemURL url) {
+        return new File(rootUri.getPath(), url.path).toString();
+    }
+
+    @Override
+    LocalFilesystemURL URLforFilesystemPath(String path) {
+        return null;
+    }
+
+    @Override
+    boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException, NoModificationAllowedException {
+        throw new NoModificationAllowedException("Assets are read-only");
+    }
+
+    @Override
+    boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws NoModificationAllowedException {
+        throw new NoModificationAllowedException("Assets are read-only");
+    }
+
+}

+ 223 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/ContentFilesystem.java

@@ -0,0 +1,223 @@
+/*
+       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.file;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import org.apache.cordova.CordovaResourceApi;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class ContentFilesystem extends Filesystem {
+
+    private final Context context;
+
+	public ContentFilesystem(Context context, CordovaResourceApi resourceApi) {
+		super(Uri.parse("content://"), "content", resourceApi);
+        this.context = context;
+	}
+
+    @Override
+    public Uri toNativeUri(LocalFilesystemURL inputURL) {
+        String authorityAndPath = inputURL.uri.getEncodedPath().substring(this.name.length() + 2);
+        if (authorityAndPath.length() < 2) {
+            return null;
+        }
+        String ret = "content://" + authorityAndPath;
+        String query = inputURL.uri.getEncodedQuery();
+        if (query != null) {
+            ret += '?' + query;
+        }
+        String frag = inputURL.uri.getEncodedFragment();
+        if (frag != null) {
+            ret += '#' + frag;
+        }
+        return Uri.parse(ret);
+    }
+
+    @Override
+    public LocalFilesystemURL toLocalUri(Uri inputURL) {
+        if (!"content".equals(inputURL.getScheme())) {
+            return null;
+        }
+        String subPath = inputURL.getEncodedPath();
+        if (subPath.length() > 0) {
+            subPath = subPath.substring(1);
+        }
+        Uri.Builder b = new Uri.Builder()
+            .scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
+            .authority("localhost")
+            .path(name)
+            .appendPath(inputURL.getAuthority());
+        if (subPath.length() > 0) {
+            b.appendEncodedPath(subPath);
+        }
+        Uri localUri = b.encodedQuery(inputURL.getEncodedQuery())
+            .encodedFragment(inputURL.getEncodedFragment())
+            .build();
+        return LocalFilesystemURL.parse(localUri);
+    }
+
+    @Override
+	public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL,
+			String fileName, JSONObject options, boolean directory) throws IOException, TypeMismatchException, JSONException {
+        throw new UnsupportedOperationException("getFile() not supported for content:. Use resolveLocalFileSystemURL instead.");
+	}
+
+	@Override
+	public boolean removeFileAtLocalURL(LocalFilesystemURL inputURL)
+			throws NoModificationAllowedException {
+        Uri contentUri = toNativeUri(inputURL);
+		try {
+            context.getContentResolver().delete(contentUri, null, null);
+		} catch (UnsupportedOperationException t) {
+			// Was seeing this on the File mobile-spec tests on 4.0.3 x86 emulator.
+			// The ContentResolver applies only when the file was registered in the
+			// first case, which is generally only the case with images.
+            NoModificationAllowedException nmae = new NoModificationAllowedException("Deleting not supported for content uri: " + contentUri);
+            nmae.initCause(t);
+            throw nmae;
+		}
+        return true;
+	}
+
+	@Override
+	public boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL)
+			throws NoModificationAllowedException {
+		throw new NoModificationAllowedException("Cannot remove content url");
+	}
+
+    @Override
+    public LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException {
+        throw new UnsupportedOperationException("readEntriesAtLocalURL() not supported for content:. Use resolveLocalFileSystemURL instead.");
+    }
+
+	@Override
+	public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
+        long size = -1;
+        long lastModified = 0;
+        Uri nativeUri = toNativeUri(inputURL);
+        String mimeType = resourceApi.getMimeType(nativeUri);
+        Cursor cursor = openCursorForURL(nativeUri);
+        try {
+            if (cursor != null && cursor.moveToFirst()) {
+                Long sizeForCursor = resourceSizeForCursor(cursor);
+                if (sizeForCursor != null) {
+                    size = sizeForCursor.longValue();
+                }
+                Long modified = lastModifiedDateForCursor(cursor);
+                if (modified != null)
+                    lastModified = modified.longValue();
+            } else {
+                // Some content providers don't support cursors at all!
+                CordovaResourceApi.OpenForReadResult offr = resourceApi.openForRead(nativeUri);
+                size = offr.length;
+            }
+        } catch (IOException e) {
+            FileNotFoundException fnfe = new FileNotFoundException();
+            fnfe.initCause(e);
+            throw fnfe;
+        } finally {
+        	if (cursor != null)
+        		cursor.close();
+        }
+
+        JSONObject metadata = new JSONObject();
+        try {
+        	metadata.put("size", size);
+        	metadata.put("type", mimeType);
+        	metadata.put("name", name);
+        	metadata.put("fullPath", inputURL.path);
+        	metadata.put("lastModifiedDate", lastModified);
+        } catch (JSONException e) {
+        	return null;
+        }
+        return metadata;
+	}
+
+	@Override
+	public long writeToFileAtURL(LocalFilesystemURL inputURL, String data,
+			int offset, boolean isBinary) throws NoModificationAllowedException {
+        throw new NoModificationAllowedException("Couldn't write to file given its content URI");
+    }
+	@Override
+	public long truncateFileAtURL(LocalFilesystemURL inputURL, long size)
+			throws NoModificationAllowedException {
+        throw new NoModificationAllowedException("Couldn't truncate file given its content URI");
+	}
+
+	protected Cursor openCursorForURL(Uri nativeUri) {
+        ContentResolver contentResolver = context.getContentResolver();
+        try {
+            return contentResolver.query(nativeUri, null, null, null, null);
+        } catch (UnsupportedOperationException e) {
+            return null;
+        }
+	}
+
+	private Long resourceSizeForCursor(Cursor cursor) {
+        int columnIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
+        if (columnIndex != -1) {
+            String sizeStr = cursor.getString(columnIndex);
+            if (sizeStr != null) {
+            	return Long.parseLong(sizeStr);
+            }
+        }
+        return null;
+	}
+	
+	protected Long lastModifiedDateForCursor(Cursor cursor) {
+        int columnIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED);
+        if (columnIndex == -1) {
+            columnIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED);
+        }
+        if (columnIndex != -1) {
+            String dateStr = cursor.getString(columnIndex);
+            if (dateStr != null) {
+                return Long.parseLong(dateStr);
+            }
+        }
+        return null;
+	}
+
+    @Override
+    public String filesystemPathForURL(LocalFilesystemURL url) {
+        File f = resourceApi.mapUriToFile(toNativeUri(url));
+        return f == null ? null : f.getAbsolutePath();
+    }
+
+	@Override
+	public LocalFilesystemURL URLforFilesystemPath(String path) {
+		// Returns null as we don't support reverse mapping back to content:// URLs
+		return null;
+	}
+
+	@Override
+	public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) {
+		return true;
+	}
+}

+ 134 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/DirectoryManager.java

@@ -0,0 +1,134 @@
+/*
+       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.file;
+
+import android.os.Environment;
+import android.os.StatFs;
+
+import java.io.File;
+
+/**
+ * This class provides file directory utilities.
+ * All file operations are performed on the SD card.
+ *
+ * It is used by the FileUtils class.
+ */
+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 space in external storage
+     *
+     * @return 		Size in KB or -1 if not available
+     */
+    public static long getFreeExternalStorageSpace() {
+        String status = Environment.getExternalStorageState();
+        long freeSpaceInBytes = 0;
+
+        // Check if external storage exists
+        if (status.equals(Environment.MEDIA_MOUNTED)) {
+            freeSpaceInBytes = getFreeSpaceInBytes(Environment.getExternalStorageDirectory().getPath());
+        } else {
+            // If no external storage then return -1
+            return -1;
+        }
+
+        return freeSpaceInBytes / 1024;
+    }
+
+    /**
+     * Given a path return the number of free bytes in the filesystem containing the path.
+     *
+     * @param path to the file system
+     * @return free space in bytes
+     */
+    public static long getFreeSpaceInBytes(String path) {
+        try {
+            StatFs stat = new StatFs(path);
+            long blockSize = stat.getBlockSize();
+            long availableBlocks = stat.getAvailableBlocks();
+            return availableBlocks * blockSize;
+        } catch (IllegalArgumentException e) {
+            // The path was invalid. Just return 0 free bytes.
+            return 0;
+        }
+    }
+
+    /**
+     * 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;
+    }
+}

+ 29 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/EncodingException.java

@@ -0,0 +1,29 @@
+/*
+       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.file;
+
+@SuppressWarnings("serial")
+public class EncodingException extends Exception {
+
+    public EncodingException(String message) {
+        super(message);
+    }
+
+}

+ 29 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/FileExistsException.java

@@ -0,0 +1,29 @@
+/*
+       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.file;
+
+@SuppressWarnings("serial")
+public class FileExistsException extends Exception {
+
+    public FileExistsException(String msg) {
+        super(msg);
+    }
+
+}

+ 1225 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/FileUtils.java

@@ -0,0 +1,1225 @@
+/*
+       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.file;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.util.Base64;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.LOG;
+import org.apache.cordova.PermissionHelper;
+import org.apache.cordova.PluginResult;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.security.Permission;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+/**
+ * This class provides file and directory services to JavaScript.
+ */
+public class FileUtils extends CordovaPlugin {
+    private static final String LOG_TAG = "FileUtils";
+
+    public static int NOT_FOUND_ERR = 1;
+    public static int SECURITY_ERR = 2;
+    public static int ABORT_ERR = 3;
+
+    public static int NOT_READABLE_ERR = 4;
+    public static int ENCODING_ERR = 5;
+    public static int NO_MODIFICATION_ALLOWED_ERR = 6;
+    public static int INVALID_STATE_ERR = 7;
+    public static int SYNTAX_ERR = 8;
+    public static int INVALID_MODIFICATION_ERR = 9;
+    public static int QUOTA_EXCEEDED_ERR = 10;
+    public static int TYPE_MISMATCH_ERR = 11;
+    public static int PATH_EXISTS_ERR = 12;
+
+    /*
+     * Permission callback codes
+     */
+
+    public static final int ACTION_GET_FILE = 0;
+    public static final int ACTION_WRITE = 1;
+    public static final int ACTION_GET_DIRECTORY = 2;
+
+    public static final int WRITE = 3;
+    public static final int READ = 4;
+
+    public static int UNKNOWN_ERR = 1000;
+
+    private boolean configured = false;
+
+    private PendingRequests pendingRequests;
+
+
+
+    /*
+     * We need both read and write when accessing the storage, I think.
+     */
+
+    private String [] permissions = {
+            Manifest.permission.READ_EXTERNAL_STORAGE,
+            Manifest.permission.WRITE_EXTERNAL_STORAGE };
+
+    // This field exists only to support getEntry, below, which has been deprecated
+    private static FileUtils filePlugin;
+
+    private interface FileOp {
+        void run(JSONArray args) throws Exception;
+    }
+
+    private ArrayList<Filesystem> filesystems;
+
+    public void registerFilesystem(Filesystem fs) {
+    	if (fs != null && filesystemForName(fs.name)== null) {
+    		this.filesystems.add(fs);
+    	}
+    }
+
+    private Filesystem filesystemForName(String name) {
+    	for (Filesystem fs:filesystems) {
+    		if (fs != null && fs.name != null && fs.name.equals(name)) {
+    			return fs;
+    		}
+    	}
+    	return null;
+    }
+
+    protected String[] getExtraFileSystemsPreference(Activity activity) {
+        String fileSystemsStr = preferences.getString("androidextrafilesystems", "files,files-external,documents,sdcard,cache,cache-external,assets,root");
+        return fileSystemsStr.split(",");
+    }
+
+    protected void registerExtraFileSystems(String[] filesystems, HashMap<String, String> availableFileSystems) {
+        HashSet<String> installedFileSystems = new HashSet<String>();
+
+        /* Register filesystems in order */
+        for (String fsName : filesystems) {
+            if (!installedFileSystems.contains(fsName)) {
+                String fsRoot = availableFileSystems.get(fsName);
+                if (fsRoot != null) {
+                    File newRoot = new File(fsRoot);
+                    if (newRoot.mkdirs() || newRoot.isDirectory()) {
+                        registerFilesystem(new LocalFilesystem(fsName, webView.getContext(), webView.getResourceApi(), newRoot));
+                        installedFileSystems.add(fsName);
+                    } else {
+                       LOG.d(LOG_TAG, "Unable to create root dir for filesystem \"" + fsName + "\", skipping");
+                    }
+                } else {
+                    LOG.d(LOG_TAG, "Unrecognized extra filesystem identifier: " + fsName);
+                }
+            }
+        }
+    }
+
+    protected HashMap<String, String> getAvailableFileSystems(Activity activity) {
+        Context context = activity.getApplicationContext();
+        HashMap<String, String> availableFileSystems = new HashMap<String,String>();
+
+        availableFileSystems.put("files", context.getFilesDir().getAbsolutePath());
+        availableFileSystems.put("documents", new File(context.getFilesDir(), "Documents").getAbsolutePath());
+        availableFileSystems.put("cache", context.getCacheDir().getAbsolutePath());
+        availableFileSystems.put("root", "/");
+        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+          try {
+            availableFileSystems.put("files-external", context.getExternalFilesDir(null).getAbsolutePath());
+            availableFileSystems.put("sdcard", Environment.getExternalStorageDirectory().getAbsolutePath());
+            availableFileSystems.put("cache-external", context.getExternalCacheDir().getAbsolutePath());
+          }
+          catch(NullPointerException e) {
+              LOG.d(LOG_TAG, "External storage unavailable, check to see if USB Mass Storage Mode is on");
+          }
+        }
+
+        return availableFileSystems;
+    }
+
+    @Override
+    public void initialize(CordovaInterface cordova, CordovaWebView webView) {
+    	super.initialize(cordova, webView);
+    	this.filesystems = new ArrayList<Filesystem>();
+        this.pendingRequests = new PendingRequests();
+
+    	String tempRoot = null;
+    	String persistentRoot = null;
+
+    	Activity activity = cordova.getActivity();
+    	String packageName = activity.getPackageName();
+
+        String location = preferences.getString("androidpersistentfilelocation", "internal");
+
+    	tempRoot = activity.getCacheDir().getAbsolutePath();
+    	if ("internal".equalsIgnoreCase(location)) {
+    		persistentRoot = activity.getFilesDir().getAbsolutePath() + "/files/";
+    		this.configured = true;
+    	} else if ("compatibility".equalsIgnoreCase(location)) {
+    		/*
+    		 *  Fall-back to compatibility mode -- this is the logic implemented in
+    		 *  earlier versions of this plugin, and should be maintained here so
+    		 *  that apps which were originally deployed with older versions of the
+    		 *  plugin can continue to provide access to files stored under those
+    		 *  versions.
+    		 */
+    		if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+    			persistentRoot = Environment.getExternalStorageDirectory().getAbsolutePath();
+    			tempRoot = Environment.getExternalStorageDirectory().getAbsolutePath() +
+    					"/Android/data/" + packageName + "/cache/";
+    		} else {
+    			persistentRoot = "/data/data/" + packageName;
+    		}
+    		this.configured = true;
+    	}
+
+    	if (this.configured) {
+			// Create the directories if they don't exist.
+			File tmpRootFile = new File(tempRoot);
+            File persistentRootFile = new File(persistentRoot);
+            tmpRootFile.mkdirs();
+            persistentRootFile.mkdirs();
+
+    		// Register initial filesystems
+    		// Note: The temporary and persistent filesystems need to be the first two
+    		// registered, so that they will match window.TEMPORARY and window.PERSISTENT,
+    		// per spec.
+    		this.registerFilesystem(new LocalFilesystem("temporary", webView.getContext(), webView.getResourceApi(), tmpRootFile));
+    		this.registerFilesystem(new LocalFilesystem("persistent", webView.getContext(), webView.getResourceApi(), persistentRootFile));
+    		this.registerFilesystem(new ContentFilesystem(webView.getContext(), webView.getResourceApi()));
+            this.registerFilesystem(new AssetFilesystem(webView.getContext().getAssets(), webView.getResourceApi()));
+
+            registerExtraFileSystems(getExtraFileSystemsPreference(activity), getAvailableFileSystems(activity));
+
+    		// Initialize static plugin reference for deprecated getEntry method
+    		if (filePlugin == null) {
+    			FileUtils.filePlugin = this;
+    		}
+    	} else {
+    		LOG.e(LOG_TAG, "File plugin configuration error: Please set AndroidPersistentFileLocation in config.xml to one of \"internal\" (for new applications) or \"compatibility\" (for compatibility with previous versions)");
+    		activity.finish();
+    	}
+    }
+
+    public static FileUtils getFilePlugin() {
+		return filePlugin;
+	}
+
+	private Filesystem filesystemForURL(LocalFilesystemURL localURL) {
+    	if (localURL == null) return null;
+    	return filesystemForName(localURL.fsName);
+    }
+
+    @Override
+    public Uri remapUri(Uri uri) {
+        // Remap only cdvfile: URLs (not content:).
+        if (!LocalFilesystemURL.FILESYSTEM_PROTOCOL.equals(uri.getScheme())) {
+            return null;
+        }
+        try {
+        	LocalFilesystemURL inputURL = LocalFilesystemURL.parse(uri);
+        	Filesystem fs = this.filesystemForURL(inputURL);
+        	if (fs == null) {
+        		return null;
+        	}
+        	String path = fs.filesystemPathForURL(inputURL);
+        	if (path != null) {
+        		return Uri.parse("file://" + fs.filesystemPathForURL(inputURL));
+        	}
+        	return null;
+        } catch (IllegalArgumentException e) {
+        	return null;
+        }
+    }
+
+    public boolean execute(String action, final String rawArgs, final CallbackContext callbackContext) {
+        if (!configured) {
+            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, "File plugin is not configured. Please see the README.md file for details on how to update config.xml"));
+            return true;
+        }
+        if (action.equals("testSaveLocationExists")) {
+            threadhelper(new FileOp() {
+                public void run(JSONArray args) {
+                    boolean b = DirectoryManager.testSaveLocationExists();
+                    callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("getFreeDiskSpace")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) {
+                    // The getFreeDiskSpace plugin API is not documented, but some apps call it anyway via exec().
+                    // For compatibility it always returns free space in the primary external storage, and
+                    // does NOT fallback to internal store if external storage is unavailable.
+                    long l = DirectoryManager.getFreeExternalStorageSpace();
+                    callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, l));
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("testFileExists")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException {
+                    String fname=args.getString(0);
+                    boolean b = DirectoryManager.testFileExists(fname);
+                    callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("testDirectoryExists")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException {
+                    String fname=args.getString(0);
+                    boolean b = DirectoryManager.testFileExists(fname);
+                    callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("readAsText")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException, MalformedURLException {
+                    String encoding = args.getString(1);
+                    int start = args.getInt(2);
+                    int end = args.getInt(3);
+                    String fname=args.getString(0);
+                    readFileAs(fname, start, end, callbackContext, encoding, PluginResult.MESSAGE_TYPE_STRING);
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("readAsDataURL")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException, MalformedURLException  {
+                    int start = args.getInt(1);
+                    int end = args.getInt(2);
+                    String fname=args.getString(0);
+                    readFileAs(fname, start, end, callbackContext, null, -1);
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("readAsArrayBuffer")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException, MalformedURLException  {
+                    int start = args.getInt(1);
+                    int end = args.getInt(2);
+                    String fname=args.getString(0);
+                    readFileAs(fname, start, end, callbackContext, null, PluginResult.MESSAGE_TYPE_ARRAYBUFFER);
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("readAsBinaryString")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException, MalformedURLException  {
+                    int start = args.getInt(1);
+                    int end = args.getInt(2);
+                    String fname=args.getString(0);
+                    readFileAs(fname, start, end, callbackContext, null, PluginResult.MESSAGE_TYPE_BINARYSTRING);
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("write")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException, FileNotFoundException, IOException, NoModificationAllowedException {
+                    String fname=args.getString(0);
+                    String nativeURL = resolveLocalFileSystemURI(fname).getString("nativeURL");
+                    String data=args.getString(1);
+                    int offset=args.getInt(2);
+                    Boolean isBinary=args.getBoolean(3);
+
+                    if(needPermission(nativeURL, WRITE)) {
+                        getWritePermission(rawArgs, ACTION_WRITE, callbackContext);
+                    }
+                    else {
+                        long fileSize = write(fname, data, offset, isBinary);
+                        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
+                    }
+
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("truncate")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException, FileNotFoundException, IOException, NoModificationAllowedException {
+                    String fname=args.getString(0);
+                    int offset=args.getInt(1);
+                    long fileSize = truncateFile(fname, offset);
+                    callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("requestAllFileSystems")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws IOException, JSONException {
+                    callbackContext.success(requestAllFileSystems());
+                }
+            }, rawArgs, callbackContext);
+        } else if (action.equals("requestAllPaths")) {
+            cordova.getThreadPool().execute(
+                    new Runnable() {
+                        public void run() {
+                        	try {
+					callbackContext.success(requestAllPaths());
+				} catch (JSONException e) {
+					// TODO Auto-generated catch block
+					e.printStackTrace();
+				}
+                        }
+                    }
+            );
+        } else if (action.equals("requestFileSystem")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException {
+                    int fstype = args.getInt(0);
+                    long requiredSize = args.optLong(1);
+                    requestFileSystem(fstype, requiredSize, callbackContext);
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("resolveLocalFileSystemURI")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws IOException, JSONException {
+                    String fname=args.getString(0);
+                    JSONObject obj = resolveLocalFileSystemURI(fname);
+                    callbackContext.success(obj);
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("getFileMetadata")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws FileNotFoundException, JSONException, MalformedURLException {
+                    String fname=args.getString(0);
+                    JSONObject obj = getFileMetadata(fname);
+                    callbackContext.success(obj);
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("getParent")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException, IOException {
+                    String fname=args.getString(0);
+                    JSONObject obj = getParent(fname);
+                    callbackContext.success(obj);
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("getDirectory")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+                    String dirname = args.getString(0);
+                    String path = args.getString(1);
+                    String nativeURL = resolveLocalFileSystemURI(dirname).getString("nativeURL");
+                    boolean containsCreate = (args.isNull(2)) ? false : args.getJSONObject(2).optBoolean("create", false);
+
+                    if(containsCreate && needPermission(nativeURL, WRITE)) {
+                        getWritePermission(rawArgs, ACTION_GET_DIRECTORY, callbackContext);
+                    }
+                    else if(!containsCreate && needPermission(nativeURL, READ)) {
+                        getReadPermission(rawArgs, ACTION_GET_DIRECTORY, callbackContext);
+                    }
+                    else {
+                        JSONObject obj = getFile(dirname, path, args.optJSONObject(2), true);
+                        callbackContext.success(obj);
+                    }
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("getFile")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+                    String dirname = args.getString(0);
+                    String path = args.getString(1);
+                    String nativeURL = resolveLocalFileSystemURI(dirname).getString("nativeURL");
+                    boolean containsCreate = (args.isNull(2)) ? false : args.getJSONObject(2).optBoolean("create", false);
+
+                    if(containsCreate && needPermission(nativeURL, WRITE)) {
+                        getWritePermission(rawArgs, ACTION_GET_FILE, callbackContext);
+                    }
+                    else if(!containsCreate && needPermission(nativeURL, READ)) {
+                        getReadPermission(rawArgs, ACTION_GET_FILE, callbackContext);
+                    }
+                    else {
+                        JSONObject obj = getFile(dirname, path, args.optJSONObject(2), false);
+                        callbackContext.success(obj);
+                    }
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("remove")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException, NoModificationAllowedException, InvalidModificationException, MalformedURLException {
+                    String fname=args.getString(0);
+                    boolean success = remove(fname);
+                    if (success) {
+                        callbackContext.success();
+                    } else {
+                        callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+                    }
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("removeRecursively")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException, FileExistsException, MalformedURLException, NoModificationAllowedException {
+                    String fname=args.getString(0);
+                    boolean success = removeRecursively(fname);
+                    if (success) {
+                        callbackContext.success();
+                    } else {
+                        callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+                    }
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("moveTo")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException {
+                    String fname=args.getString(0);
+                    String newParent=args.getString(1);
+                    String newName=args.getString(2);
+                    JSONObject entry = transferTo(fname, newParent, newName, true);
+                    callbackContext.success(entry);
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("copyTo")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException {
+                    String fname=args.getString(0);
+                    String newParent=args.getString(1);
+                    String newName=args.getString(2);
+                    JSONObject entry = transferTo(fname, newParent, newName, false);
+                    callbackContext.success(entry);
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("readEntries")) {
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws FileNotFoundException, JSONException, MalformedURLException {
+                    String fname=args.getString(0);
+                    JSONArray entries = readEntries(fname);
+                    callbackContext.success(entries);
+                }
+            }, rawArgs, callbackContext);
+        }
+        else if (action.equals("_getLocalFilesystemPath")) {
+            // Internal method for testing: Get the on-disk location of a local filesystem url.
+            // [Currently used for testing file-transfer]
+            threadhelper( new FileOp( ){
+                public void run(JSONArray args) throws FileNotFoundException, JSONException, MalformedURLException {
+                    String localURLstr = args.getString(0);
+                    String fname = filesystemPathForURL(localURLstr);
+                    callbackContext.success(fname);
+                }
+            }, rawArgs, callbackContext);
+        }
+        else {
+            return false;
+        }
+        return true;
+    }
+
+    private void getReadPermission(String rawArgs, int action, CallbackContext callbackContext) {
+        int requestCode = pendingRequests.createRequest(rawArgs, action, callbackContext);
+        PermissionHelper.requestPermission(this, requestCode, Manifest.permission.READ_EXTERNAL_STORAGE);
+    }
+
+    private void getWritePermission(String rawArgs, int action, CallbackContext callbackContext) {
+        int requestCode = pendingRequests.createRequest(rawArgs, action, callbackContext);
+        PermissionHelper.requestPermission(this, requestCode, Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    }
+
+    private boolean hasReadPermission() {
+        return PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
+    }
+
+    private boolean hasWritePermission() {
+        return PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    }
+
+    private boolean needPermission(String nativeURL, int permissionType) throws JSONException {
+        JSONObject j = requestAllPaths();
+        ArrayList<String> allowedStorageDirectories = new ArrayList<String>();
+        allowedStorageDirectories.add(j.getString("applicationDirectory"));
+        allowedStorageDirectories.add(j.getString("applicationStorageDirectory"));
+        if(j.has("externalApplicationStorageDirectory")) {
+            allowedStorageDirectories.add(j.getString("externalApplicationStorageDirectory"));
+        }
+
+        if(permissionType == READ && hasReadPermission()) {
+            return false;
+        }
+        else if(permissionType == WRITE && hasWritePermission()) {
+            return false;
+        }
+
+        // Permission required if the native url lies outside the allowed storage directories
+        for(String directory : allowedStorageDirectories) {
+            if(nativeURL.startsWith(directory)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    public LocalFilesystemURL resolveNativeUri(Uri nativeUri) {
+        LocalFilesystemURL localURL = null;
+
+        // Try all installed filesystems. Return the best matching URL
+        // (determined by the shortest resulting URL)
+        for (Filesystem fs : filesystems) {
+            LocalFilesystemURL url = fs.toLocalUri(nativeUri);
+            if (url != null) {
+                // A shorter fullPath implies that the filesystem is a better
+                // match for the local path than the previous best.
+                if (localURL == null || (url.uri.toString().length() < localURL.toString().length())) {
+                    localURL = url;
+                }
+            }
+        }
+        return localURL;
+    }
+
+    /*
+     * These two native-only methods can be used by other plugins to translate between
+     * device file system paths and URLs. By design, there is no direct JavaScript
+     * interface to these methods.
+     */
+
+    public String filesystemPathForURL(String localURLstr) throws MalformedURLException {
+        try {
+            LocalFilesystemURL inputURL = LocalFilesystemURL.parse(localURLstr);
+            Filesystem fs = this.filesystemForURL(inputURL);
+            if (fs == null) {
+                throw new MalformedURLException("No installed handlers for this URL");
+            }
+            return fs.filesystemPathForURL(inputURL);
+        } catch (IllegalArgumentException e) {
+            MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+            mue.initCause(e);
+        	throw mue;
+        }
+    }
+
+    public LocalFilesystemURL filesystemURLforLocalPath(String localPath) {
+        LocalFilesystemURL localURL = null;
+        int shortestFullPath = 0;
+
+        // Try all installed filesystems. Return the best matching URL
+        // (determined by the shortest resulting URL)
+        for (Filesystem fs: filesystems) {
+            LocalFilesystemURL url = fs.URLforFilesystemPath(localPath);
+            if (url != null) {
+                // A shorter fullPath implies that the filesystem is a better
+                // match for the local path than the previous best.
+                if (localURL == null || (url.path.length() < shortestFullPath)) {
+                    localURL = url;
+                    shortestFullPath = url.path.length();
+                }
+            }
+        }
+        return localURL;
+    }
+
+
+	/* helper to execute functions async and handle the result codes
+     *
+     */
+    private void threadhelper(final FileOp f, final String rawArgs, final CallbackContext callbackContext){
+        cordova.getThreadPool().execute(new Runnable() {
+            public void run() {
+                try {
+                    JSONArray args = new JSONArray(rawArgs);
+                    f.run(args);
+                } catch ( Exception e) {
+                    if( e instanceof EncodingException){
+                        callbackContext.error(FileUtils.ENCODING_ERR);
+                    } else if(e instanceof FileNotFoundException) {
+                        callbackContext.error(FileUtils.NOT_FOUND_ERR);
+                    } else if(e instanceof FileExistsException) {
+                        callbackContext.error(FileUtils.PATH_EXISTS_ERR);
+                    } else if(e instanceof NoModificationAllowedException ) {
+                        callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+                    } else if(e instanceof InvalidModificationException ) {
+                        callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR);
+                    } else if(e instanceof MalformedURLException ) {
+                        callbackContext.error(FileUtils.ENCODING_ERR);
+                    } else if(e instanceof IOException ) {
+                        callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR);
+                    } else if(e instanceof EncodingException ) {
+                        callbackContext.error(FileUtils.ENCODING_ERR);
+                    } else if(e instanceof TypeMismatchException ) {
+                        callbackContext.error(FileUtils.TYPE_MISMATCH_ERR);
+                    } else if(e instanceof JSONException ) {
+                        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
+                    } else if (e instanceof SecurityException) {
+                        callbackContext.error(FileUtils.SECURITY_ERR);
+                    } else {
+                        e.printStackTrace();
+                    	callbackContext.error(FileUtils.UNKNOWN_ERR);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Allows the user to look up the Entry for a file or directory referred to by a local URI.
+     *
+     * @param uriString of the file/directory to look up
+     * @return a JSONObject representing a Entry from the filesystem
+     * @throws MalformedURLException if the url is not valid
+     * @throws FileNotFoundException if the file does not exist
+     * @throws IOException if the user can't read the file
+     * @throws JSONException
+     */
+    private JSONObject resolveLocalFileSystemURI(String uriString) throws IOException, JSONException {
+        if (uriString == null) {
+            throw new MalformedURLException("Unrecognized filesystem URL");
+        }
+        Uri uri = Uri.parse(uriString);
+        boolean isNativeUri = false;
+
+        LocalFilesystemURL inputURL = LocalFilesystemURL.parse(uri);
+        if (inputURL == null) {
+            /* Check for file://, content:// urls */
+            inputURL = resolveNativeUri(uri);
+            isNativeUri = true;
+        }
+
+        try {
+            Filesystem fs = this.filesystemForURL(inputURL);
+            if (fs == null) {
+                throw new MalformedURLException("No installed handlers for this URL");
+            }
+            if (fs.exists(inputURL)) {
+                if (!isNativeUri) {
+                    // If not already resolved as native URI, resolve to a native URI and back to
+                    // fix the terminating slash based on whether the entry is a directory or file.
+                    inputURL = fs.toLocalUri(fs.toNativeUri(inputURL));
+                }
+
+                return fs.getEntryForLocalURL(inputURL);
+            }
+        } catch (IllegalArgumentException e) {
+            MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+            mue.initCause(e);
+        	throw mue;
+        }
+        throw new FileNotFoundException();
+    }
+
+    /**
+     * Read the list of files from this directory.
+     *
+     * @return a JSONArray containing JSONObjects that represent Entry objects.
+     * @throws FileNotFoundException if the directory is not found.
+     * @throws JSONException
+     * @throws MalformedURLException
+     */
+    private JSONArray readEntries(String baseURLstr) throws FileNotFoundException, JSONException, MalformedURLException {
+        try {
+        	LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
+        	Filesystem fs = this.filesystemForURL(inputURL);
+        	if (fs == null) {
+        		throw new MalformedURLException("No installed handlers for this URL");
+        	}
+        	return fs.readEntriesAtLocalURL(inputURL);
+
+        } catch (IllegalArgumentException e) {
+            MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+            mue.initCause(e);
+        	throw mue;
+        }
+    }
+
+    /**
+     * A setup method that handles the move/copy of files/directories
+     *
+     * @param newName for the file directory to be called, if null use existing file name
+     * @param move if false do a copy, if true do a move
+     * @return a Entry object
+     * @throws NoModificationAllowedException
+     * @throws IOException
+     * @throws InvalidModificationException
+     * @throws EncodingException
+     * @throws JSONException
+     * @throws FileExistsException
+     */
+    private JSONObject transferTo(String srcURLstr, String destURLstr, String newName, boolean move) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException {
+        if (srcURLstr == null || destURLstr == null) {
+            // either no source or no destination provided
+        	throw new FileNotFoundException();
+        }
+
+        LocalFilesystemURL srcURL = LocalFilesystemURL.parse(srcURLstr);
+        LocalFilesystemURL destURL = LocalFilesystemURL.parse(destURLstr);
+
+        Filesystem srcFs = this.filesystemForURL(srcURL);
+        Filesystem destFs = this.filesystemForURL(destURL);
+
+        // Check for invalid file name
+        if (newName != null && newName.contains(":")) {
+            throw new EncodingException("Bad file name");
+        }
+
+        return destFs.copyFileToURL(destURL, newName, srcFs, srcURL, move);
+    }
+
+    /**
+     * Deletes a directory and all of its contents, if any. In the event of an error
+     * [e.g. trying to delete a directory that contains a file that cannot be removed],
+     * some of the contents of the directory may be deleted.
+     * It is an error to attempt to delete the root directory of a filesystem.
+     *
+     * @return a boolean representing success of failure
+     * @throws FileExistsException
+     * @throws NoModificationAllowedException
+     * @throws MalformedURLException
+     */
+    private boolean removeRecursively(String baseURLstr) throws FileExistsException, NoModificationAllowedException, MalformedURLException {
+        try {
+        	LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
+        	// You can't delete the root directory.
+        	if ("".equals(inputURL.path) || "/".equals(inputURL.path)) {
+        		throw new NoModificationAllowedException("You can't delete the root directory");
+        	}
+
+        	Filesystem fs = this.filesystemForURL(inputURL);
+        	if (fs == null) {
+        		throw new MalformedURLException("No installed handlers for this URL");
+        	}
+        	return fs.recursiveRemoveFileAtLocalURL(inputURL);
+
+        } catch (IllegalArgumentException e) {
+            MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+            mue.initCause(e);
+        	throw mue;
+        }
+    }
+
+
+    /**
+     * Deletes 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 the root directory of a filesystem.
+     *
+     * @return a boolean representing success of failure
+     * @throws NoModificationAllowedException
+     * @throws InvalidModificationException
+     * @throws MalformedURLException
+     */
+    private boolean remove(String baseURLstr) throws NoModificationAllowedException, InvalidModificationException, MalformedURLException {
+        try {
+        	LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
+        	// You can't delete the root directory.
+        	if ("".equals(inputURL.path) || "/".equals(inputURL.path)) {
+
+        		throw new NoModificationAllowedException("You can't delete the root directory");
+        	}
+
+        	Filesystem fs = this.filesystemForURL(inputURL);
+        	if (fs == null) {
+        		throw new MalformedURLException("No installed handlers for this URL");
+        	}
+        	return fs.removeFileAtLocalURL(inputURL);
+
+        } catch (IllegalArgumentException e) {
+            MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+            mue.initCause(e);
+        	throw mue;
+        }
+    }
+
+    /**
+     * Creates or looks up a file.
+     *
+     * @param baseURLstr base directory
+     * @param path file/directory to lookup or create
+     * @param options specify whether to create or not
+     * @param directory if true look up directory, if false look up file
+     * @return a Entry object
+     * @throws FileExistsException
+     * @throws IOException
+     * @throws TypeMismatchException
+     * @throws EncodingException
+     * @throws JSONException
+     */
+    private JSONObject getFile(String baseURLstr, String path, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+        try {
+        	LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
+        	Filesystem fs = this.filesystemForURL(inputURL);
+        	if (fs == null) {
+        		throw new MalformedURLException("No installed handlers for this URL");
+        	}
+        	return fs.getFileForLocalURL(inputURL, path, options, directory);
+
+        } catch (IllegalArgumentException e) {
+            MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+            mue.initCause(e);
+        	throw mue;
+        }
+
+    }
+
+    /**
+     * Look up the parent DirectoryEntry containing this Entry.
+     * If this Entry is the root of its filesystem, its parent is itself.
+     */
+    private JSONObject getParent(String baseURLstr) throws JSONException, IOException {
+        try {
+        	LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
+        	Filesystem fs = this.filesystemForURL(inputURL);
+        	if (fs == null) {
+        		throw new MalformedURLException("No installed handlers for this URL");
+        	}
+        	return fs.getParentForLocalURL(inputURL);
+
+        } catch (IllegalArgumentException e) {
+            MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+            mue.initCause(e);
+        	throw mue;
+        }
+    }
+
+    /**
+     * Returns a File that represents the current state of the file that this FileEntry represents.
+     *
+     * @return returns a JSONObject represent a W3C File object
+     */
+    private JSONObject getFileMetadata(String baseURLstr) throws FileNotFoundException, JSONException, MalformedURLException {
+        try {
+        	LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
+        	Filesystem fs = this.filesystemForURL(inputURL);
+        	if (fs == null) {
+        		throw new MalformedURLException("No installed handlers for this URL");
+        	}
+        	return fs.getFileMetadataForLocalURL(inputURL);
+
+        } catch (IllegalArgumentException e) {
+            MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+            mue.initCause(e);
+        	throw mue;
+        }
+    }
+
+    /**
+     * Requests a filesystem in which to store application data.
+     *
+     * @param type of file system requested
+     * @param requiredSize required free space in the file system in bytes
+     * @param callbackContext context for returning the result or error
+     * @throws JSONException
+     */
+    private void requestFileSystem(int type, long requiredSize, final CallbackContext callbackContext) throws JSONException {
+        Filesystem rootFs = null;
+        try {
+            rootFs = this.filesystems.get(type);
+        } catch (ArrayIndexOutOfBoundsException e) {
+            // Pass null through
+        }
+        if (rootFs == null) {
+            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_FOUND_ERR));
+        } else {
+            // If a nonzero required size was specified, check that the retrieved filesystem has enough free space.
+            long availableSize = 0;
+            if (requiredSize > 0) {
+                availableSize = rootFs.getFreeSpaceInBytes();
+            }
+
+            if (availableSize < requiredSize) {
+                callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FileUtils.QUOTA_EXCEEDED_ERR));
+            } else {
+                JSONObject fs = new JSONObject();
+                fs.put("name", rootFs.name);
+                fs.put("root", rootFs.getRootEntry());
+                callbackContext.success(fs);
+            }
+        }
+    }
+
+    /**
+     * Requests a filesystem in which to store application data.
+     *
+     * @return a JSONObject representing the file system
+     */
+    private JSONArray requestAllFileSystems() throws IOException, JSONException {
+        JSONArray ret = new JSONArray();
+        for (Filesystem fs : filesystems) {
+            ret.put(fs.getRootEntry());
+        }
+        return ret;
+    }
+
+    private static String toDirUrl(File f) {
+        return Uri.fromFile(f).toString() + '/';
+    }
+
+    private JSONObject requestAllPaths() throws JSONException {
+        Context context = cordova.getActivity();
+        JSONObject ret = new JSONObject();
+        ret.put("applicationDirectory", "file:///android_asset/");
+        ret.put("applicationStorageDirectory", toDirUrl(context.getFilesDir().getParentFile()));
+        ret.put("dataDirectory", toDirUrl(context.getFilesDir()));
+        ret.put("cacheDirectory", toDirUrl(context.getCacheDir()));
+        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+          try {
+            ret.put("externalApplicationStorageDirectory", toDirUrl(context.getExternalFilesDir(null).getParentFile()));
+            ret.put("externalDataDirectory", toDirUrl(context.getExternalFilesDir(null)));
+            ret.put("externalCacheDirectory", toDirUrl(context.getExternalCacheDir()));
+            ret.put("externalRootDirectory", toDirUrl(Environment.getExternalStorageDirectory()));
+          }
+          catch(NullPointerException e) {
+            /* If external storage is unavailable, context.getExternal* returns null */
+              LOG.d(LOG_TAG, "Unable to access these paths, most liklely due to USB storage");
+          }
+        }
+        return ret;
+    }
+
+   /**
+     * Returns a JSON object representing the given File. Internal APIs should be modified
+     * to use URLs instead of raw FS paths wherever possible, when interfacing with this plugin.
+     *
+     * @param file the File to convert
+     * @return a JSON representation of the given File
+     * @throws JSONException
+     */
+    public JSONObject getEntryForFile(File file) throws JSONException {
+        JSONObject entry;
+
+        for (Filesystem fs : filesystems) {
+             entry = fs.makeEntryForFile(file);
+             if (entry != null) {
+                 return entry;
+             }
+        }
+        return null;
+    }
+
+    /**
+     * Returns a JSON object representing the given File. Deprecated, as this is only used by
+     * FileTransfer, and because it is a static method that should really be an instance method,
+     * since it depends on the actual filesystem roots in use. Internal APIs should be modified
+     * to use URLs instead of raw FS paths wherever possible, when interfacing with this plugin.
+     *
+     * @param file the File to convert
+     * @return a JSON representation of the given File
+     * @throws JSONException
+     */
+    @Deprecated
+    public static JSONObject getEntry(File file) throws JSONException {
+ 		if (getFilePlugin() != null) {
+             return getFilePlugin().getEntryForFile(file);
+		}
+		return null;
+    }
+
+    /**
+     * Read the contents of a file.
+     * This is done in a background thread; the result is sent to the callback.
+     *
+     * @param start             Start position in the file.
+     * @param end               End position to stop at (exclusive).
+     * @param callbackContext   The context through which to send the result.
+     * @param encoding          The encoding to return contents as.  Typical value is UTF-8. (see http://www.iana.org/assignments/character-sets)
+     * @param resultType        The desired type of data to send to the callback.
+     * @return                  Contents of file.
+     */
+    public void readFileAs(final String srcURLstr, final int start, final int end, final CallbackContext callbackContext, final String encoding, final int resultType) throws MalformedURLException {
+        try {
+        	LocalFilesystemURL inputURL = LocalFilesystemURL.parse(srcURLstr);
+        	Filesystem fs = this.filesystemForURL(inputURL);
+        	if (fs == null) {
+        		throw new MalformedURLException("No installed handlers for this URL");
+        	}
+
+            fs.readFileAtURL(inputURL, start, end, new Filesystem.ReadFileCallback() {
+                public void handleData(InputStream inputStream, String contentType) {
+            		try {
+                        ByteArrayOutputStream os = new ByteArrayOutputStream();
+                        final int BUFFER_SIZE = 8192;
+                        byte[] buffer = new byte[BUFFER_SIZE];
+
+                        for (;;) {
+                            int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE);
+
+                            if (bytesRead <= 0) {
+                                break;
+                            }
+                            os.write(buffer, 0, bytesRead);
+                        }
+
+            			PluginResult result;
+            			switch (resultType) {
+            			case PluginResult.MESSAGE_TYPE_STRING:
+                            result = new PluginResult(PluginResult.Status.OK, os.toString(encoding));
+            				break;
+            			case PluginResult.MESSAGE_TYPE_ARRAYBUFFER:
+                            result = new PluginResult(PluginResult.Status.OK, os.toByteArray());
+            				break;
+            			case PluginResult.MESSAGE_TYPE_BINARYSTRING:
+                            result = new PluginResult(PluginResult.Status.OK, os.toByteArray(), true);
+            				break;
+            			default: // Base64.
+                        byte[] base64 = Base64.encode(os.toByteArray(), Base64.NO_WRAP);
+            			String s = "data:" + contentType + ";base64," + new String(base64, "US-ASCII");
+            			result = new PluginResult(PluginResult.Status.OK, s);
+            			}
+
+            			callbackContext.sendPluginResult(result);
+            		} catch (IOException e) {
+            			LOG.d(LOG_TAG, e.getLocalizedMessage());
+            			callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_READABLE_ERR));
+                    }
+            	}
+            });
+
+
+        } catch (IllegalArgumentException e) {
+            MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+            mue.initCause(e);
+        	throw mue;
+        } catch (FileNotFoundException e) {
+        	callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_FOUND_ERR));
+        } catch (IOException e) {
+        	LOG.d(LOG_TAG, e.getLocalizedMessage());
+        	callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_READABLE_ERR));
+        }
+    }
+
+
+    /**
+     * Write contents of file.
+     *
+     * @param data				The contents of the file.
+     * @param offset			The position to begin writing the file.
+     * @param isBinary          True if the file contents are base64-encoded binary data
+     */
+    /**/
+    public long write(String srcURLstr, String data, int offset, boolean isBinary) throws FileNotFoundException, IOException, NoModificationAllowedException {
+        try {
+        	LocalFilesystemURL inputURL = LocalFilesystemURL.parse(srcURLstr);
+        	Filesystem fs = this.filesystemForURL(inputURL);
+        	if (fs == null) {
+        		throw new MalformedURLException("No installed handlers for this URL");
+        	}
+
+            long x = fs.writeToFileAtURL(inputURL, data, offset, isBinary); LOG.d("TEST",srcURLstr + ": "+x); return x;
+        } catch (IllegalArgumentException e) {
+            MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+            mue.initCause(e);
+        	throw mue;
+        }
+
+    }
+
+    /**
+     * Truncate the file to size
+     */
+    private long truncateFile(String srcURLstr, long size) throws FileNotFoundException, IOException, NoModificationAllowedException {
+        try {
+        	LocalFilesystemURL inputURL = LocalFilesystemURL.parse(srcURLstr);
+        	Filesystem fs = this.filesystemForURL(inputURL);
+        	if (fs == null) {
+        		throw new MalformedURLException("No installed handlers for this URL");
+        	}
+
+            return fs.truncateFileAtURL(inputURL, size);
+        } catch (IllegalArgumentException e) {
+            MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+            mue.initCause(e);
+        	throw mue;
+        }
+    }
+
+
+    /*
+     * Handle the response
+     */
+
+    public void onRequestPermissionResult(int requestCode, String[] permissions,
+                                          int[] grantResults) throws JSONException {
+
+        final PendingRequests.Request req = pendingRequests.getAndRemove(requestCode);
+        if (req != null) {
+            for(int r:grantResults)
+            {
+                if(r == PackageManager.PERMISSION_DENIED)
+                {
+                    req.getCallbackContext().sendPluginResult(new PluginResult(PluginResult.Status.ERROR, SECURITY_ERR));
+                    return;
+                }
+            }
+            switch(req.getAction())
+            {
+                case ACTION_GET_FILE:
+                    threadhelper( new FileOp( ){
+                        public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+                            String dirname = args.getString(0);
+
+                            String path = args.getString(1);
+                            JSONObject obj = getFile(dirname, path, args.optJSONObject(2), false);
+                            req.getCallbackContext().success(obj);
+                        }
+                    }, req.getRawArgs(), req.getCallbackContext());
+                    break;
+                case ACTION_GET_DIRECTORY:
+                    threadhelper( new FileOp( ){
+                        public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+                            String dirname = args.getString(0);
+
+                            String path = args.getString(1);
+                            JSONObject obj = getFile(dirname, path, args.optJSONObject(2), true);
+                            req.getCallbackContext().success(obj);
+                        }
+                    }, req.getRawArgs(), req.getCallbackContext());
+                    break;
+                case ACTION_WRITE:
+                    threadhelper( new FileOp( ){
+                        public void run(JSONArray args) throws JSONException, FileNotFoundException, IOException, NoModificationAllowedException {
+                            String fname=args.getString(0);
+                            String data=args.getString(1);
+                            int offset=args.getInt(2);
+                            Boolean isBinary=args.getBoolean(3);
+                            long fileSize = write(fname, data, offset, isBinary);
+                            req.getCallbackContext().sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
+                        }
+                    }, req.getRawArgs(), req.getCallbackContext());
+                    break;
+            }
+        } else {
+           LOG.d(LOG_TAG, "Received permission callback for unknown request code");
+        }
+    }
+}

+ 331 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/Filesystem.java

@@ -0,0 +1,331 @@
+/*
+       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.file;
+
+import android.net.Uri;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import org.apache.cordova.CordovaResourceApi;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public abstract class Filesystem {
+
+    protected final Uri rootUri;
+    protected final CordovaResourceApi resourceApi;
+    public final String name;
+    private JSONObject rootEntry;
+
+    public Filesystem(Uri rootUri, String name, CordovaResourceApi resourceApi) {
+        this.rootUri = rootUri;
+        this.name = name;
+        this.resourceApi = resourceApi;
+    }
+
+    public interface ReadFileCallback {
+		public void handleData(InputStream inputStream, String contentType) throws IOException;
+	}
+
+    public static JSONObject makeEntryForURL(LocalFilesystemURL inputURL, Uri nativeURL) {
+        try {
+            String path = inputURL.path;
+            int end = path.endsWith("/") ? 1 : 0;
+            String[] parts = path.substring(0, path.length() - end).split("/+");
+            String fileName = parts[parts.length - 1];
+
+            JSONObject entry = new JSONObject();
+            entry.put("isFile", !inputURL.isDirectory);
+            entry.put("isDirectory", inputURL.isDirectory);
+            entry.put("name", fileName);
+            entry.put("fullPath", path);
+            // The file system can't be specified, as it would lead to an infinite loop,
+            // but the filesystem name can be.
+            entry.put("filesystemName", inputURL.fsName);
+            // Backwards compatibility
+            entry.put("filesystem", "temporary".equals(inputURL.fsName) ? 0 : 1);
+
+            String nativeUrlStr = nativeURL.toString();
+            if (inputURL.isDirectory && !nativeUrlStr.endsWith("/")) {
+                nativeUrlStr += "/";
+            }
+            entry.put("nativeURL", nativeUrlStr);
+            return entry;
+        } catch (JSONException e) {
+            e.printStackTrace();
+            throw new RuntimeException(e);
+        }
+    }
+
+    public JSONObject makeEntryForURL(LocalFilesystemURL inputURL) {
+        Uri nativeUri = toNativeUri(inputURL);
+        return nativeUri == null ? null : makeEntryForURL(inputURL, nativeUri);
+    }
+
+    public JSONObject makeEntryForNativeUri(Uri nativeUri) {
+        LocalFilesystemURL inputUrl = toLocalUri(nativeUri);
+        return inputUrl == null ? null : makeEntryForURL(inputUrl, nativeUri);
+    }
+
+    public JSONObject getEntryForLocalURL(LocalFilesystemURL inputURL) throws IOException {
+        return makeEntryForURL(inputURL);
+    }
+
+    public JSONObject makeEntryForFile(File file) {
+        return makeEntryForNativeUri(Uri.fromFile(file));
+    }
+
+    abstract JSONObject getFileForLocalURL(LocalFilesystemURL inputURL, String path,
+			JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException;
+
+	abstract boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException, NoModificationAllowedException;
+
+	abstract boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws FileExistsException, NoModificationAllowedException;
+
+	abstract LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException;
+
+    public final JSONArray readEntriesAtLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
+        LocalFilesystemURL[] children = listChildren(inputURL);
+        JSONArray entries = new JSONArray();
+        if (children != null) {
+            for (LocalFilesystemURL url : children) {
+                entries.put(makeEntryForURL(url));
+            }
+        }
+        return entries;
+    }
+
+	abstract JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException;
+
+    public Uri getRootUri() {
+        return rootUri;
+    }
+
+    public boolean exists(LocalFilesystemURL inputURL) {
+        try {
+            getFileMetadataForLocalURL(inputURL);
+        } catch (FileNotFoundException e) {
+            return false;
+        }
+        return true;
+    }
+
+    public Uri nativeUriForFullPath(String fullPath) {
+        Uri ret = null;
+        if (fullPath != null) {
+            String encodedPath = Uri.fromFile(new File(fullPath)).getEncodedPath();
+            if (encodedPath.startsWith("/")) {
+                encodedPath = encodedPath.substring(1);
+            }
+            ret = rootUri.buildUpon().appendEncodedPath(encodedPath).build();
+        }
+        return ret;
+    }
+
+    public LocalFilesystemURL localUrlforFullPath(String fullPath) {
+        Uri nativeUri = nativeUriForFullPath(fullPath);
+        if (nativeUri != null) {
+            return toLocalUri(nativeUri);
+        }
+        return null;
+    }
+
+    /**
+     * Removes multiple repeated //s, and collapses processes ../s.
+     */
+    protected static String normalizePath(String rawPath) {
+        // If this is an absolute path, trim the leading "/" and replace it later
+        boolean isAbsolutePath = rawPath.startsWith("/");
+        if (isAbsolutePath) {
+            rawPath = rawPath.replaceFirst("/+", "");
+        }
+        ArrayList<String> components = new ArrayList<String>(Arrays.asList(rawPath.split("/+")));
+        for (int index = 0; index < components.size(); ++index) {
+            if (components.get(index).equals("..")) {
+                components.remove(index);
+                if (index > 0) {
+                    components.remove(index-1);
+                    --index;
+                }
+            }
+        }
+        StringBuilder normalizedPath = new StringBuilder();
+        for(String component: components) {
+            normalizedPath.append("/");
+            normalizedPath.append(component);
+        }
+        if (isAbsolutePath) {
+            return normalizedPath.toString();
+        } else {
+            return normalizedPath.toString().substring(1);
+        }
+    }
+
+    /**
+     * Gets the free space in bytes available on this filesystem.
+     * Subclasses may override this method to return nonzero free space.
+     */
+    public long getFreeSpaceInBytes() {
+        return 0;
+    }
+
+    public abstract Uri toNativeUri(LocalFilesystemURL inputURL);
+    public abstract LocalFilesystemURL toLocalUri(Uri inputURL);
+
+    public JSONObject getRootEntry() {
+        if (rootEntry == null) {
+            rootEntry = makeEntryForNativeUri(rootUri);
+        }
+        return rootEntry;
+    }
+
+	public JSONObject getParentForLocalURL(LocalFilesystemURL inputURL) throws IOException {
+        Uri parentUri = inputURL.uri;
+        String parentPath = new File(inputURL.uri.getPath()).getParent();
+        if (!"/".equals(parentPath)) {
+            parentUri = inputURL.uri.buildUpon().path(parentPath + '/').build();
+		}
+		return getEntryForLocalURL(LocalFilesystemURL.parse(parentUri));
+	}
+
+    protected LocalFilesystemURL makeDestinationURL(String newName, LocalFilesystemURL srcURL, LocalFilesystemURL destURL, boolean isDirectory) {
+        // I know this looks weird but it is to work around a JSON bug.
+        if ("null".equals(newName) || "".equals(newName)) {
+            newName = srcURL.uri.getLastPathSegment();;
+        }
+
+        String newDest = destURL.uri.toString();
+        if (newDest.endsWith("/")) {
+            newDest = newDest + newName;
+        } else {
+            newDest = newDest + "/" + newName;
+        }
+        if (isDirectory) {
+            newDest += '/';
+        }
+        return LocalFilesystemURL.parse(newDest);
+    }
+
+	/* Read a source URL (possibly from a different filesystem, srcFs,) and copy it to
+	 * the destination URL on this filesystem, optionally with a new filename.
+	 * If move is true, then this method should either perform an atomic move operation
+	 * or remove the source file when finished.
+	 */
+    public JSONObject copyFileToURL(LocalFilesystemURL destURL, String newName,
+            Filesystem srcFs, LocalFilesystemURL srcURL, boolean move) throws IOException, InvalidModificationException, JSONException, NoModificationAllowedException, FileExistsException {
+        // First, check to see that we can do it
+        if (move && !srcFs.canRemoveFileAtLocalURL(srcURL)) {
+            throw new NoModificationAllowedException("Cannot move file at source URL");
+        }
+        final LocalFilesystemURL destination = makeDestinationURL(newName, srcURL, destURL, srcURL.isDirectory);
+
+        Uri srcNativeUri = srcFs.toNativeUri(srcURL);
+
+        CordovaResourceApi.OpenForReadResult ofrr = resourceApi.openForRead(srcNativeUri);
+        OutputStream os = null;
+        try {
+            os = getOutputStreamForURL(destination);
+        } catch (IOException e) {
+            ofrr.inputStream.close();
+            throw e;
+        }
+        // Closes streams.
+        resourceApi.copyResource(ofrr, os);
+
+        if (move) {
+            srcFs.removeFileAtLocalURL(srcURL);
+        }
+        return getEntryForLocalURL(destination);
+    }
+
+    public OutputStream getOutputStreamForURL(LocalFilesystemURL inputURL) throws IOException {
+        return resourceApi.openOutputStream(toNativeUri(inputURL));
+    }
+
+    public void readFileAtURL(LocalFilesystemURL inputURL, long start, long end,
+                              ReadFileCallback readFileCallback) throws IOException {
+        CordovaResourceApi.OpenForReadResult ofrr = resourceApi.openForRead(toNativeUri(inputURL));
+        if (end < 0) {
+            end = ofrr.length;
+        }
+        long numBytesToRead = end - start;
+        try {
+            if (start > 0) {
+                ofrr.inputStream.skip(start);
+            }
+            InputStream inputStream = ofrr.inputStream;
+            if (end < ofrr.length) {
+                inputStream = new LimitedInputStream(inputStream, numBytesToRead);
+            }
+            readFileCallback.handleData(inputStream, ofrr.mimeType);
+        } finally {
+            ofrr.inputStream.close();
+        }
+    }
+
+	abstract long writeToFileAtURL(LocalFilesystemURL inputURL, String data, int offset,
+			boolean isBinary) throws NoModificationAllowedException, IOException;
+
+	abstract long truncateFileAtURL(LocalFilesystemURL inputURL, long size)
+			throws IOException, NoModificationAllowedException;
+
+	// This method should return null if filesystem urls cannot be mapped to paths
+	abstract String filesystemPathForURL(LocalFilesystemURL url);
+
+	abstract LocalFilesystemURL URLforFilesystemPath(String path);
+
+	abstract boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL);
+
+    protected class LimitedInputStream extends FilterInputStream {
+        long numBytesToRead;
+        public LimitedInputStream(InputStream in, long numBytesToRead) {
+            super(in);
+            this.numBytesToRead = numBytesToRead;
+        }
+        @Override
+        public int read() throws IOException {
+            if (numBytesToRead <= 0) {
+                return -1;
+            }
+            numBytesToRead--;
+            return in.read();
+        }
+        @Override
+        public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
+            if (numBytesToRead <= 0) {
+                return -1;
+            }
+            int bytesToRead = byteCount;
+            if (byteCount > numBytesToRead) {
+                bytesToRead = (int)numBytesToRead; // Cast okay; long is less than int here.
+            }
+            int numBytesRead = in.read(buffer, byteOffset, bytesToRead);
+            numBytesToRead -= numBytesRead;
+            return numBytesRead;
+        }
+    }
+}

+ 30 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/InvalidModificationException.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.file;
+
+@SuppressWarnings("serial")
+public class InvalidModificationException extends Exception {
+
+    public InvalidModificationException(String message) {
+        super(message);
+    }
+
+}

+ 513 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/LocalFilesystem.java

@@ -0,0 +1,513 @@
+/*
+       Licensed to the Apache Software Foundation (ASF) under one
+       or more contributor license agreements.  See the NOTICE file
+       distributed with this work for additional information
+       regarding copyright ownership.  The ASF licenses this file
+       to you under the Apache License, Version 2.0 (the
+       "License"); you may not use this file except in compliance
+       with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing,
+       software distributed under the License is distributed on an
+       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+       KIND, either express or implied.  See the License for the
+       specific language governing permissions and limitations
+       under the License.
+ */
+package org.apache.cordova.file;
+
+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.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import org.apache.cordova.CordovaResourceApi;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.os.Build;
+import android.os.Environment;
+import android.util.Base64;
+import android.net.Uri;
+import android.content.Context;
+import android.content.Intent;
+
+import java.nio.charset.Charset;
+
+public class LocalFilesystem extends Filesystem {
+    private final Context context;
+
+    public LocalFilesystem(String name, Context context, CordovaResourceApi resourceApi, File fsRoot) {
+        super(Uri.fromFile(fsRoot).buildUpon().appendEncodedPath("").build(), name, resourceApi);
+        this.context = context;
+    }
+
+    public String filesystemPathForFullPath(String fullPath) {
+	    return new File(rootUri.getPath(), fullPath).toString();
+	}
+
+	@Override
+	public String filesystemPathForURL(LocalFilesystemURL url) {
+		return filesystemPathForFullPath(url.path);
+	}
+
+	private String fullPathForFilesystemPath(String absolutePath) {
+		if (absolutePath != null && absolutePath.startsWith(rootUri.getPath())) {
+			return absolutePath.substring(rootUri.getPath().length() - 1);
+		}
+		return null;
+	}
+
+    @Override
+    public Uri toNativeUri(LocalFilesystemURL inputURL) {
+        return nativeUriForFullPath(inputURL.path);
+    }
+
+    @Override
+    public LocalFilesystemURL toLocalUri(Uri inputURL) {
+        if (!"file".equals(inputURL.getScheme())) {
+            return null;
+        }
+        File f = new File(inputURL.getPath());
+        // Removes and duplicate /s (e.g. file:///a//b/c)
+        Uri resolvedUri = Uri.fromFile(f);
+        String rootUriNoTrailingSlash = rootUri.getEncodedPath();
+        rootUriNoTrailingSlash = rootUriNoTrailingSlash.substring(0, rootUriNoTrailingSlash.length() - 1);
+        if (!resolvedUri.getEncodedPath().startsWith(rootUriNoTrailingSlash)) {
+            return null;
+        }
+        String subPath = resolvedUri.getEncodedPath().substring(rootUriNoTrailingSlash.length());
+        // Strip leading slash
+        if (!subPath.isEmpty()) {
+            subPath = subPath.substring(1);
+        }
+        Uri.Builder b = new Uri.Builder()
+            .scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
+            .authority("localhost")
+            .path(name);
+        if (!subPath.isEmpty()) {
+            b.appendEncodedPath(subPath);
+        }
+        if (f.isDirectory()) {
+            // Add trailing / for directories.
+            b.appendEncodedPath("");
+        }
+        return LocalFilesystemURL.parse(b.build());
+    }
+
+	@Override
+	public LocalFilesystemURL URLforFilesystemPath(String path) {
+	    return localUrlforFullPath(fullPathForFilesystemPath(path));
+	}
+
+	@Override
+	public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL,
+			String path, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+        boolean create = false;
+        boolean exclusive = false;
+
+        if (options != null) {
+            create = options.optBoolean("create");
+            if (create) {
+                exclusive = options.optBoolean("exclusive");
+            }
+        }
+
+        // Check for a ":" character in the file to line up with BB and iOS
+        if (path.contains(":")) {
+            throw new EncodingException("This path has an invalid \":\" in it.");
+        }
+
+        LocalFilesystemURL requestedURL;
+
+        // Check whether the supplied path is absolute or relative
+        if (directory && !path.endsWith("/")) {
+            path += "/";
+        }
+        if (path.startsWith("/")) {
+        	requestedURL = localUrlforFullPath(normalizePath(path));
+        } else {
+        	requestedURL = localUrlforFullPath(normalizePath(inputURL.path + "/" + path));
+        }
+
+        File fp = new File(this.filesystemPathForURL(requestedURL));
+
+        if (create) {
+            if (exclusive && fp.exists()) {
+                throw new FileExistsException("create/exclusive fails");
+            }
+            if (directory) {
+                fp.mkdir();
+            } else {
+                fp.createNewFile();
+            }
+            if (!fp.exists()) {
+                throw new FileExistsException("create fails");
+            }
+        }
+        else {
+            if (!fp.exists()) {
+                throw new FileNotFoundException("path does not exist");
+            }
+            if (directory) {
+                if (fp.isFile()) {
+                    throw new TypeMismatchException("path doesn't exist or is file");
+                }
+            } else {
+                if (fp.isDirectory()) {
+                    throw new TypeMismatchException("path doesn't exist or is directory");
+                }
+            }
+        }
+
+        // Return the directory
+        return makeEntryForURL(requestedURL);
+	}
+
+	@Override
+	public boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException {
+
+        File fp = new File(filesystemPathForURL(inputURL));
+
+        // You can't delete a directory that is not empty
+        if (fp.isDirectory() && fp.list().length > 0) {
+            throw new InvalidModificationException("You can't delete a directory that is not empty.");
+        }
+
+        return fp.delete();
+	}
+
+    @Override
+    public boolean exists(LocalFilesystemURL inputURL) {
+        File fp = new File(filesystemPathForURL(inputURL));
+        return fp.exists();
+    }
+
+    @Override
+    public long getFreeSpaceInBytes() {
+        return DirectoryManager.getFreeSpaceInBytes(rootUri.getPath());
+    }
+
+    @Override
+	public boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws FileExistsException {
+        File directory = new File(filesystemPathForURL(inputURL));
+    	return removeDirRecursively(directory);
+	}
+
+	protected boolean removeDirRecursively(File directory) throws FileExistsException {
+        if (directory.isDirectory()) {
+            for (File file : directory.listFiles()) {
+                removeDirRecursively(file);
+            }
+        }
+
+        if (!directory.delete()) {
+            throw new FileExistsException("could not delete: " + directory.getName());
+        } else {
+            return true;
+        }
+	}
+
+    @Override
+    public LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException {
+        File fp = new File(filesystemPathForURL(inputURL));
+
+        if (!fp.exists()) {
+            // The directory we are listing doesn't exist so we should fail.
+            throw new FileNotFoundException();
+        }
+
+        File[] files = fp.listFiles();
+        if (files == null) {
+            // inputURL is a directory
+            return null;
+        }
+        LocalFilesystemURL[] entries = new LocalFilesystemURL[files.length];
+        for (int i = 0; i < files.length; i++) {
+            entries[i] = URLforFilesystemPath(files[i].getPath());
+        }
+
+        return entries;
+	}
+
+	@Override
+	public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
+        File file = new File(filesystemPathForURL(inputURL));
+
+        if (!file.exists()) {
+            throw new FileNotFoundException("File at " + inputURL.uri + " does not exist.");
+        }
+
+        JSONObject metadata = new JSONObject();
+        try {
+            // Ensure that directories report a size of 0
+        	metadata.put("size", file.isDirectory() ? 0 : file.length());
+        	metadata.put("type", resourceApi.getMimeType(Uri.fromFile(file)));
+        	metadata.put("name", file.getName());
+        	metadata.put("fullPath", inputURL.path);
+        	metadata.put("lastModifiedDate", file.lastModified());
+        } catch (JSONException e) {
+        	return null;
+        }
+        return metadata;
+	}
+
+    private void copyFile(Filesystem srcFs, LocalFilesystemURL srcURL, File destFile, boolean move) throws IOException, InvalidModificationException, NoModificationAllowedException {
+        if (move) {
+            String realSrcPath = srcFs.filesystemPathForURL(srcURL);
+            if (realSrcPath != null) {
+                File srcFile = new File(realSrcPath);
+                if (srcFile.renameTo(destFile)) {
+                    return;
+                }
+                // Trying to rename the file failed.  Possibly because we moved across file system on the device.
+            }
+        }
+
+        CordovaResourceApi.OpenForReadResult offr = resourceApi.openForRead(srcFs.toNativeUri(srcURL));
+        copyResource(offr, new FileOutputStream(destFile));
+
+        if (move) {
+            srcFs.removeFileAtLocalURL(srcURL);
+        }
+    }
+
+    private void copyDirectory(Filesystem srcFs, LocalFilesystemURL srcURL, File dstDir, boolean move) throws IOException, NoModificationAllowedException, InvalidModificationException, FileExistsException {
+        if (move) {
+            String realSrcPath = srcFs.filesystemPathForURL(srcURL);
+            if (realSrcPath != null) {
+                File srcDir = new File(realSrcPath);
+                // If the destination directory already exists and is empty then delete it.  This is according to spec.
+                if (dstDir.exists()) {
+                    if (dstDir.list().length > 0) {
+                        throw new InvalidModificationException("directory is not empty");
+                    }
+                    dstDir.delete();
+                }
+                // Try to rename the directory
+                if (srcDir.renameTo(dstDir)) {
+                    return;
+                }
+                // Trying to rename the file failed.  Possibly because we moved across file system on the device.
+            }
+        }
+
+        if (dstDir.exists()) {
+            if (dstDir.list().length > 0) {
+                throw new InvalidModificationException("directory is not empty");
+            }
+        } else {
+            if (!dstDir.mkdir()) {
+                // If we can't create the directory then fail
+                throw new NoModificationAllowedException("Couldn't create the destination directory");
+            }
+        }
+
+        LocalFilesystemURL[] children = srcFs.listChildren(srcURL);
+        for (LocalFilesystemURL childLocalUrl : children) {
+            File target = new File(dstDir, new File(childLocalUrl.path).getName());
+            if (childLocalUrl.isDirectory) {
+                copyDirectory(srcFs, childLocalUrl, target, false);
+            } else {
+                copyFile(srcFs, childLocalUrl, target, false);
+            }
+        }
+
+        if (move) {
+            srcFs.recursiveRemoveFileAtLocalURL(srcURL);
+        }
+    }
+
+	@Override
+	public JSONObject copyFileToURL(LocalFilesystemURL destURL, String newName,
+			Filesystem srcFs, LocalFilesystemURL srcURL, boolean move) throws IOException, InvalidModificationException, JSONException, NoModificationAllowedException, FileExistsException {
+
+		// Check to see if the destination directory exists
+        String newParent = this.filesystemPathForURL(destURL);
+        File destinationDir = new File(newParent);
+        if (!destinationDir.exists()) {
+            // The destination does not exist so we should fail.
+            throw new FileNotFoundException("The source does not exist");
+        }
+
+        // Figure out where we should be copying to
+        final LocalFilesystemURL destinationURL = makeDestinationURL(newName, srcURL, destURL, srcURL.isDirectory);
+
+        Uri dstNativeUri = toNativeUri(destinationURL);
+        Uri srcNativeUri = srcFs.toNativeUri(srcURL);
+        // Check to see if source and destination are the same file
+        if (dstNativeUri.equals(srcNativeUri)) {
+            throw new InvalidModificationException("Can't copy onto itself");
+        }
+
+        if (move && !srcFs.canRemoveFileAtLocalURL(srcURL)) {
+            throw new InvalidModificationException("Source URL is read-only (cannot move)");
+        }
+
+        File destFile = new File(dstNativeUri.getPath());
+        if (destFile.exists()) {
+            if (!srcURL.isDirectory && destFile.isDirectory()) {
+                throw new InvalidModificationException("Can't copy/move a file to an existing directory");
+            } else if (srcURL.isDirectory && destFile.isFile()) {
+                throw new InvalidModificationException("Can't copy/move a directory to an existing file");
+            }
+        }
+
+        if (srcURL.isDirectory) {
+            // E.g. Copy /sdcard/myDir to /sdcard/myDir/backup
+            if (dstNativeUri.toString().startsWith(srcNativeUri.toString() + '/')) {
+                throw new InvalidModificationException("Can't copy directory into itself");
+            }
+            copyDirectory(srcFs, srcURL, destFile, move);
+        } else {
+            copyFile(srcFs, srcURL, destFile, move);
+        }
+        return makeEntryForURL(destinationURL);
+	}
+
+	@Override
+	public long writeToFileAtURL(LocalFilesystemURL inputURL, String data,
+			int offset, boolean isBinary) throws IOException, NoModificationAllowedException {
+
+        boolean append = false;
+        if (offset > 0) {
+            this.truncateFileAtURL(inputURL, offset);
+            append = true;
+        }
+
+        byte[] rawData;
+        if (isBinary) {
+            rawData = Base64.decode(data, Base64.DEFAULT);
+        } else {
+            rawData = data.getBytes(Charset.defaultCharset());
+        }
+        ByteArrayInputStream in = new ByteArrayInputStream(rawData);
+        try
+        {
+        	byte buff[] = new byte[rawData.length];
+            String absolutePath = filesystemPathForURL(inputURL);
+            FileOutputStream out = new FileOutputStream(absolutePath, append);
+            try {
+            	in.read(buff, 0, buff.length);
+            	out.write(buff, 0, rawData.length);
+            	out.flush();
+            } finally {
+            	// Always close the output
+            	out.close();
+            }
+            if (isPublicDirectory(absolutePath)) {
+                broadcastNewFile(Uri.fromFile(new File(absolutePath)));
+            }
+        }
+        catch (NullPointerException e)
+        {
+            // This is a bug in the Android implementation of the Java Stack
+            NoModificationAllowedException realException = new NoModificationAllowedException(inputURL.toString());
+            realException.initCause(e);
+            throw realException;
+        }
+
+        return rawData.length;
+	}
+
+    private boolean isPublicDirectory(String absolutePath) {
+        // TODO: should expose a way to scan app's private files (maybe via a flag).
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            // Lollipop has a bug where SD cards are null.
+            for (File f : context.getExternalMediaDirs()) {
+                if(f != null && absolutePath.startsWith(f.getAbsolutePath())) {
+                    return true;
+                }
+            }
+        }
+
+        String extPath = Environment.getExternalStorageDirectory().getAbsolutePath();
+        return absolutePath.startsWith(extPath);
+    }
+
+     /**
+     * Send broadcast of new file so files appear over MTP
+     */
+    private void broadcastNewFile(Uri nativeUri) {
+        Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, nativeUri);
+        context.sendBroadcast(intent);
+    }
+
+	@Override
+	public long truncateFileAtURL(LocalFilesystemURL inputURL, long size) throws IOException {
+        File file = new File(filesystemPathForURL(inputURL));
+
+        if (!file.exists()) {
+            throw new FileNotFoundException("File at " + inputURL.uri + " does not exist.");
+        }
+
+        RandomAccessFile raf = new RandomAccessFile(filesystemPathForURL(inputURL), "rw");
+        try {
+            if (raf.length() >= size) {
+                FileChannel channel = raf.getChannel();
+                channel.truncate(size);
+                return size;
+            }
+
+            return raf.length();
+        } finally {
+            raf.close();
+        }
+
+
+	}
+
+	@Override
+	public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) {
+		String path = filesystemPathForURL(inputURL);
+		File file = new File(path);
+		return file.exists();
+	}
+
+    // This is a copy & paste from CordovaResource API that is required since CordovaResourceApi
+    // has a bug pre-4.0.0.
+    // TODO: Once cordova-android@4.0.0 is released, delete this copy and make the plugin depend on
+    // 4.0.0 with an engine tag.
+    private static void copyResource(CordovaResourceApi.OpenForReadResult input, OutputStream outputStream) throws IOException {
+        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();
+                }
+                // transferFrom()'s 2nd arg is a relative position. Need to set the absolute
+                // position first.
+                inChannel.position(offset);
+                outChannel.transferFrom(inChannel, 0, 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();
+            }
+        }
+    }
+}

+ 64 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/LocalFilesystemURL.java

@@ -0,0 +1,64 @@
+/*
+       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.file;
+
+import android.net.Uri;
+
+public class LocalFilesystemURL {
+	
+	public static final String FILESYSTEM_PROTOCOL = "cdvfile";
+
+    public final Uri uri;
+    public final String fsName;
+    public final String path;
+    public final boolean isDirectory;
+
+	private LocalFilesystemURL(Uri uri, String fsName, String fsPath, boolean isDirectory) {
+		this.uri = uri;
+        this.fsName = fsName;
+        this.path = fsPath;
+        this.isDirectory = isDirectory;
+	}
+
+    public static LocalFilesystemURL parse(Uri uri) {
+        if (!FILESYSTEM_PROTOCOL.equals(uri.getScheme())) {
+            return null;
+        }
+        String path = uri.getPath();
+        if (path.length() < 1) {
+            return null;
+        }
+        int firstSlashIdx = path.indexOf('/', 1);
+        if (firstSlashIdx < 0) {
+            return null;
+        }
+        String fsName = path.substring(1, firstSlashIdx);
+        path = path.substring(firstSlashIdx);
+        boolean isDirectory = path.charAt(path.length() - 1) == '/';
+        return new LocalFilesystemURL(uri, fsName, path, isDirectory);
+    }
+
+    public static LocalFilesystemURL parse(String uri) {
+        return parse(Uri.parse(uri));
+    }
+
+    public String toString() {
+        return uri.toString();
+    }
+}

+ 29 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/NoModificationAllowedException.java

@@ -0,0 +1,29 @@
+/*
+       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.file;
+
+@SuppressWarnings("serial")
+public class NoModificationAllowedException extends Exception {
+
+    public NoModificationAllowedException(String message) {
+        super(message);
+    }
+
+}

+ 94 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/PendingRequests.java

@@ -0,0 +1,94 @@
+/*
+       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.file;
+
+import android.util.SparseArray;
+
+import org.apache.cordova.CallbackContext;
+
+/**
+ * Holds pending runtime permission requests
+ */
+class PendingRequests {
+    private int currentReqId = 0;
+    private SparseArray<Request> requests = new SparseArray<Request>();
+
+    /**
+     * Creates a request and adds it to the array of pending requests. Each created request gets a
+     * unique result code for use with requestPermission()
+     * @param rawArgs           The raw arguments passed to the plugin
+     * @param action            The action this request corresponds to (get file, etc.)
+     * @param callbackContext   The CallbackContext for this plugin call
+     * @return                  The request code that can be used to retrieve the Request object
+     */
+    public synchronized int createRequest(String rawArgs, int action, CallbackContext callbackContext)  {
+        Request req = new Request(rawArgs, action, callbackContext);
+        requests.put(req.requestCode, req);
+        return req.requestCode;
+    }
+
+    /**
+     * Gets the request corresponding to this request code and removes it from the pending requests
+     * @param requestCode   The request code for the desired request
+     * @return              The request corresponding to the given request code or null if such a
+     *                      request is not found
+     */
+    public synchronized Request getAndRemove(int requestCode) {
+        Request result = requests.get(requestCode);
+        requests.remove(requestCode);
+        return result;
+    }
+
+    /**
+     * Holds the options and CallbackContext for a call made to the plugin.
+     */
+    public class Request {
+
+        // Unique int used to identify this request in any Android permission callback
+        private int requestCode;
+
+        // Action to be performed after permission request result
+        private int action;
+
+        // Raw arguments passed to plugin
+        private String rawArgs;
+
+        // The callback context for this plugin request
+        private CallbackContext callbackContext;
+
+        private Request(String rawArgs, int action, CallbackContext callbackContext) {
+            this.rawArgs = rawArgs;
+            this.action = action;
+            this.callbackContext = callbackContext;
+            this.requestCode = currentReqId ++;
+        }
+
+        public int getAction() {
+            return this.action;
+        }
+
+        public String getRawArgs() {
+            return rawArgs;
+        }
+
+        public CallbackContext getCallbackContext() {
+            return callbackContext;
+        }
+    }
+}

+ 30 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/file/TypeMismatchException.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.file;
+
+@SuppressWarnings("serial")
+public class TypeMismatchException extends Exception {
+
+    public TypeMismatchException(String message) {
+        super(message);
+    }
+
+}

+ 63 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileProgressResult.java

@@ -0,0 +1,63 @@
+/*
+       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.filetransfer;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Encapsulates in-progress status of uploading or downloading a file to a remote server.
+ */
+public class FileProgressResult {
+
+    private boolean lengthComputable = false; // declares whether total is known
+    private long loaded = 0;                  // bytes sent so far
+    private long total = 0;                   // bytes total, if known
+
+    public boolean getLengthComputable() {
+        return lengthComputable;
+    }
+
+    public void setLengthComputable(boolean computable) {
+        this.lengthComputable = computable;
+    }
+
+    public long getLoaded() {
+        return loaded;
+    }
+
+    public void setLoaded(long bytes) {
+        this.loaded = bytes;
+    }
+
+    public long getTotal() {
+        return total;
+    }
+
+    public void setTotal(long bytes) {
+        this.total = bytes;
+    }
+
+    public JSONObject toJSONObject() throws JSONException {
+        return new JSONObject(
+                "{loaded:" + loaded +
+                ",total:" + total +
+                ",lengthComputable:" + (lengthComputable ? "true" : "false") + "}");
+    }
+}

+ 973 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileTransfer.java

@@ -0,0 +1,973 @@
+/*
+       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.filetransfer;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.Inflater;
+
+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.LOG;
+import org.apache.cordova.PluginManager;
+import org.apache.cordova.PluginResult;
+import org.apache.cordova.Whitelist;
+import org.apache.cordova.file.FileUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+import android.webkit.CookieManager;
+
+public class FileTransfer extends CordovaPlugin {
+
+    private static final String LOG_TAG = "FileTransfer";
+    private static final String LINE_START = "--";
+    private static final String LINE_END = "\r\n";
+    private static final String BOUNDARY =  "+++++";
+
+    public static int FILE_NOT_FOUND_ERR = 1;
+    public static int INVALID_URL_ERR = 2;
+    public static int CONNECTION_ERR = 3;
+    public static int ABORTED_ERR = 4;
+    public static int NOT_MODIFIED_ERR = 5;
+
+    private static HashMap<String, RequestContext> activeRequests = new HashMap<String, RequestContext>();
+    private static final int MAX_BUFFER_SIZE = 16 * 1024;
+
+    private static final class RequestContext {
+        String source;
+        String target;
+        File targetFile;
+        CallbackContext callbackContext;
+        HttpURLConnection connection;
+        boolean aborted;
+        RequestContext(String source, String target, CallbackContext callbackContext) {
+            this.source = source;
+            this.target = target;
+            this.callbackContext = callbackContext;
+        }
+        void sendPluginResult(PluginResult pluginResult) {
+            synchronized (this) {
+                if (!aborted) {
+                    callbackContext.sendPluginResult(pluginResult);
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds an interface method to an InputStream to return the number of bytes
+     * read from the raw stream. This is used to track total progress against
+     * the HTTP Content-Length header value from the server.
+     */
+    private static abstract class TrackingInputStream extends FilterInputStream {
+      public TrackingInputStream(final InputStream in) {
+        super(in);
+      }
+        public abstract long getTotalRawBytesRead();
+  }
+
+    private static class ExposedGZIPInputStream extends GZIPInputStream {
+      public ExposedGZIPInputStream(final InputStream in) throws IOException {
+        super(in);
+      }
+      public Inflater getInflater() {
+        return inf;
+      }
+  }
+
+    /**
+     * Provides raw bytes-read tracking for a GZIP input stream. Reports the
+     * total number of compressed bytes read from the input, rather than the
+     * number of uncompressed bytes.
+     */
+    private static class TrackingGZIPInputStream extends TrackingInputStream {
+      private ExposedGZIPInputStream gzin;
+      public TrackingGZIPInputStream(final ExposedGZIPInputStream gzin) throws IOException {
+        super(gzin);
+        this.gzin = gzin;
+      }
+      public long getTotalRawBytesRead() {
+        return gzin.getInflater().getBytesRead();
+      }
+  }
+
+    /**
+     * Provides simple total-bytes-read tracking for an existing InputStream
+     */
+    private static class SimpleTrackingInputStream extends TrackingInputStream {
+        private long bytesRead = 0;
+        public SimpleTrackingInputStream(InputStream stream) {
+            super(stream);
+        }
+
+        private int updateBytesRead(int newBytesRead) {
+          if (newBytesRead != -1) {
+            bytesRead += newBytesRead;
+          }
+          return newBytesRead;
+        }
+
+        @Override
+        public int read() throws IOException {
+            return updateBytesRead(super.read());
+        }
+
+        // Note: FilterInputStream delegates read(byte[] bytes) to the below method,
+        // so we don't override it or else double count (CB-5631).
+        @Override
+        public int read(byte[] bytes, int offset, int count) throws IOException {
+            return updateBytesRead(super.read(bytes, offset, count));
+        }
+
+        public long getTotalRawBytesRead() {
+          return bytesRead;
+        }
+    }
+
+    @Override
+    public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
+        if (action.equals("upload") || action.equals("download")) {
+            String source = args.getString(0);
+            String target = args.getString(1);
+
+            if (action.equals("upload")) {
+                upload(source, target, args, callbackContext);
+            } else {
+                download(source, target, args, callbackContext);
+            }
+            return true;
+        } else if (action.equals("abort")) {
+            String objectId = args.getString(0);
+            abort(objectId);
+            callbackContext.success();
+            return true;
+        }
+        return false;
+    }
+
+    private static void addHeadersToRequest(URLConnection connection, JSONObject headers) {
+        try {
+            for (Iterator<?> iter = headers.keys(); iter.hasNext(); ) {
+                /* RFC 2616 says that non-ASCII characters and control
+                 * characters are not allowed in header names or values.
+                 * Additionally, spaces are not allowed in header names.
+                 * RFC 2046 Quoted-printable encoding may be used to encode
+                 * arbitrary characters, but we donon- not do that encoding here.
+                 */
+                String headerKey = iter.next().toString();
+                String cleanHeaderKey = headerKey.replaceAll("\\n","")
+                        .replaceAll("\\s+","")
+                        .replaceAll(":", "")
+                        .replaceAll("[^\\x20-\\x7E]+", "");
+
+                JSONArray headerValues = headers.optJSONArray(headerKey);
+                if (headerValues == null) {
+                    headerValues = new JSONArray();
+
+                     /* RFC 2616 also says that any amount of consecutive linear
+                      * whitespace within a header value can be replaced with a
+                      * single space character, without affecting the meaning of
+                      * that value.
+                      */
+
+                    String headerValue = headers.getString(headerKey);
+                    String finalValue = headerValue.replaceAll("\\s+", " ").replaceAll("\\n"," ").replaceAll("[^\\x20-\\x7E]+", " ");
+                    headerValues.put(finalValue);
+                }
+
+                //Use the clean header key, not the one that we passed in
+                connection.setRequestProperty(cleanHeaderKey, headerValues.getString(0));
+                for (int i = 1; i < headerValues.length(); ++i) {
+                    connection.addRequestProperty(headerKey, headerValues.getString(i));
+                }
+            }
+        } catch (JSONException e1) {
+          // No headers to be manipulated!
+        }
+    }
+
+    private String getCookies(final String target) {
+        boolean gotCookie = false;
+        String cookie = null;
+        Class webViewClass = webView.getClass();
+        try {
+            Method gcmMethod = webViewClass.getMethod("getCookieManager");
+            Class iccmClass  = gcmMethod.getReturnType();
+            Method gcMethod  = iccmClass.getMethod("getCookie", String.class);
+
+            cookie = (String)gcMethod.invoke(
+                        iccmClass.cast(
+                            gcmMethod.invoke(webView)
+                        ), target);
+
+            gotCookie = true;
+        } catch (NoSuchMethodException e) {
+        } catch (IllegalAccessException e) {
+        } catch (InvocationTargetException e) {
+        } catch (ClassCastException e) {
+        }
+
+        if (!gotCookie && CookieManager.getInstance() != null) {
+            cookie = CookieManager.getInstance().getCookie(target);
+        }
+
+        return cookie;
+    }
+
+    /**
+     * Uploads the specified file to the server URL provided using an HTTP multipart request.
+     * @param source        Full path of the file on the file system
+     * @param target        URL of the server to receive the file
+     * @param args          JSON Array of args
+     * @param callbackContext    callback id for optional progress reports
+     *
+     * args[2] fileKey       Name of file request parameter
+     * args[3] fileName      File name to be used on server
+     * args[4] mimeType      Describes file content type
+     * args[5] params        key:value pairs of user-defined parameters
+     * @return FileUploadResult containing result of upload request
+     */
+    private void upload(final String source, final String target, JSONArray args, CallbackContext callbackContext) throws JSONException {
+        LOG.d(LOG_TAG, "upload " + source + " to " +  target);
+
+        // Setup the options
+        final String fileKey = getArgument(args, 2, "file");
+        final String fileName = getArgument(args, 3, "image.jpg");
+        final String mimeType = getArgument(args, 4, "image/jpeg");
+        final JSONObject params = args.optJSONObject(5) == null ? new JSONObject() : args.optJSONObject(5);
+        // Always use chunked mode unless set to false as per API
+        final boolean chunkedMode = args.optBoolean(7) || args.isNull(7);
+        // Look for headers on the params map for backwards compatibility with older Cordova versions.
+        final JSONObject headers = args.optJSONObject(8) == null ? params.optJSONObject("headers") : args.optJSONObject(8);
+        final String objectId = args.getString(9);
+        final String httpMethod = getArgument(args, 10, "POST");
+
+        final CordovaResourceApi resourceApi = webView.getResourceApi();
+
+        LOG.d(LOG_TAG, "fileKey: " + fileKey);
+        LOG.d(LOG_TAG, "fileName: " + fileName);
+        LOG.d(LOG_TAG, "mimeType: " + mimeType);
+        LOG.d(LOG_TAG, "params: " + params);
+        LOG.d(LOG_TAG, "chunkedMode: " + chunkedMode);
+        LOG.d(LOG_TAG, "headers: " + headers);
+        LOG.d(LOG_TAG, "objectId: " + objectId);
+        LOG.d(LOG_TAG, "httpMethod: " + httpMethod);
+
+        final Uri targetUri = resourceApi.remapUri(Uri.parse(target));
+
+        int uriType = CordovaResourceApi.getUriType(targetUri);
+        final boolean useHttps = uriType == CordovaResourceApi.URI_TYPE_HTTPS;
+        if (uriType != CordovaResourceApi.URI_TYPE_HTTP && !useHttps) {
+            JSONObject error = createFileTransferError(INVALID_URL_ERR, source, target, null, 0, null);
+            LOG.e(LOG_TAG, "Unsupported URI: " + targetUri);
+            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+            return;
+        }
+
+        final RequestContext context = new RequestContext(source, target, callbackContext);
+        synchronized (activeRequests) {
+            activeRequests.put(objectId, context);
+        }
+
+        cordova.getThreadPool().execute(new Runnable() {
+            public void run() {
+                if (context.aborted) {
+                    return;
+                }
+
+                // We should call remapUri on background thread otherwise it throws
+                // IllegalStateException when trying to remap 'cdvfile://localhost/content/...' URIs
+                // via ContentFilesystem (see https://issues.apache.org/jira/browse/CB-9022)
+                Uri tmpSrc = Uri.parse(source);
+                final Uri sourceUri = resourceApi.remapUri(
+                        tmpSrc.getScheme() != null ? tmpSrc : Uri.fromFile(new File(source)));
+
+                HttpURLConnection conn = null;
+                int totalBytes = 0;
+                int fixedLength = -1;
+                try {
+                    // Create return object
+                    FileUploadResult result = new FileUploadResult();
+                    FileProgressResult progress = new FileProgressResult();
+
+                    //------------------ CLIENT REQUEST
+                    // Open a HTTP connection to the URL based on protocol
+                    conn = resourceApi.createHttpConnection(targetUri);
+
+                    // Allow Inputs
+                    conn.setDoInput(true);
+
+                    // Allow Outputs
+                    conn.setDoOutput(true);
+
+                    // Don't use a cached copy.
+                    conn.setUseCaches(false);
+
+                    // Use a post method.
+                    conn.setRequestMethod(httpMethod);
+
+                    // if we specified a Content-Type header, don't do multipart form upload
+                    boolean multipartFormUpload = (headers == null) || !headers.has("Content-Type");
+                    if (multipartFormUpload) {
+                        conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
+                    }
+
+                    // Set the cookies on the response
+                    String cookie = getCookies(target);
+
+                    if (cookie != null) {
+                        conn.setRequestProperty("Cookie", cookie);
+                    }
+
+                    // Handle the other headers
+                    if (headers != null) {
+                        addHeadersToRequest(conn, headers);
+                    }
+
+                    /*
+                        * Store the non-file portions of the multipart data as a string, so that we can add it
+                        * to the contentSize, since it is part of the body of the HTTP request.
+                        */
+                    StringBuilder beforeData = new StringBuilder();
+                    try {
+                        for (Iterator<?> iter = params.keys(); iter.hasNext();) {
+                            Object key = iter.next();
+                            if(!String.valueOf(key).equals("headers"))
+                            {
+                              beforeData.append(LINE_START).append(BOUNDARY).append(LINE_END);
+                              beforeData.append("Content-Disposition: form-data; name=\"").append(key.toString()).append('"');
+                              beforeData.append(LINE_END).append(LINE_END);
+                              beforeData.append(params.getString(key.toString()));
+                              beforeData.append(LINE_END);
+                            }
+                        }
+                    } catch (JSONException e) {
+                        LOG.e(LOG_TAG, e.getMessage(), e);
+                    }
+
+                    beforeData.append(LINE_START).append(BOUNDARY).append(LINE_END);
+                    beforeData.append("Content-Disposition: form-data; name=\"").append(fileKey).append("\";");
+                    beforeData.append(" filename=\"").append(fileName).append('"').append(LINE_END);
+                    beforeData.append("Content-Type: ").append(mimeType).append(LINE_END).append(LINE_END);
+                    byte[] beforeDataBytes = beforeData.toString().getBytes("UTF-8");
+                    byte[] tailParamsBytes = (LINE_END + LINE_START + BOUNDARY + LINE_START + LINE_END).getBytes("UTF-8");
+
+
+                    // Get a input stream of the file on the phone
+                    OpenForReadResult readResult = resourceApi.openForRead(sourceUri);
+
+                    int stringLength = beforeDataBytes.length + tailParamsBytes.length;
+                    if (readResult.length >= 0) {
+                        fixedLength = (int)readResult.length;
+                        if (multipartFormUpload)
+                            fixedLength += stringLength;
+                        progress.setLengthComputable(true);
+                        progress.setTotal(fixedLength);
+                    }
+                    LOG.d(LOG_TAG, "Content Length: " + fixedLength);
+                    // setFixedLengthStreamingMode causes and OutOfMemoryException on pre-Froyo devices.
+                    // http://code.google.com/p/android/issues/detail?id=3164
+                    // It also causes OOM if HTTPS is used, even on newer devices.
+                    boolean useChunkedMode = chunkedMode || (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO);
+                    useChunkedMode = useChunkedMode || (fixedLength == -1);
+
+                    if (useChunkedMode) {
+                        conn.setChunkedStreamingMode(MAX_BUFFER_SIZE);
+                        // Although setChunkedStreamingMode sets this header, setting it explicitly here works
+                        // around an OutOfMemoryException when using https.
+                        conn.setRequestProperty("Transfer-Encoding", "chunked");
+                    } else {
+                        conn.setFixedLengthStreamingMode(fixedLength);
+
+                        if (useHttps) {
+                            LOG.w(LOG_TAG, "setFixedLengthStreamingMode could cause OutOfMemoryException - switch to chunkedMode=true to avoid it if this is an issue.");
+                        }
+                    }
+
+                    conn.connect();
+
+                    OutputStream sendStream = null;
+                    try {
+                        sendStream = conn.getOutputStream();
+                        synchronized (context) {
+                            if (context.aborted) {
+                                return;
+                            }
+                            context.connection = conn;
+                        }
+
+                        if (multipartFormUpload) {
+                            //We don't want to change encoding, we just want this to write for all Unicode.
+                            sendStream.write(beforeDataBytes);
+                            totalBytes += beforeDataBytes.length;
+                        }
+
+                        // create a buffer of maximum size
+                        int bytesAvailable = readResult.inputStream.available();
+                        int bufferSize = Math.min(bytesAvailable, MAX_BUFFER_SIZE);
+                        byte[] buffer = new byte[bufferSize];
+
+                        // read file and write it into form...
+                        int bytesRead = readResult.inputStream.read(buffer, 0, bufferSize);
+
+                        long prevBytesRead = 0;
+                        while (bytesRead > 0) {
+                            totalBytes += bytesRead;
+                            result.setBytesSent(totalBytes);
+                            sendStream.write(buffer, 0, bytesRead);
+                            if (totalBytes > prevBytesRead + 102400) {
+                                prevBytesRead = totalBytes;
+                                LOG.d(LOG_TAG, "Uploaded " + totalBytes + " of " + fixedLength + " bytes");
+                            }
+                            bytesAvailable = readResult.inputStream.available();
+                            bufferSize = Math.min(bytesAvailable, MAX_BUFFER_SIZE);
+                            bytesRead = readResult.inputStream.read(buffer, 0, bufferSize);
+
+                            // Send a progress event.
+                            progress.setLoaded(totalBytes);
+                            PluginResult progressResult = new PluginResult(PluginResult.Status.OK, progress.toJSONObject());
+                            progressResult.setKeepCallback(true);
+                            context.sendPluginResult(progressResult);
+                        }
+
+                        if (multipartFormUpload) {
+                            // send multipart form data necessary after file data...
+                            sendStream.write(tailParamsBytes);
+                            totalBytes += tailParamsBytes.length;
+                        }
+                        sendStream.flush();
+                    } finally {
+                        safeClose(readResult.inputStream);
+                        safeClose(sendStream);
+                    }
+                    synchronized (context) {
+                        context.connection = null;
+                    }
+                    LOG.d(LOG_TAG, "Sent " + totalBytes + " of " + fixedLength);
+
+                    //------------------ read the SERVER RESPONSE
+                    String responseString;
+                    int responseCode = conn.getResponseCode();
+                    LOG.d(LOG_TAG, "response code: " + responseCode);
+                    LOG.d(LOG_TAG, "response headers: " + conn.getHeaderFields());
+                    TrackingInputStream inStream = null;
+                    try {
+                        inStream = getInputStream(conn);
+                        synchronized (context) {
+                            if (context.aborted) {
+                                return;
+                            }
+                            context.connection = conn;
+                        }
+
+                        ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(1024, conn.getContentLength()));
+                        byte[] buffer = new byte[1024];
+                        int bytesRead = 0;
+                        // write bytes to file
+                        while ((bytesRead = inStream.read(buffer)) > 0) {
+                            out.write(buffer, 0, bytesRead);
+                        }
+                        responseString = out.toString("UTF-8");
+                    } finally {
+                        synchronized (context) {
+                            context.connection = null;
+                        }
+                        safeClose(inStream);
+                    }
+
+                    LOG.d(LOG_TAG, "got response from server");
+                    LOG.d(LOG_TAG, responseString.substring(0, Math.min(256, responseString.length())));
+
+                    // send request and retrieve response
+                    result.setResponseCode(responseCode);
+                    result.setResponse(responseString);
+
+                    context.sendPluginResult(new PluginResult(PluginResult.Status.OK, result.toJSONObject()));
+                } catch (FileNotFoundException e) {
+                    JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, source, target, conn, e);
+                    LOG.e(LOG_TAG, error.toString(), e);
+                    context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+                } catch (IOException e) {
+                    JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, conn, e);
+                    LOG.e(LOG_TAG, error.toString(), e);
+                    LOG.e(LOG_TAG, "Failed after uploading " + totalBytes + " of " + fixedLength + " bytes.");
+                    context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+                } catch (JSONException e) {
+                    LOG.e(LOG_TAG, e.getMessage(), e);
+                    context.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
+                } catch (Throwable t) {
+                    // Shouldn't happen, but will
+                    JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, conn, t);
+                    LOG.e(LOG_TAG, error.toString(), t);
+                    context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+                } finally {
+                    synchronized (activeRequests) {
+                        activeRequests.remove(objectId);
+                    }
+                }
+            }
+        });
+    }
+
+    private static void safeClose(Closeable stream) {
+        if (stream != null) {
+            try {
+                stream.close();
+            } catch (IOException e) {
+            }
+        }
+    }
+
+    private static TrackingInputStream getInputStream(URLConnection conn) throws IOException {
+        String encoding = conn.getContentEncoding();
+        if (encoding != null && encoding.equalsIgnoreCase("gzip")) {
+          return new TrackingGZIPInputStream(new ExposedGZIPInputStream(conn.getInputStream()));
+        }
+        return new SimpleTrackingInputStream(conn.getInputStream());
+    }
+
+    private static JSONObject createFileTransferError(int errorCode, String source, String target, URLConnection connection, Throwable throwable) {
+
+        int httpStatus = 0;
+        StringBuilder bodyBuilder = new StringBuilder();
+        String body = null;
+        if (connection != null) {
+            try {
+                if (connection instanceof HttpURLConnection) {
+                    httpStatus = ((HttpURLConnection)connection).getResponseCode();
+                    InputStream err = ((HttpURLConnection) connection).getErrorStream();
+                    if(err != null)
+                    {
+                        BufferedReader reader = new BufferedReader(new InputStreamReader(err, "UTF-8"));
+                        try {
+                            String line = reader.readLine();
+                            while(line != null) {
+                                bodyBuilder.append(line);
+                                line = reader.readLine();
+                                if(line != null) {
+                                    bodyBuilder.append('\n');
+                                }
+                            }
+                            body = bodyBuilder.toString();
+                        } finally {
+                            reader.close();
+                        }
+                    }
+                }
+            // IOException can leave connection object in a bad state, so catch all exceptions.
+            } catch (Throwable e) {
+                LOG.w(LOG_TAG, "Error getting HTTP status code from connection.", e);
+            }
+        }
+
+        return createFileTransferError(errorCode, source, target, body, httpStatus, throwable);
+    }
+
+        /**
+        * Create an error object based on the passed in errorCode
+        * @param errorCode      the error
+        * @return JSONObject containing the error
+        */
+    private static JSONObject createFileTransferError(int errorCode, String source, String target, String body, Integer httpStatus, Throwable throwable) {
+        JSONObject error = null;
+        try {
+            error = new JSONObject();
+            error.put("code", errorCode);
+            error.put("source", source);
+            error.put("target", target);
+            if(body != null)
+            {
+                error.put("body", body);
+            }
+            if (httpStatus != null) {
+                error.put("http_status", httpStatus);
+            }
+            if (throwable != null) {
+                String msg = throwable.getMessage();
+                if (msg == null || "".equals(msg)) {
+                    msg = throwable.toString();
+                }
+                error.put("exception", msg);
+            }
+        } catch (JSONException e) {
+            LOG.e(LOG_TAG, e.getMessage(), e);
+        }
+        return error;
+    }
+
+    /**
+     * Convenience method to read a parameter from the list of JSON args.
+     * @param args                      the args passed to the Plugin
+     * @param position          the position to retrieve the arg from
+     * @param defaultString the default to be used if the arg does not exist
+     * @return String with the retrieved value
+     */
+    private static String getArgument(JSONArray args, int position, String defaultString) {
+        String arg = defaultString;
+        if (args.length() > position) {
+            arg = args.optString(position);
+            if (arg == null || "null".equals(arg)) {
+                arg = defaultString;
+            }
+        }
+        return arg;
+    }
+    /**
+     * 获取重定向地址
+     *
+     * @param path
+     * @return
+     * @throws Exception
+     */
+    private String getRedirectUrl(String path) {
+        String newUrl = path;
+        try {
+
+            URL url = new URL(path);
+            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+            //设置超时时间为3秒
+            conn.setConnectTimeout(3 * 1000);
+            //防止屏蔽程序抓取而返回403错误
+            conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
+
+            Map<String, List<String>> map = conn.getHeaderFields();
+            //遍历所有的响应头字段
+            for (String key : map.keySet()) {
+                System.out.println("chromium" + key + "--->" + map.get(key).get(0));
+                if ("Location".equals(key)) {
+                    //获取新地址
+                    newUrl = map.get(key).get(0);
+                    System.out.println("chromium" + " newUrl--->" + newUrl);
+                    return newUrl;
+                }
+            }
+            return newUrl;
+        } catch (IOException ignored) {
+            Log.e("chromium", Objects.requireNonNull(ignored.getMessage()));
+            return newUrl;
+        }
+    }
+
+    /**
+     * Downloads a file form a given URL and saves it to the specified directory.
+     *
+     * @param sources        URL of the server to receive the file
+     * @param target            Full path of the file on the file system
+     */
+    private void download(final String sources, final String target, JSONArray args, CallbackContext callbackContext) throws JSONException {
+        String source = getRedirectUrl(sources);
+        LOG.d(LOG_TAG, "download " + source + " to " +  target);
+
+        final CordovaResourceApi resourceApi = webView.getResourceApi();
+
+        final String objectId = args.getString(3);
+        final JSONObject headers = args.optJSONObject(4);
+
+        final Uri sourceUri = resourceApi.remapUri(Uri.parse(source));
+        int uriType = CordovaResourceApi.getUriType(sourceUri);
+        final boolean useHttps = uriType == CordovaResourceApi.URI_TYPE_HTTPS;
+        final boolean isLocalTransfer = !useHttps && uriType != CordovaResourceApi.URI_TYPE_HTTP;
+        if (uriType == CordovaResourceApi.URI_TYPE_UNKNOWN) {
+            JSONObject error = createFileTransferError(INVALID_URL_ERR, source, target, null, 0, null);
+            LOG.e(LOG_TAG, "Unsupported URI: " + sourceUri);
+            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+            return;
+        }
+
+        /* This code exists for compatibility between 3.x and 4.x versions of Cordova.
+         * Previously the CordovaWebView class had a method, getWhitelist, which would
+         * return a Whitelist object. Since the fixed whitelist is removed in Cordova 4.x,
+         * the correct call now is to shouldAllowRequest from the plugin manager.
+         */
+        Boolean shouldAllowRequest = null;
+        if (isLocalTransfer) {
+            shouldAllowRequest = true;
+        }
+        if (shouldAllowRequest == null) {
+            try {
+                Method gwl = webView.getClass().getMethod("getWhitelist");
+                Whitelist whitelist = (Whitelist)gwl.invoke(webView);
+                shouldAllowRequest = whitelist.isUrlWhiteListed(source);
+            } catch (NoSuchMethodException e) {
+            } catch (IllegalAccessException e) {
+            } catch (InvocationTargetException e) {
+            }
+        }
+        if (shouldAllowRequest == null) {
+            try {
+                Method gpm = webView.getClass().getMethod("getPluginManager");
+                PluginManager pm = (PluginManager)gpm.invoke(webView);
+                Method san = pm.getClass().getMethod("shouldAllowRequest", String.class);
+                shouldAllowRequest = (Boolean)san.invoke(pm, source);
+            } catch (NoSuchMethodException e) {
+            } catch (IllegalAccessException e) {
+            } catch (InvocationTargetException e) {
+            }
+        }
+
+        if (!Boolean.TRUE.equals(shouldAllowRequest)) {
+            LOG.w(LOG_TAG, "Source URL is not in white list: '" + source + "'");
+            JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, null, 401, null);
+            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+            return;
+        }
+
+
+        final RequestContext context = new RequestContext(source, target, callbackContext);
+        synchronized (activeRequests) {
+            activeRequests.put(objectId, context);
+        }
+
+        cordova.getThreadPool().execute(new Runnable() {
+            public void run() {
+                if (context.aborted) {
+                    return;
+                }
+                // Accept a path or a URI for the source.
+                Uri tmpTarget = Uri.parse(target);
+                Uri targetUri = resourceApi.remapUri(
+                        tmpTarget.getScheme() != null ? tmpTarget : Uri.fromFile(new File(target)));
+                HttpURLConnection connection = null;
+                File file = null;
+                PluginResult result = null;
+                TrackingInputStream inputStream = null;
+                boolean cached = false;
+
+                OutputStream outputStream = null;
+                try {
+                    OpenForReadResult readResult = null;
+
+                    file = resourceApi.mapUriToFile(targetUri);
+                    context.targetFile = file;
+
+                    LOG.d(LOG_TAG, "Download file:" + sourceUri);
+
+                    FileProgressResult progress = new FileProgressResult();
+
+                    if (isLocalTransfer) {
+                        readResult = resourceApi.openForRead(sourceUri);
+                        if (readResult.length != -1) {
+                            progress.setLengthComputable(true);
+                            progress.setTotal(readResult.length);
+                        }
+                        inputStream = new SimpleTrackingInputStream(readResult.inputStream);
+                    } else {
+                        // connect to server
+                        // Open a HTTP connection to the URL based on protocol
+                        connection = resourceApi.createHttpConnection(sourceUri);
+                        connection.setRequestMethod("GET");
+
+                        // TODO: Make OkHttp use this CookieManager by default.
+                        String cookie = getCookies(sourceUri.toString());
+
+                        if(cookie != null)
+                        {
+                            connection.setRequestProperty("cookie", cookie);
+                        }
+
+                        // This must be explicitly set for gzip progress tracking to work.
+                        connection.setRequestProperty("Accept-Encoding", "gzip");
+
+                        // Handle the other headers
+                        if (headers != null) {
+                            addHeadersToRequest(connection, headers);
+                        }
+
+                        connection.connect();
+                        if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
+                            cached = true;
+                            connection.disconnect();
+                            LOG.d(LOG_TAG, "Resource not modified: " + source);
+                            JSONObject error = createFileTransferError(NOT_MODIFIED_ERR, source, target, connection, null);
+                            result = new PluginResult(PluginResult.Status.ERROR, error);
+                        } else {
+                            if (connection.getContentEncoding() == null || connection.getContentEncoding().equalsIgnoreCase("gzip")) {
+                                // Only trust content-length header if we understand
+                                // the encoding -- identity or gzip
+                                if (connection.getContentLength() != -1) {
+                                    progress.setLengthComputable(true);
+                                    progress.setTotal(connection.getContentLength());
+                                }
+                            }
+                            inputStream = getInputStream(connection);
+                        }
+                    }
+
+                    if (!cached) {
+                        try {
+                            synchronized (context) {
+                                if (context.aborted) {
+                                    return;
+                                }
+                                context.connection = connection;
+                            }
+
+                            // write bytes to file
+                            byte[] buffer = new byte[MAX_BUFFER_SIZE];
+                            int bytesRead = 0;
+                            outputStream = resourceApi.openOutputStream(targetUri);
+                            while ((bytesRead = inputStream.read(buffer)) > 0) {
+                                outputStream.write(buffer, 0, bytesRead);
+                                // Send a progress event.
+                                progress.setLoaded(inputStream.getTotalRawBytesRead());
+                                PluginResult progressResult = new PluginResult(PluginResult.Status.OK, progress.toJSONObject());
+                                progressResult.setKeepCallback(true);
+                                context.sendPluginResult(progressResult);
+                            }
+                        } finally {
+                            synchronized (context) {
+                                context.connection = null;
+                            }
+                            safeClose(inputStream);
+                            safeClose(outputStream);
+                        }
+
+                        LOG.d(LOG_TAG, "Saved file: " + target);
+
+
+                        // create FileEntry object
+                        Class webViewClass = webView.getClass();
+                        PluginManager pm = null;
+                        try {
+                            Method gpm = webViewClass.getMethod("getPluginManager");
+                            pm = (PluginManager) gpm.invoke(webView);
+                        } catch (NoSuchMethodException e) {
+                        } catch (IllegalAccessException e) {
+                        } catch (InvocationTargetException e) {
+                        }
+                        if (pm == null) {
+                            try {
+                                Field pmf = webViewClass.getField("pluginManager");
+                                pm = (PluginManager)pmf.get(webView);
+                            } catch (NoSuchFieldException e) {
+                            } catch (IllegalAccessException e) {
+                            }
+                        }
+                        file = resourceApi.mapUriToFile(targetUri);
+                        context.targetFile = file;
+                        FileUtils filePlugin = (FileUtils) pm.getPlugin("File");
+                        if (filePlugin != null) {
+                            JSONObject fileEntry = filePlugin.getEntryForFile(file);
+                            if (fileEntry != null) {
+                                result = new PluginResult(PluginResult.Status.OK, fileEntry);
+                            } else {
+                                JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, connection, null);
+                                LOG.e(LOG_TAG, "File plugin cannot represent download path");
+                                result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
+                            }
+                        } else {
+                            LOG.e(LOG_TAG, "File plugin not found; cannot save downloaded file");
+                            result = new PluginResult(PluginResult.Status.ERROR, "File plugin not found; cannot save downloaded file");
+                        }
+                    }
+                } catch (FileNotFoundException e) {
+                    JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, source, target, connection, e);
+                    LOG.e(LOG_TAG, error.toString(), e);
+                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
+                } catch (IOException e) {
+                    JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, connection, e);
+                    LOG.e(LOG_TAG, error.toString(), e);
+                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
+                } catch (JSONException e) {
+                    LOG.e(LOG_TAG, e.getMessage(), e);
+                    result = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
+                } catch (Throwable e) {
+                    JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, connection, e);
+                    LOG.e(LOG_TAG, error.toString(), e);
+                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
+                } finally {
+                    synchronized (activeRequests) {
+                        activeRequests.remove(objectId);
+                    }
+
+                    if (result == null) {
+                        result = new PluginResult(PluginResult.Status.ERROR, createFileTransferError(CONNECTION_ERR, source, target, connection, null));
+                    }
+                    // Remove incomplete download.
+                    if (!cached && result.getStatus() != PluginResult.Status.OK.ordinal() && file != null) {
+                        file.delete();
+                    }
+                    context.sendPluginResult(result);
+                }
+            }
+        });
+    }
+
+    /**
+     * Abort an ongoing upload or download.
+     */
+    private void abort(String objectId) {
+        final RequestContext context;
+        synchronized (activeRequests) {
+            context = activeRequests.remove(objectId);
+        }
+        if (context != null) {
+            // Closing the streams can block, so execute on a background thread.
+            cordova.getThreadPool().execute(new Runnable() {
+                public void run() {
+                    synchronized (context) {
+                        File file = context.targetFile;
+                        if (file != null) {
+                            file.delete();
+                        }
+                        // Trigger the abort callback immediately to minimize latency between it and abort() being called.
+                        JSONObject error = createFileTransferError(ABORTED_ERR, context.source, context.target, null, -1, null);
+                        context.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, error));
+                        context.aborted = true;
+                        if (context.connection != null) {
+                            try {
+                                context.connection.disconnect();
+                            } catch (Exception e) {
+                                LOG.e(LOG_TAG, "CB-8431 Catch workaround for fatal exception", e);
+                            }
+                        }
+                    }
+                }
+            });
+        }
+    }
+}

+ 73 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileUploadResult.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.
+*/
+package org.apache.cordova.filetransfer;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Encapsulates the result and/or status of uploading a file to a remote server.
+ */
+public class FileUploadResult {
+
+    private long bytesSent = 0;         // bytes sent
+    private int responseCode = -1;      // HTTP response code
+    private String response = null;     // HTTP response
+    private String objectId = null;     // FileTransfer object id
+
+    public long getBytesSent() {
+        return bytesSent;
+    }
+
+    public void setBytesSent(long bytes) {
+        this.bytesSent = bytes;
+    }
+
+    public int getResponseCode() {
+        return responseCode;
+    }
+
+    public void setResponseCode(int responseCode) {
+        this.responseCode = responseCode;
+    }
+
+    public String getResponse() {
+        return response;
+    }
+
+    public void setResponse(String response) {
+        this.response = response;
+    }
+
+    public String getObjectId() {
+        return objectId;
+    }
+
+    public void setObjectId(String objectId) {
+        this.objectId = objectId;
+    }
+
+    public JSONObject toJSONObject() throws JSONException {
+        return new JSONObject(
+                "{bytesSent:" + bytesSent +
+                ",responseCode:" + responseCode +
+                ",response:" + JSONObject.quote(response) +
+                ",objectId:" + JSONObject.quote(objectId) + "}");
+    }
+}

+ 413 - 0
cordova/platforms/android/app/src/main/java/org/apache/cordova/splashscreen/SplashScreen.java

@@ -0,0 +1,413 @@
+/*
+       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.splashscreen;
+
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Configuration;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Handler;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CordovaWebView;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+public class SplashScreen extends CordovaPlugin {
+    private static final String LOG_TAG = "SplashScreen";
+    // Cordova 3.x.x has a copy of this plugin bundled with it (SplashScreenInternal.java).
+    // Enable functionality only if running on 4.x.x.
+    private static final boolean HAS_BUILT_IN_SPLASH_SCREEN = Integer.valueOf(CordovaWebView.CORDOVA_VERSION.split("\\.")[0]) < 4;
+    private static final int DEFAULT_SPLASHSCREEN_DURATION = 3000;
+    private static final int DEFAULT_FADE_DURATION = 500;
+    private static Dialog splashDialog;
+    private static ProgressDialog spinnerDialog;
+    private static boolean firstShow = true;
+    private static boolean lastHideAfterDelay; // https://issues.apache.org/jira/browse/CB-9094
+
+    /**
+     * Displays the splash drawable.
+     */
+    private ImageView splashImageView;
+
+    /**
+     * Remember last device orientation to detect orientation changes.
+     */
+    private int orientation;
+
+    // Helper to be compile-time compatible with both Cordova 3.x and 4.x.
+    private View getView() {
+        try {
+            return (View)webView.getClass().getMethod("getView").invoke(webView);
+        } catch (Exception e) {
+            return (View)webView;
+        }
+    }
+
+    private int getSplashId() {
+        int drawableId = 0;
+        String splashResource = preferences.getString("SplashScreen", "screen");
+        if (splashResource != null) {
+            drawableId = cordova.getActivity().getResources().getIdentifier(splashResource, "drawable", cordova.getActivity().getClass().getPackage().getName());
+            if (drawableId == 0) {
+                drawableId = cordova.getActivity().getResources().getIdentifier(splashResource, "drawable", cordova.getActivity().getPackageName());
+            }
+        }
+        return drawableId;
+    }
+
+    @Override
+    protected void pluginInitialize() {
+        if (HAS_BUILT_IN_SPLASH_SCREEN) {
+            return;
+        }
+        // Make WebView invisible while loading URL
+        // CB-11326 Ensure we're calling this on UI thread
+        cordova.getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                getView().setVisibility(View.INVISIBLE);
+            }
+        });
+        int drawableId = getSplashId();
+
+        // Save initial orientation.
+        orientation = cordova.getActivity().getResources().getConfiguration().orientation;
+
+        if (firstShow) {
+            boolean autoHide = preferences.getBoolean("AutoHideSplashScreen", true);
+            showSplashScreen(autoHide);
+        }
+
+        if (preferences.getBoolean("SplashShowOnlyFirstTime", true)) {
+            firstShow = false;
+        }
+    }
+
+    /**
+     * Shorter way to check value of "SplashMaintainAspectRatio" preference.
+     */
+    private boolean isMaintainAspectRatio () {
+        return preferences.getBoolean("SplashMaintainAspectRatio", false);
+    }
+
+    private int getFadeDuration () {
+        int fadeSplashScreenDuration = preferences.getBoolean("FadeSplashScreen", true) ?
+            preferences.getInteger("FadeSplashScreenDuration", DEFAULT_FADE_DURATION) : 0;
+
+        if (fadeSplashScreenDuration < 30) {
+            // [CB-9750] This value used to be in decimal seconds, so we will assume that if someone specifies 10
+            // they mean 10 seconds, and not the meaningless 10ms
+            fadeSplashScreenDuration *= 1000;
+        }
+
+        return fadeSplashScreenDuration;
+    }
+
+    @Override
+    public void onPause(boolean multitasking) {
+        if (HAS_BUILT_IN_SPLASH_SCREEN) {
+            return;
+        }
+        // hide the splash screen to avoid leaking a window
+        this.removeSplashScreen(true);
+    }
+
+    @Override
+    public void onDestroy() {
+        if (HAS_BUILT_IN_SPLASH_SCREEN) {
+            return;
+        }
+        // hide the splash screen to avoid leaking a window
+        this.removeSplashScreen(true);
+        // If we set this to true onDestroy, we lose track when we go from page to page!
+        //firstShow = true;
+    }
+
+    @Override
+    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+        if (action.equals("hide")) {
+            cordova.getActivity().runOnUiThread(new Runnable() {
+                public void run() {
+                    webView.postMessage("splashscreen", "hide");
+                }
+            });
+        } else if (action.equals("show")) {
+            cordova.getActivity().runOnUiThread(new Runnable() {
+                public void run() {
+                    webView.postMessage("splashscreen", "show");
+                }
+            });
+        } else {
+            return false;
+        }
+
+        callbackContext.success();
+        return true;
+    }
+
+    @Override
+    public Object onMessage(String id, Object data) {
+        if (HAS_BUILT_IN_SPLASH_SCREEN) {
+            return null;
+        }
+        if ("splashscreen".equals(id)) {
+            if ("hide".equals(data.toString())) {
+                this.removeSplashScreen(false);
+            } else {
+                this.showSplashScreen(false);
+            }
+        } else if ("spinner".equals(id)) {
+            if ("stop".equals(data.toString())) {
+                getView().setVisibility(View.VISIBLE);
+            }
+        } else if ("onReceivedError".equals(id)) {
+            this.spinnerStop();
+        }
+        return null;
+    }
+
+    // Don't add @Override so that plugin still compiles on 3.x.x for a while
+    public void onConfigurationChanged(Configuration newConfig) {
+        if (newConfig.orientation != orientation) {
+            orientation = newConfig.orientation;
+
+            // Splash drawable may change with orientation, so reload it.
+            if (splashImageView != null) {
+                int drawableId = getSplashId();
+                if (drawableId != 0) {
+                    splashImageView.setImageDrawable(cordova.getActivity().getResources().getDrawable(drawableId));
+                }
+            }
+        }
+    }
+
+    private void removeSplashScreen(final boolean forceHideImmediately) {
+        cordova.getActivity().runOnUiThread(new Runnable() {
+            public void run() {
+        if (splashDialog != null && splashImageView != null && splashDialog.isShowing()) {//check for non-null splashImageView, see https://issues.apache.org/jira/browse/CB-12277
+                    final int fadeSplashScreenDuration = getFadeDuration();
+                    // CB-10692 If the plugin is being paused/destroyed, skip the fading and hide it immediately
+                    if (fadeSplashScreenDuration > 0 && forceHideImmediately == false) {
+                        AlphaAnimation fadeOut = new AlphaAnimation(1, 0);
+                        fadeOut.setInterpolator(new DecelerateInterpolator());
+                        fadeOut.setDuration(fadeSplashScreenDuration);
+
+                        splashImageView.setAnimation(fadeOut);
+                        splashImageView.startAnimation(fadeOut);
+
+                        fadeOut.setAnimationListener(new Animation.AnimationListener() {
+                            @Override
+                            public void onAnimationStart(Animation animation) {
+                                spinnerStop();
+                            }
+
+                            @Override
+                            public void onAnimationEnd(Animation animation) {
+                                if (splashDialog != null && splashImageView != null && splashDialog.isShowing()) {//check for non-null splashImageView, see https://issues.apache.org/jira/browse/CB-12277
+                                    splashDialog.dismiss();
+                                    splashDialog = null;
+                                    splashImageView = null;
+                                }
+                            }
+
+                            @Override
+                            public void onAnimationRepeat(Animation animation) {
+                            }
+                        });
+                    } else {
+                        spinnerStop();
+                        splashDialog.dismiss();
+                        splashDialog = null;
+                        splashImageView = null;
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Shows the splash screen over the full Activity
+     */
+    @SuppressWarnings("deprecation")
+    private void showSplashScreen(final boolean hideAfterDelay) {
+        final int splashscreenTime = preferences.getInteger("SplashScreenDelay", DEFAULT_SPLASHSCREEN_DURATION);
+        final int drawableId = getSplashId();
+
+        final int fadeSplashScreenDuration = getFadeDuration();
+        final int effectiveSplashDuration = Math.max(0, splashscreenTime - fadeSplashScreenDuration);
+
+        lastHideAfterDelay = hideAfterDelay;
+
+        // Prevent to show the splash dialog if the activity is in the process of finishing
+        if (cordova.getActivity().isFinishing()) {
+            return;
+        }
+        // If the splash dialog is showing don't try to show it again
+        if (splashDialog != null && splashDialog.isShowing()) {
+            return;
+        }
+        if (drawableId == 0 || (splashscreenTime <= 0 && hideAfterDelay)) {
+            return;
+        }
+
+        cordova.getActivity().runOnUiThread(new Runnable() {
+            public void run() {
+                // Get reference to display
+                Display display = cordova.getActivity().getWindowManager().getDefaultDisplay();
+                Context context = webView.getContext();
+
+                // Use an ImageView to render the image because of its flexible scaling options.
+                splashImageView = new ImageView(context);
+                splashImageView.setImageResource(drawableId);
+                LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+                splashImageView.setLayoutParams(layoutParams);
+
+                splashImageView.setMinimumHeight(display.getHeight());
+                splashImageView.setMinimumWidth(display.getWidth());
+
+                // TODO: Use the background color of the webView's parent instead of using the preference.
+                splashImageView.setBackgroundColor(preferences.getInteger("backgroundColor", Color.BLACK));
+
+                if (isMaintainAspectRatio()) {
+                    // CENTER_CROP scale mode is equivalent to CSS "background-size:cover"
+                    splashImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
+                }
+                else {
+                    // FIT_XY scales image non-uniformly to fit into image view.
+                    splashImageView.setScaleType(ImageView.ScaleType.FIT_XY);
+                }
+
+                // Create and show the dialog
+                splashDialog = new Dialog(context, android.R.style.Theme_Translucent_NoTitleBar);
+                // check to see if the splash screen should be full screen
+                if ((cordova.getActivity().getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN)
+                        == WindowManager.LayoutParams.FLAG_FULLSCREEN) {
+                    splashDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+                            WindowManager.LayoutParams.FLAG_FULLSCREEN);
+                }
+                splashDialog.setContentView(splashImageView);
+                splashDialog.setCancelable(false);
+                splashDialog.show();
+
+                if (preferences.getBoolean("ShowSplashScreenSpinner", true)) {
+                    spinnerStart();
+                }
+
+                // Set Runnable to remove splash screen just in case
+                if (hideAfterDelay) {
+                    final Handler handler = new Handler();
+                    handler.postDelayed(new Runnable() {
+                        public void run() {
+                            if (lastHideAfterDelay) {
+                                removeSplashScreen(false);
+                            }
+                        }
+                    }, effectiveSplashDuration);
+                }
+            }
+        });
+    }
+
+    // Show only spinner in the center of the screen
+    private void spinnerStart() {
+        cordova.getActivity().runOnUiThread(new Runnable() {
+            public void run() {
+                spinnerStop();
+
+                spinnerDialog = new ProgressDialog(webView.getContext());
+                spinnerDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+                    public void onCancel(DialogInterface dialog) {
+                        spinnerDialog = null;
+                    }
+                });
+
+                spinnerDialog.setCancelable(false);
+                spinnerDialog.setIndeterminate(true);
+
+                RelativeLayout centeredLayout = new RelativeLayout(cordova.getActivity());
+                centeredLayout.setGravity(Gravity.CENTER);
+                centeredLayout.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+
+                ProgressBar progressBar = new ProgressBar(webView.getContext());
+                RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+                layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
+                progressBar.setLayoutParams(layoutParams);
+
+                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+                    String colorName = preferences.getString("SplashScreenSpinnerColor", null);
+                    if(colorName != null){
+                        int[][] states = new int[][] {
+                            new int[] { android.R.attr.state_enabled}, // enabled
+                            new int[] {-android.R.attr.state_enabled}, // disabled
+                            new int[] {-android.R.attr.state_checked}, // unchecked
+                            new int[] { android.R.attr.state_pressed}  // pressed
+                        };
+                        int progressBarColor = Color.parseColor(colorName);
+                        int[] colors = new int[] {
+                            progressBarColor,
+                            progressBarColor,
+                            progressBarColor,
+                            progressBarColor
+                        };
+                        ColorStateList colorStateList = new ColorStateList(states, colors);
+                        progressBar.setIndeterminateTintList(colorStateList);
+                    }
+                }
+
+                centeredLayout.addView(progressBar);
+
+                spinnerDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+                spinnerDialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+
+                spinnerDialog.show();
+                spinnerDialog.setContentView(centeredLayout);
+            }
+        });
+    }
+
+    private void spinnerStop() {
+        cordova.getActivity().runOnUiThread(new Runnable() {
+            public void run() {
+                if (spinnerDialog != null && spinnerDialog.isShowing()) {
+                    spinnerDialog.dismiss();
+                    spinnerDialog = null;
+                }
+            }
+        });
+    }
+}

BIN
cordova/platforms/android/app/src/main/res/drawable-land-hdpi/screen.png


BIN
cordova/platforms/android/app/src/main/res/drawable-land-ldpi/screen.png


BIN
cordova/platforms/android/app/src/main/res/drawable-land-mdpi/screen.png


BIN
cordova/platforms/android/app/src/main/res/drawable-land-xhdpi/screen.png


BIN
cordova/platforms/android/app/src/main/res/drawable-land-xxhdpi/screen.png


BIN
cordova/platforms/android/app/src/main/res/drawable-land-xxxhdpi/screen.png


BIN
cordova/platforms/android/app/src/main/res/drawable-port-hdpi/screen.png


BIN
cordova/platforms/android/app/src/main/res/drawable-port-ldpi/screen.png


BIN
cordova/platforms/android/app/src/main/res/drawable-port-mdpi/screen.png


BIN
cordova/platforms/android/app/src/main/res/drawable-port-xhdpi/screen.png


BIN
cordova/platforms/android/app/src/main/res/drawable-port-xxhdpi/screen.png


BIN
cordova/platforms/android/app/src/main/res/drawable-port-xxxhdpi/screen.png


+ 27 - 1
cordova/platforms/android/app/src/main/res/xml/config.xml

@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='utf-8'?>
-<widget id="com.shotshock.twong" version="1.2.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+<widget id="com.shotshock.twong" version="1.2.8" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
     <feature name="Whitelist">
         <param name="android-package" value="org.apache.cordova.whitelist.WhitelistPlugin" />
         <param name="onload" value="true" />
@@ -36,6 +36,21 @@
         <param name="android-package" value="io.ionic.keyboard.IonicKeyboard" />
         <param name="onload" value="true" />
     </feature>
+    <feature name="File">
+        <param name="android-package" value="org.apache.cordova.file.FileUtils" />
+        <param name="onload" value="true" />
+    </feature>
+    <allow-navigation href="cdvfile:*" />
+    <feature name="FileTransfer">
+        <param name="android-package" value="org.apache.cordova.filetransfer.FileTransfer" />
+    </feature>
+    <feature name="FileOpener2">
+        <param name="android-package" value="io.github.pwlin.cordova.plugins.fileopener2.FileOpener2" />
+    </feature>
+    <feature name="SplashScreen">
+        <param name="android-package" value="org.apache.cordova.splashscreen.SplashScreen" />
+        <param name="onload" value="true" />
+    </feature>
     <name>美天旺</name>
     <description>
         美天旺是完璧时空旗下新型垂直电商App,专注于新经济下的购物服务!
@@ -57,11 +72,22 @@
     <icon density="xhdpi" src="res/android/mipmap-xhdpi/ic_launcher.png" />
     <icon density="xxhdpi" src="res/android/mipmap-xxhdpi/ic_launcher.png" />
     <icon density="xxxhdpi" src="res/android/mipmap-xxxhdpi/ic_launcher.png" />
+    <splash density="port-mdpi" src="res/screen/android/splash-port-mdpi.png" />
+    <splash density="port-hdpi" src="res/screen/android/splash-port-hdpi.png" />
+    <splash density="port-xhdpi" src="res/screen/android/splash-port-xhdpi.png" />
+    <splash density="port-xxhdpi" src="res/screen/android/splash-port-xxhdpi.png" />
+    <splash density="port-xxxhdpi" src="res/screen/android/splash-port-xxxhdpi.png" />
     <preference name="loglevel" value="DEBUG" />
     <preference name="WECHATAPPID" value="wx1bb4342986c22b28" />
     <preference name="Fullscreen" value="true" />
     <preference name="WebViewBounce" value="false" />
     <preference name="DisallowOverscroll" value="true" />
+    <preference name="FadeSplashScreen" value="false" />
+    <preference name="ShowSplashScreenSpinner" value="false" />
+    <preference name="SplashMaintainAspectRatio" value="true" />
+    <preference name="SplashScreenSpinnerColor" value="white" />
     <preference name="CodePushDeploymentKey" value="YOUR-ANDROID-DEPLOYMENT-KEY" />
     <preference name="CodePushPublicKey" value="YOUR-PUBLIC-KEY" />
+    <preference name="AndroidPersistentFileLocation" value="Compatibility" />
+    <preference name="SplashScreenDelay" value="3000" />
 </widget>

+ 14 - 0
cordova/platforms/android/app/src/main/res/xml/opener_paths.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- https://developer.android.com/reference/android/support/v4/content/FileProvider.html#SpecifyFiles -->
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- cordova.file.dataDirectory -->
+    <files-path name="files" path="." />
+    <!-- cordova.file.cacheDirectory -->
+    <cache-path name="cache" path="." />
+    <!-- cordova.file.externalDataDirectory -->
+    <external-files-path name="external-files" path="." />
+    <!-- cordova.file.externalCacheDirectory -->
+    <external-cache-path name="external-cache" path="." />
+    <!-- cordova.file.externalRootDirectory -->
+    <external-path name="external" path="." />
+</paths>

+ 210 - 1
cordova/platforms/android/platform_www/cordova_plugins.js

@@ -80,6 +80,211 @@ cordova.define('cordova/plugin_list', function(require, exports, module) {
         "cordova.plugins.Keyboard"
       ],
       "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.DirectoryEntry",
+      "file": "plugins/cordova-plugin-file/www/DirectoryEntry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.DirectoryEntry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.DirectoryReader",
+      "file": "plugins/cordova-plugin-file/www/DirectoryReader.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.DirectoryReader"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Entry",
+      "file": "plugins/cordova-plugin-file/www/Entry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Entry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.File",
+      "file": "plugins/cordova-plugin-file/www/File.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.File"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileEntry",
+      "file": "plugins/cordova-plugin-file/www/FileEntry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileEntry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileError",
+      "file": "plugins/cordova-plugin-file/www/FileError.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileError"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileReader",
+      "file": "plugins/cordova-plugin-file/www/FileReader.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileReader"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileSystem",
+      "file": "plugins/cordova-plugin-file/www/FileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileUploadOptions",
+      "file": "plugins/cordova-plugin-file/www/FileUploadOptions.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileUploadOptions"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileUploadResult",
+      "file": "plugins/cordova-plugin-file/www/FileUploadResult.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileUploadResult"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileWriter",
+      "file": "plugins/cordova-plugin-file/www/FileWriter.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileWriter"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Flags",
+      "file": "plugins/cordova-plugin-file/www/Flags.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Flags"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.LocalFileSystem",
+      "file": "plugins/cordova-plugin-file/www/LocalFileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.LocalFileSystem"
+      ],
+      "merges": [
+        "window"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Metadata",
+      "file": "plugins/cordova-plugin-file/www/Metadata.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Metadata"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.ProgressEvent",
+      "file": "plugins/cordova-plugin-file/www/ProgressEvent.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.ProgressEvent"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.fileSystems",
+      "file": "plugins/cordova-plugin-file/www/fileSystems.js",
+      "pluginId": "cordova-plugin-file"
+    },
+    {
+      "id": "cordova-plugin-file.requestFileSystem",
+      "file": "plugins/cordova-plugin-file/www/requestFileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.requestFileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.resolveLocalFileSystemURI",
+      "file": "plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "window"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.isChrome",
+      "file": "plugins/cordova-plugin-file/www/browser/isChrome.js",
+      "pluginId": "cordova-plugin-file",
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.androidFileSystem",
+      "file": "plugins/cordova-plugin-file/www/android/FileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "FileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.fileSystems-roots",
+      "file": "plugins/cordova-plugin-file/www/fileSystems-roots.js",
+      "pluginId": "cordova-plugin-file",
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.fileSystemPaths",
+      "file": "plugins/cordova-plugin-file/www/fileSystemPaths.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "cordova"
+      ],
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file-transfer.FileTransferError",
+      "file": "plugins/cordova-plugin-file-transfer/www/FileTransferError.js",
+      "pluginId": "cordova-plugin-file-transfer",
+      "clobbers": [
+        "window.FileTransferError"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file-transfer.FileTransfer",
+      "file": "plugins/cordova-plugin-file-transfer/www/FileTransfer.js",
+      "pluginId": "cordova-plugin-file-transfer",
+      "clobbers": [
+        "window.FileTransfer"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file-opener2.FileOpener2",
+      "file": "plugins/cordova-plugin-file-opener2/www/plugins.FileOpener2.js",
+      "pluginId": "cordova-plugin-file-opener2",
+      "clobbers": [
+        "cordova.plugins.fileOpener2"
+      ]
+    },
+    {
+      "id": "cordova-plugin-splashscreen.SplashScreen",
+      "file": "plugins/cordova-plugin-splashscreen/www/splashscreen.js",
+      "pluginId": "cordova-plugin-splashscreen",
+      "clobbers": [
+        "navigator.splashscreen"
+      ]
     }
   ];
   module.exports.metadata = {
@@ -95,6 +300,10 @@ cordova.define('cordova/plugin_list', function(require, exports, module) {
     "cordova-plugin-app-version": "0.1.9",
     "cordova-plugin-console": "1.1.0",
     "cordova-plugin-alipay-v2": "2.0.0",
-    "ionic-plugin-keyboard": "2.2.1"
+    "ionic-plugin-keyboard": "2.2.1",
+    "cordova-plugin-file": "6.0.2",
+    "cordova-plugin-file-transfer": "1.7.1",
+    "cordova-plugin-file-opener2": "3.0.5",
+    "cordova-plugin-splashscreen": "6.0.0"
   };
 });

+ 2 - 1
cordova/platforms/android/project.properties

@@ -13,4 +13,5 @@ android.library.reference.1=CordovaLib
 android.library.reference.2=app
 cordova.gradle.include.1=cordova-plugin-wechat/twong-android-build.gradle
 cordova.gradle.include.2=cordova-plugin-alipay-v2/twong-alipay.gradle
-cordova.gradle.include.3=cordova-plugin-alipay-v2/twong-alipay.gradle
+cordova.gradle.include.3=cordova-plugin-alipay-v2/twong-alipay.gradle
+cordova.system.library.1=com.android.support:support-v4:27.+

+ 2 - 2
cordova/platforms/ios/CordovaLib/CordovaLib.xcodeproj/xcuserdata/x.xcuserdatad/xcschemes/xcschememanagement.plist

@@ -7,12 +7,12 @@
 		<key>Cordova.xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>1</integer>
+			<integer>2</integer>
 		</dict>
 		<key>CordovaLib.xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>2</integer>
+			<integer>1</integer>
 		</dict>
 	</dict>
 </dict>

+ 226 - 1
cordova/platforms/ios/ios.json

@@ -95,6 +95,18 @@
             {
               "xml": "<feature name=\"Keyboard\"><param name=\"ios-package\" onload=\"true\" value=\"IonicKeyboard\" /></feature>",
               "count": 1
+            },
+            {
+              "xml": "<feature name=\"File\"><param name=\"ios-package\" value=\"CDVFile\" /><param name=\"onload\" value=\"true\" /></feature>",
+              "count": 1
+            },
+            {
+              "xml": "<feature name=\"FileTransfer\"><param name=\"ios-package\" value=\"CDVFileTransfer\" /></feature>",
+              "count": 1
+            },
+            {
+              "xml": "<feature name=\"FileOpener2\"><param name=\"ios-package\" value=\"FileOpener2\" /></feature>",
+              "count": 1
             }
           ],
           "/widget": []
@@ -174,6 +186,18 @@
     },
     "ionic-plugin-keyboard": {
       "PACKAGE_NAME": "com.shotshock.twong"
+    },
+    "cordova-plugin-file": {
+      "PACKAGE_NAME": "com.shotshock.twong"
+    },
+    "cordova-plugin-file-transfer": {
+      "PACKAGE_NAME": "com.shotshock.twong"
+    },
+    "cordova-plugin-file-opener2": {
+      "PACKAGE_NAME": "com.shotshock.twong"
+    },
+    "cordova-plugin-splashscreen": {
+      "PACKAGE_NAME": "com.shotshock.twong"
     }
   },
   "dependent_plugins": {},
@@ -258,6 +282,203 @@
         "cordova.plugins.Keyboard"
       ],
       "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.DirectoryEntry",
+      "file": "plugins/cordova-plugin-file/www/DirectoryEntry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.DirectoryEntry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.DirectoryReader",
+      "file": "plugins/cordova-plugin-file/www/DirectoryReader.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.DirectoryReader"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Entry",
+      "file": "plugins/cordova-plugin-file/www/Entry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Entry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.File",
+      "file": "plugins/cordova-plugin-file/www/File.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.File"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileEntry",
+      "file": "plugins/cordova-plugin-file/www/FileEntry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileEntry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileError",
+      "file": "plugins/cordova-plugin-file/www/FileError.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileError"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileReader",
+      "file": "plugins/cordova-plugin-file/www/FileReader.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileReader"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileSystem",
+      "file": "plugins/cordova-plugin-file/www/FileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileUploadOptions",
+      "file": "plugins/cordova-plugin-file/www/FileUploadOptions.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileUploadOptions"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileUploadResult",
+      "file": "plugins/cordova-plugin-file/www/FileUploadResult.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileUploadResult"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileWriter",
+      "file": "plugins/cordova-plugin-file/www/FileWriter.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileWriter"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Flags",
+      "file": "plugins/cordova-plugin-file/www/Flags.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Flags"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.LocalFileSystem",
+      "file": "plugins/cordova-plugin-file/www/LocalFileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.LocalFileSystem"
+      ],
+      "merges": [
+        "window"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Metadata",
+      "file": "plugins/cordova-plugin-file/www/Metadata.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Metadata"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.ProgressEvent",
+      "file": "plugins/cordova-plugin-file/www/ProgressEvent.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.ProgressEvent"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.fileSystems",
+      "file": "plugins/cordova-plugin-file/www/fileSystems.js",
+      "pluginId": "cordova-plugin-file"
+    },
+    {
+      "id": "cordova-plugin-file.requestFileSystem",
+      "file": "plugins/cordova-plugin-file/www/requestFileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.requestFileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.resolveLocalFileSystemURI",
+      "file": "plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "window"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.isChrome",
+      "file": "plugins/cordova-plugin-file/www/browser/isChrome.js",
+      "pluginId": "cordova-plugin-file",
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.iosFileSystem",
+      "file": "plugins/cordova-plugin-file/www/ios/FileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "FileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.fileSystems-roots",
+      "file": "plugins/cordova-plugin-file/www/fileSystems-roots.js",
+      "pluginId": "cordova-plugin-file",
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.fileSystemPaths",
+      "file": "plugins/cordova-plugin-file/www/fileSystemPaths.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "cordova"
+      ],
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file-transfer.FileTransferError",
+      "file": "plugins/cordova-plugin-file-transfer/www/FileTransferError.js",
+      "pluginId": "cordova-plugin-file-transfer",
+      "clobbers": [
+        "window.FileTransferError"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file-transfer.FileTransfer",
+      "file": "plugins/cordova-plugin-file-transfer/www/FileTransfer.js",
+      "pluginId": "cordova-plugin-file-transfer",
+      "clobbers": [
+        "window.FileTransfer"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file-opener2.FileOpener2",
+      "file": "plugins/cordova-plugin-file-opener2/www/plugins.FileOpener2.js",
+      "pluginId": "cordova-plugin-file-opener2",
+      "clobbers": [
+        "cordova.plugins.fileOpener2"
+      ]
     }
   ],
   "plugin_metadata": {
@@ -272,6 +493,10 @@
     "cordova-plugin-inappbrowser": "4.0.0",
     "cordova-plugin-app-version": "0.1.9",
     "cordova-plugin-alipay-v2": "2.0.0",
-    "ionic-plugin-keyboard": "2.2.1"
+    "ionic-plugin-keyboard": "2.2.1",
+    "cordova-plugin-file": "6.0.2",
+    "cordova-plugin-file-transfer": "1.7.1",
+    "cordova-plugin-file-opener2": "3.0.5",
+    "cordova-plugin-splashscreen": "6.0.0"
   }
 }

+ 202 - 1
cordova/platforms/ios/platform_www/cordova_plugins.js

@@ -80,6 +80,203 @@ cordova.define('cordova/plugin_list', function(require, exports, module) {
         "cordova.plugins.Keyboard"
       ],
       "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.DirectoryEntry",
+      "file": "plugins/cordova-plugin-file/www/DirectoryEntry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.DirectoryEntry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.DirectoryReader",
+      "file": "plugins/cordova-plugin-file/www/DirectoryReader.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.DirectoryReader"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Entry",
+      "file": "plugins/cordova-plugin-file/www/Entry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Entry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.File",
+      "file": "plugins/cordova-plugin-file/www/File.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.File"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileEntry",
+      "file": "plugins/cordova-plugin-file/www/FileEntry.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileEntry"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileError",
+      "file": "plugins/cordova-plugin-file/www/FileError.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileError"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileReader",
+      "file": "plugins/cordova-plugin-file/www/FileReader.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileReader"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileSystem",
+      "file": "plugins/cordova-plugin-file/www/FileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileUploadOptions",
+      "file": "plugins/cordova-plugin-file/www/FileUploadOptions.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileUploadOptions"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileUploadResult",
+      "file": "plugins/cordova-plugin-file/www/FileUploadResult.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileUploadResult"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.FileWriter",
+      "file": "plugins/cordova-plugin-file/www/FileWriter.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.FileWriter"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Flags",
+      "file": "plugins/cordova-plugin-file/www/Flags.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Flags"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.LocalFileSystem",
+      "file": "plugins/cordova-plugin-file/www/LocalFileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.LocalFileSystem"
+      ],
+      "merges": [
+        "window"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.Metadata",
+      "file": "plugins/cordova-plugin-file/www/Metadata.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.Metadata"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.ProgressEvent",
+      "file": "plugins/cordova-plugin-file/www/ProgressEvent.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.ProgressEvent"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.fileSystems",
+      "file": "plugins/cordova-plugin-file/www/fileSystems.js",
+      "pluginId": "cordova-plugin-file"
+    },
+    {
+      "id": "cordova-plugin-file.requestFileSystem",
+      "file": "plugins/cordova-plugin-file/www/requestFileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "clobbers": [
+        "window.requestFileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.resolveLocalFileSystemURI",
+      "file": "plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "window"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.isChrome",
+      "file": "plugins/cordova-plugin-file/www/browser/isChrome.js",
+      "pluginId": "cordova-plugin-file",
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.iosFileSystem",
+      "file": "plugins/cordova-plugin-file/www/ios/FileSystem.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "FileSystem"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file.fileSystems-roots",
+      "file": "plugins/cordova-plugin-file/www/fileSystems-roots.js",
+      "pluginId": "cordova-plugin-file",
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file.fileSystemPaths",
+      "file": "plugins/cordova-plugin-file/www/fileSystemPaths.js",
+      "pluginId": "cordova-plugin-file",
+      "merges": [
+        "cordova"
+      ],
+      "runs": true
+    },
+    {
+      "id": "cordova-plugin-file-transfer.FileTransferError",
+      "file": "plugins/cordova-plugin-file-transfer/www/FileTransferError.js",
+      "pluginId": "cordova-plugin-file-transfer",
+      "clobbers": [
+        "window.FileTransferError"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file-transfer.FileTransfer",
+      "file": "plugins/cordova-plugin-file-transfer/www/FileTransfer.js",
+      "pluginId": "cordova-plugin-file-transfer",
+      "clobbers": [
+        "window.FileTransfer"
+      ]
+    },
+    {
+      "id": "cordova-plugin-file-opener2.FileOpener2",
+      "file": "plugins/cordova-plugin-file-opener2/www/plugins.FileOpener2.js",
+      "pluginId": "cordova-plugin-file-opener2",
+      "clobbers": [
+        "cordova.plugins.fileOpener2"
+      ]
     }
   ];
   module.exports.metadata = {
@@ -94,6 +291,10 @@ cordova.define('cordova/plugin_list', function(require, exports, module) {
     "cordova-plugin-inappbrowser": "4.0.0",
     "cordova-plugin-app-version": "0.1.9",
     "cordova-plugin-alipay-v2": "2.0.0",
-    "ionic-plugin-keyboard": "2.2.1"
+    "ionic-plugin-keyboard": "2.2.1",
+    "cordova-plugin-file": "6.0.2",
+    "cordova-plugin-file-transfer": "1.7.1",
+    "cordova-plugin-file-opener2": "3.0.5",
+    "cordova-plugin-splashscreen": "6.0.0"
   };
 });

+ 3 - 3
cordova/platforms/ios/美天旺.xcarchive/Info.plist

@@ -13,9 +13,9 @@
 		<key>CFBundleIdentifier</key>
 		<string>com.shotshock.twong</string>
 		<key>CFBundleShortVersionString</key>
-		<string>1.2.1</string>
+		<string>1.2.0</string>
 		<key>CFBundleVersion</key>
-		<string>1.2.1</string>
+		<string>1.2.0</string>
 		<key>SigningIdentity</key>
 		<string>Apple Development: starlove_xing@126.com (FK35H3KKHX)</string>
 		<key>Team</key>
@@ -24,7 +24,7 @@
 	<key>ArchiveVersion</key>
 	<integer>2</integer>
 	<key>CreationDate</key>
-	<date>2020-11-06T01:42:54Z</date>
+	<date>2020-11-09T21:26:39Z</date>
 	<key>Name</key>
 	<string>美天旺</string>
 	<key>SchemeName</key>

BIN
cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/Assets.car


BIN
cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/CDVLaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib/objects-13.0+.nib


BIN
cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/CDVLaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib/runtime.nib


BIN
cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/CDVLaunchScreen.storyboardc/UIViewController-01J-lp-oVM.nib/objects-13.0+.nib


BIN
cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/CDVLaunchScreen.storyboardc/UIViewController-01J-lp-oVM.nib/runtime.nib


BIN
cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/Info.plist


BIN
cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/JPushConfig.plist


BIN
cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/MainViewController.nib


File diff suppressed because it is too large
+ 322 - 606
cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/_CodeSignature/CodeResources


+ 18 - 1
cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/config.xml

@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='utf-8'?>
-<widget id="com.shotshock.twong" version="1.2.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+<widget id="com.shotshock.twong" version="1.2.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
     <feature name="CDVWebViewEngine">
         <param name="ios-package" value="CDVWebViewEngine" />
     </feature>
@@ -52,6 +52,16 @@
     <feature name="Keyboard">
         <param name="ios-package" onload="true" value="IonicKeyboard" />
     </feature>
+    <feature name="File">
+        <param name="ios-package" value="CDVFile" />
+        <param name="onload" value="true" />
+    </feature>
+    <feature name="FileTransfer">
+        <param name="ios-package" value="CDVFileTransfer" />
+    </feature>
+    <feature name="FileOpener2">
+        <param name="ios-package" value="FileOpener2" />
+    </feature>
     <name>美天旺</name>
     <description>
         美天旺是完璧时空旗下新型垂直电商App,专注于新经济下的购物服务!
@@ -85,6 +95,8 @@
     <icon height="50" src="res/ios/icon-50.png" width="50" />
     <icon height="100" src="res/ios/icon-50@2x.png" width="100" />
     <icon height="1024" src="res/ios/ios-marketing-1024x1024" width="024" />
+    <splash src="res/screen/ios/Default@2x~universal~comany.png" />
+    <splash src="res/screen/ios/Default@3x~universal~comany.png" />
     <preference name="AllowInlineMediaPlayback" value="false" />
     <preference name="BackupWebStorage" value="cloud" />
     <preference name="DisallowOverscroll" value="true" />
@@ -105,6 +117,11 @@
     <preference name="alipayid" value="2021001196660263" />
     <preference name="Fullscreen" value="true" />
     <preference name="WebViewBounce" value="false" />
+    <preference name="FadeSplashScreen" value="false" />
+    <preference name="SplashScreenDelay" value="2500" />
+    <preference name="ShowSplashScreenSpinner" value="false" />
+    <preference name="SplashMaintainAspectRatio" value="true" />
+    <preference name="SplashScreenSpinnerColor" value="white" />
     <preference name="CodePushDeploymentKey" value="YOUR-ANDROID-DEPLOYMENT-KEY" />
     <preference name="CodePushPublicKey" value="YOUR-PUBLIC-KEY" />
 </widget>

BIN
cordova/platforms/ios/美天旺.xcarchive/Products/Applications/美天旺.app/美天旺


+ 38 - 4
cordova/platforms/ios/美天旺.xcodeproj/project.pbxproj

@@ -9,11 +9,13 @@
 /* Begin PBXBuildFile section */
 		0207DA581B56EA530066E2B4 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0207DA571B56EA530066E2B4 /* Images.xcassets */; };
 		0605FDF6853B470980FFEBA3 /* JPushPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 647EEB681F2B4D9C9ABF0B52 /* JPushPlugin.m */; };
+		06199B718EEC4FAFA96ED8F8 /* CDVFile.m in Sources */ = {isa = PBXBuildFile; fileRef = 2F5592E635BA4B38A7349FBF /* CDVFile.m */; };
 		0D1C93094CB14F31A16602DA /* CDVDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = 2F2CF5B35E1241B5A04D0C90 /* CDVDevice.m */; };
 		12C1234B9BB1451FBAF612BD /* libc++.1.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 8FBA2A7B061A4DCDAAA61966 /* libc++.1.tbd */; };
 		15802058D28942759398B4B4 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 969C78309FFB44E4BF5ED50A /* libresolv.tbd */; settings = {ATTRIBUTES = (Weak, ); }; };
 		16013A59C2154F7299D1E129 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B4A01EE6333649AFBFE0DF8F /* libc++.tbd */; settings = {ATTRIBUTES = (Weak, ); }; };
 		16DAD1B7EFB747E9AEA04AA1 /* alipay.m in Sources */ = {isa = PBXBuildFile; fileRef = C0F1B0B369134AA686D5331E /* alipay.m */; };
+		17E91745255D33D4000105DB /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17E91744255D33D4000105DB /* CloudKit.framework */; };
 		1D3623260D0F684500981E51 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D3623250D0F684500981E51 /* AppDelegate.m */; };
 		1D60589B0D05DD56006BFB54 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; };
 		1E2CA60A2CD54E529DFAF873 /* AppDelegate+Wechat.m in Sources */ = {isa = PBXBuildFile; fileRef = 92A41B443C4342F7BB0C317D /* AppDelegate+Wechat.m */; };
@@ -36,6 +38,7 @@
 		77F79F09A544472199CA5D57 /* CDVWechat.m in Sources */ = {isa = PBXBuildFile; fileRef = B6C1854537EF436EAFDADD32 /* CDVWechat.m */; };
 		8206F7D009A5445E8E49CA7D /* jpush-ios-3.3.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 83E675356510447DB7B0170E /* jpush-ios-3.3.3.a */; };
 		8905C3C90B2E42E78C4A15A2 /* CoreTelephony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13554308EDDB46F79B5A6259 /* CoreTelephony.framework */; };
+		8BEB6D7270204EE8AE3AFCD8 /* FileOpener2.m in Sources */ = {isa = PBXBuildFile; fileRef = 64A41B747404472594549BD5 /* FileOpener2.m */; };
 		9BAFF61F668E485DAC7782DE /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE552BFA359B4F54BB880E3E /* UIKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 		B2228A798D5E4B8EAED86FFC /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 458ECC895DAA48358D3BF864 /* CoreText.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 		B4FDE53EE5E0454FB8739856 /* libWeChatSDK.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 61A9D3CA2D8149228414BCDE /* libWeChatSDK.a */; };
@@ -44,12 +47,15 @@
 		D2B912B03F834BCAB152AAB1 /* jcore-ios-2.2.5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C02AD31B9F014104B663149E /* jcore-ios-2.2.5.a */; };
 		D38FD69CA9BD4D698F0BF808 /* AdSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 821CABB37F3A43F2940D54A2 /* AdSupport.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 		DCE8FA8B87564EDF839412A6 /* AppVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E3FC7ADB4F047949DBB8F41 /* AppVersion.m */; };
+		E0DC52D23D254CD39C4B60F9 /* CDVAssetLibraryFilesystem.m in Sources */ = {isa = PBXBuildFile; fileRef = F7B5771C31244DE18350BC13 /* CDVAssetLibraryFilesystem.m */; };
+		E1015F64ECBC49C9AEC26A2F /* CDVLocalFilesystem.m in Sources */ = {isa = PBXBuildFile; fileRef = D5EFC89EB8D145AA8AE57FD0 /* CDVLocalFilesystem.m */; };
 		E2E2740B8D984349A5861FBD /* Webkit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF45E0A2D2BA4795992C9163 /* Webkit.framework */; };
 		E3AA3AE40CB9448FB46EDB8F /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = AD7315985BE04AC2826B9DA8 /* libz.tbd */; };
 		EA6AA10E1AFD4211849F35A4 /* libsqlite3.0.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 6635FB5F08DC4046AE1611BB /* libsqlite3.0.tbd */; };
 		F7831C831CEA47C6B24FEFB3 /* CDVWKInAppBrowserUIDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AAA173FF03CD420AA6B6E317 /* CDVWKInAppBrowserUIDelegate.m */; };
 		F99BB104CEE14FD7AB13287C /* AlipaySDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE1ADDD030C744928459F422 /* AlipaySDK.framework */; };
 		F9E8BE8305AC4AB680685064 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 44B1BC210AAA46D4A8D82B6B /* QuartzCore.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
+		FA5CEEE37F1B4B598CE0176A /* CDVFileTransfer.m in Sources */ = {isa = PBXBuildFile; fileRef = 724C827D973F457BB7B48C38 /* CDVFileTransfer.m */; };
 		FEB9EE0A34BB4EA9977C8B47 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CAADBE3351646FBAF9D4CE3 /* CoreFoundation.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 /* End PBXBuildFile section */
 
@@ -86,6 +92,7 @@
 		0CF582423AFF458AB62AD327 /* Security.framework */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
 		12018477745242459E68F994 /* WechatAuthSDK.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = WechatAuthSDK.h; path = "cordova-plugin-wechat/WechatAuthSDK.h"; sourceTree = "<group>"; };
 		13554308EDDB46F79B5A6259 /* CoreTelephony.framework */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; };
+		17E91744255D33D4000105DB /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
 		1A60CECDEA38432EA27D7A03 /* AppDelegate+Wechat.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "AppDelegate+Wechat.h"; path = "cordova-plugin-wechat/AppDelegate+Wechat.h"; sourceTree = "<group>"; };
 		1D3623240D0F684500981E51 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
 		1D3623250D0F684500981E51 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
@@ -95,6 +102,7 @@
 		2B766F20F1044FCE8CD83901 /* CDVWKInAppBrowser.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = CDVWKInAppBrowser.m; path = "cordova-plugin-inappbrowser/CDVWKInAppBrowser.m"; sourceTree = "<group>"; };
 		2CAABF9D07A44D9C9B629614 /* WXApiObject.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = WXApiObject.h; path = "cordova-plugin-wechat/WXApiObject.h"; sourceTree = "<group>"; };
 		2F2CF5B35E1241B5A04D0C90 /* CDVDevice.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = CDVDevice.m; path = "cordova-plugin-device/CDVDevice.m"; sourceTree = "<group>"; };
+		2F5592E635BA4B38A7349FBF /* CDVFile.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = CDVFile.m; path = "cordova-plugin-file/CDVFile.m"; sourceTree = "<group>"; };
 		301BF52D109A57CC0062928A /* CordovaLib.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = CordovaLib.xcodeproj; path = CordovaLib/CordovaLib.xcodeproj; sourceTree = "<group>"; };
 		301BF56E109A69640062928A /* www */ = {isa = PBXFileReference; lastKnownFileType = folder; path = www; sourceTree = SOURCE_ROOT; };
 		302D95EE14D2391D003F00A1 /* MainViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainViewController.h; sourceTree = "<group>"; };
@@ -113,21 +121,25 @@
 		4E3FC7ADB4F047949DBB8F41 /* AppVersion.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = AppVersion.m; path = "cordova-plugin-app-version/AppVersion.m"; sourceTree = "<group>"; };
 		61A9D3CA2D8149228414BCDE /* libWeChatSDK.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; name = libWeChatSDK.a; path = "美天旺/Plugins/cordova-plugin-wechat/libWeChatSDK.a"; sourceTree = "<group>"; };
 		647EEB681F2B4D9C9ABF0B52 /* JPushPlugin.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = JPushPlugin.m; path = "jpush-phonegap-plugin/JPushPlugin.m"; sourceTree = "<group>"; };
+		64A41B747404472594549BD5 /* FileOpener2.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = FileOpener2.m; path = "cordova-plugin-file-opener2/FileOpener2.m"; sourceTree = "<group>"; };
 		6635FB5F08DC4046AE1611BB /* libsqlite3.0.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.0.tbd; path = usr/lib/libsqlite3.0.tbd; sourceTree = SDKROOT; };
 		6677DA15BD1C4205AF98147D /* JPushPlugin.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = JPushPlugin.h; path = "jpush-phonegap-plugin/JPushPlugin.h"; sourceTree = "<group>"; };
 		6AFF5BF81D6E424B00AB3073 /* CDVLaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = CDVLaunchScreen.storyboard; path = "美天旺/CDVLaunchScreen.storyboard"; sourceTree = SOURCE_ROOT; };
 		6B7A770BF7304A88BAA712C7 /* CDVInAppBrowserOptions.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = CDVInAppBrowserOptions.m; path = "cordova-plugin-inappbrowser/CDVInAppBrowserOptions.m"; sourceTree = "<group>"; };
+		724C827D973F457BB7B48C38 /* CDVFileTransfer.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = CDVFileTransfer.m; path = "cordova-plugin-file-transfer/CDVFileTransfer.m"; sourceTree = "<group>"; };
 		821CABB37F3A43F2940D54A2 /* AdSupport.framework */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; };
 		83E675356510447DB7B0170E /* jpush-ios-3.3.3.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; name = "jpush-ios-3.3.3.a"; path = "美天旺/Plugins/jpush-phonegap-plugin/jpush-ios-3.3.3.a"; sourceTree = "<group>"; };
 		88032390E7F24525876E7EA5 /* CDVStatusBar.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = CDVStatusBar.m; path = "cordova-plugin-statusbar/CDVStatusBar.m"; sourceTree = "<group>"; };
 		8A26553BDD9A498C87F0051F /* Foundation.framework */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
 		8AF854D7C96D4CBABF731728 /* AppDelegate+JPush.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "AppDelegate+JPush.h"; path = "jpush-phonegap-plugin/AppDelegate+JPush.h"; sourceTree = "<group>"; };
 		8D1107310486CEB800E47090 /* 美天旺-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "美天旺-Info.plist"; path = "美天旺/美天旺-Info.plist"; plistStructureDefinitionIdentifier = "com.apple.xcode.plist.structure-definition.iphone.info-plist"; sourceTree = SOURCE_ROOT; };
+		8D3014967CC54B66B205E26C /* FileOpener2.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = FileOpener2.h; path = "cordova-plugin-file-opener2/FileOpener2.h"; sourceTree = "<group>"; };
 		8FBA2A7B061A4DCDAAA61966 /* libc++.1.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.1.tbd"; path = "usr/lib/libc++.1.tbd"; sourceTree = SDKROOT; };
 		916F9573A4C84445B818B3F6 /* CDVWKInAppBrowser.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = CDVWKInAppBrowser.h; path = "cordova-plugin-inappbrowser/CDVWKInAppBrowser.h"; sourceTree = "<group>"; };
 		92A41B443C4342F7BB0C317D /* AppDelegate+Wechat.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = "AppDelegate+Wechat.m"; path = "cordova-plugin-wechat/AppDelegate+Wechat.m"; sourceTree = "<group>"; };
 		94D1F885CDDD4DBB898B535D /* JPushConfig.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; path = JPushConfig.plist; sourceTree = "<group>"; };
 		969C78309FFB44E4BF5ED50A /* libresolv.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
+		9CE06BF0A07A4C06BFF8AC19 /* CDVAssetLibraryFilesystem.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = CDVAssetLibraryFilesystem.h; path = "cordova-plugin-file/CDVAssetLibraryFilesystem.h"; sourceTree = "<group>"; };
 		A4F8D551733F431C9467CF50 /* CDVInAppBrowserNavigationController.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = CDVInAppBrowserNavigationController.m; path = "cordova-plugin-inappbrowser/CDVInAppBrowserNavigationController.m"; sourceTree = "<group>"; };
 		A5AE479895054E5F88561AB5 /* CDVWKInAppBrowserUIDelegate.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = CDVWKInAppBrowserUIDelegate.h; path = "cordova-plugin-inappbrowser/CDVWKInAppBrowserUIDelegate.h"; sourceTree = "<group>"; };
 		A64B0F17C450427DAB86A53A /* AppDelegate+JPush.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = "AppDelegate+JPush.m"; path = "jpush-phonegap-plugin/AppDelegate+JPush.m"; sourceTree = "<group>"; };
@@ -142,8 +154,12 @@
 		B6C1854537EF436EAFDADD32 /* CDVWechat.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = CDVWechat.m; path = "cordova-plugin-wechat/CDVWechat.m"; sourceTree = "<group>"; };
 		C02AD31B9F014104B663149E /* jcore-ios-2.2.5.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; name = "jcore-ios-2.2.5.a"; path = "美天旺/Plugins/cordova-plugin-jcore/jcore-ios-2.2.5.a"; sourceTree = "<group>"; };
 		C0F1B0B369134AA686D5331E /* alipay.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = alipay.m; path = "cordova-plugin-alipay-v2/alipay.m"; sourceTree = "<group>"; };
+		CC22DA8032F048EE9A9AE537 /* CDVFileTransfer.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = CDVFileTransfer.h; path = "cordova-plugin-file-transfer/CDVFileTransfer.h"; sourceTree = "<group>"; };
 		CE552BFA359B4F54BB880E3E /* UIKit.framework */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
+		CEA4F5D6878B4F22BE65F7EA /* CDVLocalFilesystem.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = CDVLocalFilesystem.h; path = "cordova-plugin-file/CDVLocalFilesystem.h"; sourceTree = "<group>"; };
 		D38501F97DE94CD99FF7AF26 /* IonicKeyboard.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = IonicKeyboard.m; path = "ionic-plugin-keyboard/IonicKeyboard.m"; sourceTree = "<group>"; };
+		D5EFC89EB8D145AA8AE57FD0 /* CDVLocalFilesystem.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = CDVLocalFilesystem.m; path = "cordova-plugin-file/CDVLocalFilesystem.m"; sourceTree = "<group>"; };
+		DC5DA19FED2E4F1384F01643 /* CDVFile.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = CDVFile.h; path = "cordova-plugin-file/CDVFile.h"; sourceTree = "<group>"; };
 		DE1ADDD030C744928459F422 /* AlipaySDK.framework */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = wrapper.framework; name = AlipaySDK.framework; path = "美天旺/Plugins/cordova-plugin-alipay-v2/AlipaySDK.framework"; sourceTree = "<group>"; };
 		E89776F7747D4496ACF1D820 /* WXApi.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = WXApi.h; path = "cordova-plugin-wechat/WXApi.h"; sourceTree = "<group>"; };
 		EB22686FE8E1419AB2968277 /* UserNotifications.framework */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
@@ -151,6 +167,7 @@
 		EB87FDF41871DAF40020F90C /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = config.xml; path = ../../config.xml; sourceTree = "<group>"; };
 		ED33DF2A687741AEAF9F8254 /* Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Bridging-Header.h"; sourceTree = "<group>"; };
 		F4B2976FF2734DD7B85D35C5 /* CDVWechat.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = CDVWechat.h; path = "cordova-plugin-wechat/CDVWechat.h"; sourceTree = "<group>"; };
+		F7B5771C31244DE18350BC13 /* CDVAssetLibraryFilesystem.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = CDVAssetLibraryFilesystem.m; path = "cordova-plugin-file/CDVAssetLibraryFilesystem.m"; sourceTree = "<group>"; };
 		F840E1F0165FE0F500CFE078 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = config.xml; path = "美天旺/config.xml"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -180,6 +197,7 @@
 				4908CE43D5C64CCF8E712F22 /* CoreMotion.framework in Frameworks */,
 				16013A59C2154F7299D1E129 /* libc++.tbd in Frameworks */,
 				B2228A798D5E4B8EAED86FFC /* CoreText.framework in Frameworks */,
+				17E91745255D33D4000105DB /* CloudKit.framework in Frameworks */,
 				F9E8BE8305AC4AB680685064 /* QuartzCore.framework in Frameworks */,
 				F99BB104CEE14FD7AB13287C /* AlipaySDK.framework in Frameworks */,
 			);
@@ -254,6 +272,7 @@
 		29B97323FDCFA39411CA2CEA /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				17E91744255D33D4000105DB /* CloudKit.framework */,
 				C02AD31B9F014104B663149E /* jcore-ios-2.2.5.a */,
 				61A9D3CA2D8149228414BCDE /* libWeChatSDK.a */,
 				AD7315985BE04AC2826B9DA8 /* libz.tbd */,
@@ -332,6 +351,16 @@
 				C0F1B0B369134AA686D5331E /* alipay.m */,
 				D38501F97DE94CD99FF7AF26 /* IonicKeyboard.m */,
 				AA86A0B7DB824ED69DBB0630 /* IonicKeyboard.h */,
+				2F5592E635BA4B38A7349FBF /* CDVFile.m */,
+				D5EFC89EB8D145AA8AE57FD0 /* CDVLocalFilesystem.m */,
+				F7B5771C31244DE18350BC13 /* CDVAssetLibraryFilesystem.m */,
+				DC5DA19FED2E4F1384F01643 /* CDVFile.h */,
+				CEA4F5D6878B4F22BE65F7EA /* CDVLocalFilesystem.h */,
+				9CE06BF0A07A4C06BFF8AC19 /* CDVAssetLibraryFilesystem.h */,
+				724C827D973F457BB7B48C38 /* CDVFileTransfer.m */,
+				CC22DA8032F048EE9A9AE537 /* CDVFileTransfer.h */,
+				64A41B747404472594549BD5 /* FileOpener2.m */,
+				8D3014967CC54B66B205E26C /* FileOpener2.h */,
 			);
 			name = Plugins;
 			path = "美天旺/Plugins";
@@ -470,6 +499,11 @@
 				DCE8FA8B87564EDF839412A6 /* AppVersion.m in Sources */,
 				16DAD1B7EFB747E9AEA04AA1 /* alipay.m in Sources */,
 				2103ED60FA8544A3AC3F9B67 /* IonicKeyboard.m in Sources */,
+				06199B718EEC4FAFA96ED8F8 /* CDVFile.m in Sources */,
+				E1015F64ECBC49C9AEC26A2F /* CDVLocalFilesystem.m in Sources */,
+				E0DC52D23D254CD39C4B60F9 /* CDVAssetLibraryFilesystem.m in Sources */,
+				FA5CEEE37F1B4B598CE0176A /* CDVFileTransfer.m in Sources */,
+				8BEB6D7270204EE8AE3AFCD8 /* FileOpener2.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -495,7 +529,7 @@
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				COPY_PHASE_STRIP = NO;
-				CURRENT_PROJECT_VERSION = 1.2.2;
+				CURRENT_PROJECT_VERSION = 1.2.8;
 				DEVELOPMENT_TEAM = D5DGD222ZA;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
@@ -517,7 +551,7 @@
 					"\"$(SRCROOT)/$(TARGET_NAME)/Plugins/cordova-plugin-wechat\"",
 					"\"$(SRCROOT)/$(TARGET_NAME)/Plugins/jpush-phonegap-plugin\"",
 				);
-				MARKETING_VERSION = 1.2.2;
+				MARKETING_VERSION = 1.2.8;
 				PRODUCT_BUNDLE_IDENTIFIER = com.shotshock.twong;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -536,7 +570,7 @@
 				CODE_SIGN_IDENTITY = "Apple Development";
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
 				COPY_PHASE_STRIP = YES;
-				CURRENT_PROJECT_VERSION = 1.2.2;
+				CURRENT_PROJECT_VERSION = 1.2.8;
 				DEVELOPMENT_TEAM = D5DGD222ZA;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
@@ -556,7 +590,7 @@
 					"\"$(SRCROOT)/$(TARGET_NAME)/Plugins/cordova-plugin-wechat\"",
 					"\"$(SRCROOT)/$(TARGET_NAME)/Plugins/jpush-phonegap-plugin\"",
 				);
-				MARKETING_VERSION = 1.2.2;
+				MARKETING_VERSION = 1.2.8;
 				PRODUCT_BUNDLE_IDENTIFIER = com.shotshock.twong;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				TARGETED_DEVICE_FAMILY = 1;

BIN
cordova/platforms/ios/美天旺.xcworkspace/xcuserdata/x.xcuserdatad/UserInterfaceState.xcuserstate


+ 3 - 3
cordova/platforms/ios/美天旺/CDVLaunchScreen.storyboard

@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
     <device id="retina6_1" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
         <capability name="Named colors" minToolsVersion="9.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
@@ -39,7 +39,7 @@
         </scene>
     </scenes>
     <resources>
-        <image name="LaunchStoryboard" width="1366" height="1366"/>
+        <image name="LaunchStoryboard" width="360" height="640"/>
         <namedColor name="BackgroundColor">
             <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
         </namedColor>

+ 2 - 2
cordova/platforms/ios/美天旺/Entitlements-Debug.plist

@@ -2,9 +2,9 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-	<key>com.apple.security.application-groups</key>
-	<array/>
 	<key>aps-environment</key>
 	<string>development</string>
+	<key>com.apple.security.application-groups</key>
+	<array/>
 </dict>
 </plist>

+ 2 - 2
cordova/platforms/ios/美天旺/Entitlements-Release.plist

@@ -2,9 +2,9 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-	<key>com.apple.security.application-groups</key>
-	<array/>
 	<key>aps-environment</key>
 	<string>production</string>
+	<key>com.apple.security.application-groups</key>
+	<array/>
 </dict>
 </plist>

+ 6 - 3
cordova/platforms/ios/美天旺/Images.xcassets/LaunchStoryboard.imageset/Contents.json

@@ -141,7 +141,8 @@
     {
       "idiom": "universal",
       "scale": "2x",
-      "width-class": "compact"
+      "width-class": "compact",
+      "filename": "Default@2x~universal~comany.png"
     },
     {
       "idiom": "universal",
@@ -194,7 +195,8 @@
     },
     {
       "idiom": "universal",
-      "scale": "2x"
+      "scale": "2x",
+      "filename": "Default@2x~universal~anyany.png"
     },
     {
       "idiom": "universal",
@@ -249,7 +251,8 @@
     {
       "idiom": "universal",
       "scale": "3x",
-      "width-class": "compact"
+      "width-class": "compact",
+      "filename": "Default@3x~universal~comany.png"
     },
     {
       "idiom": "universal",

BIN
cordova/platforms/ios/美天旺/Images.xcassets/LaunchStoryboard.imageset/Default@2x~universal~anyany.png


BIN
cordova/platforms/ios/美天旺/Images.xcassets/LaunchStoryboard.imageset/Default@2x~universal~comany.png


BIN
cordova/platforms/ios/美天旺/Images.xcassets/LaunchStoryboard.imageset/Default@3x~universal~comany.png


+ 35 - 0
cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file-opener2/FileOpener2.h

@@ -0,0 +1,35 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2013 pwlin - pwlin05@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+#import <Cordova/CDV.h>
+
+@interface FileOpener2 : CDVPlugin <UIDocumentInteractionControllerDelegate> {
+    NSString *localFile;
+}
+
+@property(nonatomic, strong) UIDocumentInteractionController *controller;
+@property(nonatomic, strong) CDVViewController *cdvViewController;
+
+- (void) open: (CDVInvokedUrlCommand*)command;
+
+@end

+ 151 - 0
cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file-opener2/FileOpener2.m

@@ -0,0 +1,151 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2013 pwlin - pwlin05@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#import "FileOpener2.h"
+#import <Cordova/CDV.h>
+
+#import <QuartzCore/QuartzCore.h>
+#import <MobileCoreServices/MobileCoreServices.h>
+
+@implementation FileOpener2
+@synthesize controller = docController;
+
+CDVPluginResult* pluginResult = nil;
+NSString* callbackId = nil;
+
+- (void) open: (CDVInvokedUrlCommand*)command {
+	callbackId = command.callbackId;
+	NSString *path = [command.arguments objectAtIndex:0];
+	NSString *contentType = [command.arguments objectAtIndex:1];
+	BOOL showPreview = YES;
+
+	if ([command.arguments count] >= 3) {
+		showPreview = [[command.arguments objectAtIndex:2] boolValue];
+	}
+
+	CDVViewController* cont = (CDVViewController*)[super viewController];
+	self.cdvViewController = cont;
+	NSString *uti = nil;
+
+	if ([contentType length] == 0) {
+		NSArray *dotParts = [path componentsSeparatedByString:@"."];
+		NSString *fileExt = [dotParts lastObject];
+
+		uti = (__bridge NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExt, NULL);
+	} else {
+		uti = (__bridge NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)contentType, NULL);
+	}
+
+	dispatch_async(dispatch_get_main_queue(), ^{
+		NSURL *fileURL = NULL;
+		NSString *decodedPath = [path stringByRemovingPercentEncoding];
+
+		if ([path isEqualToString:decodedPath]) {
+			NSLog(@"Path parameter not encoded. Building file URL encoding it...");
+			fileURL = [NSURL fileURLWithPath:[path stringByReplacingOccurrencesOfString:@"file://" withString:@""]];;
+		} else {
+			NSLog(@"Path parameter already encoded. Building file URL without encoding it...");
+			fileURL = [NSURL URLWithString:path];
+		}
+
+		localFile = fileURL.path;
+
+		NSLog(@"looking for file at %@", fileURL);
+		NSFileManager *fm = [NSFileManager defaultManager];
+
+    	if (![fm fileExistsAtPath:localFile]) {
+	    	NSDictionary *jsonObj = @{@"status" : @"9",
+	    	@"message" : @"File does not exist"};
+	    	CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:jsonObj];
+	    	[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+	    	return;
+    	}
+
+		docController = [UIDocumentInteractionController  interactionControllerWithURL:fileURL];
+		docController.delegate = self;
+		docController.UTI = uti;
+
+		//Opens the file preview
+		CGRect rect;
+
+		if ([command.arguments count] >= 4) {
+			NSArray *positionValues = [command.arguments objectAtIndex:3];
+
+			if (![positionValues isEqual:[NSNull null]] && [positionValues count] >= 2) {
+				rect = CGRectMake(0, 0, [[positionValues objectAtIndex:0] floatValue], [[positionValues objectAtIndex:1] floatValue]);
+			} else {
+				rect = CGRectMake(0, 0, 0, 0);
+			}
+		} else {
+			rect = CGRectMake(0, 0, cont.view.bounds.size.width, cont.view.bounds.size.height);
+		}
+
+		BOOL wasOpened = NO;
+
+		if (showPreview) {
+			wasOpened = [docController presentPreviewAnimated: NO];
+		} else {
+			CDVViewController* cont = self.cdvViewController;
+			wasOpened = [docController presentOpenInMenuFromRect:rect inView:cont.view animated:YES];
+		}
+
+		if (wasOpened) {
+			pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString: @""];
+		} else {
+			NSDictionary *jsonObj = [ [NSDictionary alloc]
+				initWithObjectsAndKeys :
+				@"9", @"status",
+				@"Could not handle UTI", @"message",
+				nil
+			];
+			pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:jsonObj];
+        	[self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
+		}
+	});
+}
+
+@end
+
+@implementation FileOpener2 (UIDocumentInteractionControllerDelegate)
+- (void)documentInteractionControllerDidDismissOpenInMenu:(UIDocumentInteractionController *)controller {
+	[self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
+}
+
+- (void)documentInteractionControllerDidEndPreview:(UIDocumentInteractionController *)controller {
+	[self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
+}
+
+- (UIViewController *)documentInteractionControllerViewControllerForPreview:(UIDocumentInteractionController *)controller {
+	UIViewController *presentingViewController = self.viewController;
+
+	if (presentingViewController.view.window != [UIApplication sharedApplication].keyWindow) {
+		presentingViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
+	}
+
+	while (presentingViewController.presentedViewController != nil && ![presentingViewController.presentedViewController isBeingDismissed]) {
+		presentingViewController = presentingViewController.presentedViewController;
+	}
+
+	return presentingViewController;
+}
+
+@end

+ 89 - 0
cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file-transfer/CDVFileTransfer.h

@@ -0,0 +1,89 @@
+/*
+ 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 <Foundation/Foundation.h>
+#import <Cordova/CDVPlugin.h>
+#import "CDVFile.h"
+
+enum CDVFileTransferError {
+    FILE_NOT_FOUND_ERR = 1,
+    INVALID_URL_ERR = 2,
+    CONNECTION_ERR = 3,
+    CONNECTION_ABORTED = 4,
+    NOT_MODIFIED = 5
+};
+typedef int CDVFileTransferError;
+
+enum CDVFileTransferDirection {
+    CDV_TRANSFER_UPLOAD = 1,
+    CDV_TRANSFER_DOWNLOAD = 2,
+};
+typedef int CDVFileTransferDirection;
+
+// Magic value within the options dict used to set a cookie.
+extern NSString* const kOptionsKeyCookie;
+
+@interface CDVFileTransfer : CDVPlugin {}
+
+- (void)upload:(CDVInvokedUrlCommand*)command;
+- (void)download:(CDVInvokedUrlCommand*)command;
+- (NSString*)escapePathComponentForUrlString:(NSString*)urlString;
+
+// Visible for testing.
+- (NSURLRequest*)requestForUploadCommand:(CDVInvokedUrlCommand*)command fileData:(NSData*)fileData;
+- (NSMutableDictionary*)createFileTransferError:(int)code AndSource:(NSString*)source AndTarget:(NSString*)target;
+
+- (NSMutableDictionary*)createFileTransferError:(int)code
+                                      AndSource:(NSString*)source
+                                      AndTarget:(NSString*)target
+                                  AndHttpStatus:(int)httpStatus
+                                        AndBody:(NSString*)body;
+@property (nonatomic, strong) NSOperationQueue* queue;
+@property (readonly) NSMutableDictionary* activeTransfers;
+@end
+
+@class CDVFileTransferEntityLengthRequest;
+
+@interface CDVFileTransferDelegate : NSObject {}
+
+- (void)updateBytesExpected:(long long)newBytesExpected;
+- (void)cancelTransfer:(NSURLConnection*)connection;
+
+@property (strong) NSMutableData* responseData; // atomic
+@property (nonatomic, strong) NSDictionary* responseHeaders;
+@property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskID;
+@property (nonatomic, strong) CDVFileTransfer* command;
+@property (nonatomic, assign) CDVFileTransferDirection direction;
+@property (nonatomic, strong) NSURLConnection* connection;
+@property (nonatomic, copy) NSString* callbackId;
+@property (nonatomic, copy) NSString* objectId;
+@property (nonatomic, copy) NSString* source;
+@property (nonatomic, copy) NSString* target;
+@property (nonatomic, copy) NSURL* targetURL;
+@property (nonatomic, copy) NSString* mimeType;
+@property (assign) int responseCode; // atomic
+@property (nonatomic, assign) long long bytesTransfered;
+@property (nonatomic, assign) long long bytesExpected;
+@property (nonatomic, assign) BOOL trustAllHosts;
+@property (strong) NSFileHandle* targetFileHandle;
+@property (nonatomic, strong) CDVFileTransferEntityLengthRequest* entityLengthRequest;
+@property (nonatomic, strong) CDVFile *filePlugin;
+@property (nonatomic, assign) BOOL chunkedMode;
+
+@end

+ 861 - 0
cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file-transfer/CDVFileTransfer.m

@@ -0,0 +1,861 @@
+/*
+ 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 <Cordova/CDV.h>
+#import "CDVFileTransfer.h"
+#import "CDVLocalFilesystem.h"
+
+#import <AssetsLibrary/ALAsset.h>
+#import <AssetsLibrary/ALAssetRepresentation.h>
+#import <AssetsLibrary/ALAssetsLibrary.h>
+#import <CFNetwork/CFNetwork.h>
+
+#ifndef DLog
+#ifdef DEBUG
+    #define DLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
+#else
+    #define DLog(...)
+#endif
+#endif
+
+@interface CDVFileTransfer ()
+// Sets the requests headers for the request.
+- (void)applyRequestHeaders:(NSDictionary*)headers toRequest:(NSMutableURLRequest*)req;
+// Creates a delegate to handle an upload.
+- (CDVFileTransferDelegate*)delegateForUploadCommand:(CDVInvokedUrlCommand*)command;
+// Creates an NSData* for the file for the given upload arguments.
+- (void)fileDataForUploadCommand:(CDVInvokedUrlCommand*)command;
+@end
+
+// Buffer size to use for streaming uploads.
+static const NSUInteger kStreamBufferSize = 32768;
+// Magic value within the options dict used to set a cookie.
+NSString* const kOptionsKeyCookie = @"__cookie";
+// Form boundary for multi-part requests.
+NSString* const kFormBoundary = @"+++++org.apache.cordova.formBoundary";
+
+// Writes the given data to the stream in a blocking way.
+// If successful, returns bytesToWrite.
+// If the stream was closed on the other end, returns 0.
+// If there was an error, returns -1.
+static CFIndex WriteDataToStream(NSData* data, CFWriteStreamRef stream)
+{
+    UInt8* bytes = (UInt8*)[data bytes];
+    long long bytesToWrite = [data length];
+    long long totalBytesWritten = 0;
+
+    while (totalBytesWritten < bytesToWrite) {
+        CFIndex result = CFWriteStreamWrite(stream,
+                bytes + totalBytesWritten,
+                bytesToWrite - totalBytesWritten);
+        if (result < 0) {
+            CFStreamError error = CFWriteStreamGetError(stream);
+            NSLog(@"WriteStreamError domain: %ld error: %ld", error.domain, (long)error.error);
+            return result;
+        } else if (result == 0) {
+            return result;
+        }
+        totalBytesWritten += result;
+    }
+
+    return totalBytesWritten;
+}
+
+@implementation CDVFileTransfer
+@synthesize activeTransfers;
+
+- (void)pluginInitialize {
+    activeTransfers = [[NSMutableDictionary alloc] init];
+}
+
+- (NSString*)escapePathComponentForUrlString:(NSString*)urlString
+{
+    NSRange schemeAndHostRange = [urlString rangeOfString:@"://.*?/" options:NSRegularExpressionSearch];
+
+    if (schemeAndHostRange.length == 0) {
+        return urlString;
+    }
+
+    NSInteger schemeAndHostEndIndex = NSMaxRange(schemeAndHostRange);
+    NSString* schemeAndHost = [urlString substringToIndex:schemeAndHostEndIndex];
+    NSString* pathComponent = [urlString substringFromIndex:schemeAndHostEndIndex];
+    pathComponent = [pathComponent stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+
+    return [schemeAndHost stringByAppendingString:pathComponent];
+}
+
+- (void)applyRequestHeaders:(NSDictionary*)headers toRequest:(NSMutableURLRequest*)req
+{
+    [req setValue:@"XMLHttpRequest" forHTTPHeaderField:@"X-Requested-With"];
+
+    NSString* userAgent = @""; // [self.commandDelegate userAgent];
+    if (userAgent) {
+        [req setValue:userAgent forHTTPHeaderField:@"User-Agent"];
+    }
+
+    for (NSString* headerName in headers) {
+        id value = [headers objectForKey:headerName];
+        if (!value || (value == [NSNull null])) {
+            value = @"null";
+        }
+
+        // First, remove an existing header if one exists.
+        [req setValue:nil forHTTPHeaderField:headerName];
+
+        if (![value isKindOfClass:[NSArray class]]) {
+            value = [NSArray arrayWithObject:value];
+        }
+
+        // Then, append all header values.
+        for (id __strong subValue in value) {
+            // Convert from an NSNumber -> NSString.
+            if ([subValue respondsToSelector:@selector(stringValue)]) {
+                subValue = [subValue stringValue];
+            }
+            if ([subValue isKindOfClass:[NSString class]]) {
+                [req addValue:subValue forHTTPHeaderField:headerName];
+            }
+        }
+    }
+}
+
+- (NSURLRequest*)requestForUploadCommand:(CDVInvokedUrlCommand*)command fileData:(NSData*)fileData
+{
+    // arguments order from js: [filePath, server, fileKey, fileName, mimeType, params, debug, chunkedMode]
+    // however, params is a JavaScript object and during marshalling is put into the options dict,
+    // thus debug and chunkedMode are the 6th and 7th arguments
+    NSString* target = [command argumentAtIndex:0];
+    NSString* server = [command argumentAtIndex:1];
+    NSString* fileKey = [command argumentAtIndex:2 withDefault:@"file"];
+    NSString* fileName = [command argumentAtIndex:3 withDefault:@"image.jpg"];
+    NSString* mimeType = [command argumentAtIndex:4 withDefault:@"image/jpeg"];
+    NSDictionary* options = [command argumentAtIndex:5 withDefault:nil];
+    //    BOOL trustAllHosts = [[command argumentAtIndex:6 withDefault:[NSNumber numberWithBool:YES]] boolValue]; // allow self-signed certs
+    BOOL chunkedMode = [[command argumentAtIndex:7 withDefault:[NSNumber numberWithBool:YES]] boolValue];
+    NSDictionary* headers = [command argumentAtIndex:8 withDefault:nil];
+    // Allow alternative http method, default to POST. JS side checks
+    // for allowed methods, currently PUT or POST (forces POST for
+    // unrecognised values)
+    NSString* httpMethod = [command argumentAtIndex:10 withDefault:@"POST"];
+    CDVPluginResult* result = nil;
+    CDVFileTransferError errorCode = 0;
+
+    // NSURL does not accepts URLs with spaces in the path. We escape the path in order
+    // to be more lenient.
+    NSURL* url = [NSURL URLWithString:server];
+
+    if (!url) {
+        errorCode = INVALID_URL_ERR;
+        NSLog(@"File Transfer Error: Invalid server URL %@", server);
+    } else if (!fileData) {
+        errorCode = FILE_NOT_FOUND_ERR;
+    }
+
+    if (errorCode > 0) {
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self createFileTransferError:errorCode AndSource:target AndTarget:server]];
+        [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+        return nil;
+    }
+
+    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];
+
+    [req setHTTPMethod:httpMethod];
+
+    //    Magic value to set a cookie
+    if ([options objectForKey:kOptionsKeyCookie]) {
+        [req setValue:[options objectForKey:kOptionsKeyCookie] forHTTPHeaderField:@"Cookie"];
+        [req setHTTPShouldHandleCookies:NO];
+    }
+
+    // if we specified a Content-Type header, don't do multipart form upload
+    BOOL multipartFormUpload = [headers objectForKey:@"Content-Type"] == nil;
+    if (multipartFormUpload) {
+        NSString* contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", kFormBoundary];
+        [req setValue:contentType forHTTPHeaderField:@"Content-Type"];
+    }
+    [self applyRequestHeaders:headers toRequest:req];
+
+    NSData* formBoundaryData = [[NSString stringWithFormat:@"--%@\r\n", kFormBoundary] dataUsingEncoding:NSUTF8StringEncoding];
+    NSMutableData* postBodyBeforeFile = [NSMutableData data];
+
+    for (NSString* key in options) {
+        id val = [options objectForKey:key];
+        if (!val || (val == [NSNull null]) || [key isEqualToString:kOptionsKeyCookie]) {
+            continue;
+        }
+        // if it responds to stringValue selector (eg NSNumber) get the NSString
+        if ([val respondsToSelector:@selector(stringValue)]) {
+            val = [val stringValue];
+        }
+        // finally, check whether it is a NSString (for dataUsingEncoding selector below)
+        if (![val isKindOfClass:[NSString class]]) {
+            continue;
+        }
+
+        [postBodyBeforeFile appendData:formBoundaryData];
+        [postBodyBeforeFile appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]];
+        [postBodyBeforeFile appendData:[val dataUsingEncoding:NSUTF8StringEncoding]];
+        [postBodyBeforeFile appendData:[@"\r\n" dataUsingEncoding : NSUTF8StringEncoding]];
+    }
+
+    [postBodyBeforeFile appendData:formBoundaryData];
+    [postBodyBeforeFile appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fileKey, fileName] dataUsingEncoding:NSUTF8StringEncoding]];
+    if (mimeType != nil) {
+        [postBodyBeforeFile appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n", mimeType] dataUsingEncoding:NSUTF8StringEncoding]];
+    }
+    [postBodyBeforeFile appendData:[[NSString stringWithFormat:@"Content-Length: %ld\r\n\r\n", (long)[fileData length]] dataUsingEncoding:NSUTF8StringEncoding]];
+
+    DLog(@"fileData length: %ld", [fileData length]);
+    NSData* postBodyAfterFile = [[NSString stringWithFormat:@"\r\n--%@--\r\n", kFormBoundary] dataUsingEncoding:NSUTF8StringEncoding];
+
+    long long totalPayloadLength = [fileData length];
+    if (multipartFormUpload) {
+        totalPayloadLength += [postBodyBeforeFile length] + [postBodyAfterFile length];
+    }
+
+    [req setValue:[[NSNumber numberWithLongLong:totalPayloadLength] stringValue] forHTTPHeaderField:@"Content-Length"];
+
+    if (chunkedMode) {
+        CFReadStreamRef readStream = NULL;
+        CFWriteStreamRef writeStream = NULL;
+        CFStreamCreateBoundPair(NULL, &readStream, &writeStream, kStreamBufferSize);
+        [req setHTTPBodyStream:CFBridgingRelease(readStream)];
+
+        [self.commandDelegate runInBackground:^{
+            if (CFWriteStreamOpen(writeStream)) {
+                if (multipartFormUpload) {
+                    NSData* chunks[] = { postBodyBeforeFile, fileData, postBodyAfterFile };
+                    int numChunks = sizeof(chunks) / sizeof(chunks[0]);
+
+                    for (int i = 0; i < numChunks; ++i) {
+                        // Allow uploading of an empty file
+                        if (chunks[i].length == 0) {
+                            continue;
+                        }
+
+                        CFIndex result = WriteDataToStream(chunks[i], writeStream);
+                        if (result <= 0) {
+                            break;
+                        }
+                    }
+                } else {
+                    if (totalPayloadLength > 0) {
+                        WriteDataToStream(fileData, writeStream);
+                    } else {
+                        NSLog(@"Uploading of an empty file is not supported for chunkedMode=true and multipart=false");
+                    }
+                }
+            } else {
+                NSLog(@"FileTransfer: Failed to open writeStream");
+            }
+            CFWriteStreamClose(writeStream);
+            CFRelease(writeStream);
+        }];
+    } else {
+        if (multipartFormUpload) {
+            [postBodyBeforeFile appendData:fileData];
+            [postBodyBeforeFile appendData:postBodyAfterFile];
+            [req setHTTPBody:postBodyBeforeFile];
+        } else {
+            [req setHTTPBody:fileData];
+        }
+    }
+    return req;
+}
+
+- (CDVFileTransferDelegate*)delegateForUploadCommand:(CDVInvokedUrlCommand*)command
+{
+    NSString* source = [command argumentAtIndex:0];
+    NSString* server = [command argumentAtIndex:1];
+    BOOL trustAllHosts = [[command argumentAtIndex:6 withDefault:[NSNumber numberWithBool:NO]] boolValue]; // allow self-signed certs
+    NSString* objectId = [command argumentAtIndex:9];
+    BOOL chunkedMode = [[command argumentAtIndex:7 withDefault:[NSNumber numberWithBool:YES]] boolValue];
+
+    CDVFileTransferDelegate* delegate = [[CDVFileTransferDelegate alloc] init];
+
+    delegate.command = self;
+    delegate.callbackId = command.callbackId;
+    delegate.direction = CDV_TRANSFER_UPLOAD;
+    delegate.objectId = objectId;
+    delegate.source = source;
+    delegate.target = server;
+    delegate.trustAllHosts = trustAllHosts;
+    delegate.filePlugin = [self.commandDelegate getCommandInstance:@"File"];
+    delegate.chunkedMode = chunkedMode;
+
+    return delegate;
+}
+
+- (void)fileDataForUploadCommand:(CDVInvokedUrlCommand*)command
+{
+    NSString* source = (NSString*)[command argumentAtIndex:0];
+    NSString* server = [command argumentAtIndex:1];
+    NSError* __autoreleasing err = nil;
+
+    if ([source hasPrefix:@"data:"] && [source rangeOfString:@"base64"].location != NSNotFound) {
+        NSRange commaRange = [source rangeOfString: @","];
+        if (commaRange.location == NSNotFound) {
+            // Return error is there is no comma
+            __weak CDVFileTransfer* weakSelf = self;
+            CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[weakSelf createFileTransferError:INVALID_URL_ERR AndSource:source AndTarget:server]];
+            [weakSelf.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+            return;
+        }
+
+        if (commaRange.location + 1 > source.length - 1) {
+            // Init as an empty data
+            NSData *fileData = [[NSData alloc] init];
+            [self uploadData:fileData command:command];
+            return;
+        }
+
+        NSData *fileData = [[NSData alloc] initWithBase64EncodedString:[source substringFromIndex:(commaRange.location + 1)] options:NSDataBase64DecodingIgnoreUnknownCharacters];
+        [self uploadData:fileData command:command];
+        return;
+    }
+
+    CDVFilesystemURL *sourceURL = [CDVFilesystemURL fileSystemURLWithString:source];
+    NSObject<CDVFileSystem> *fs;
+    if (sourceURL) {
+        // Try to get a CDVFileSystem which will handle this file.
+        // This requires talking to the current CDVFile plugin.
+        fs = [[self.commandDelegate getCommandInstance:@"File"] filesystemForURL:sourceURL];
+    }
+    if (fs) {
+        __weak CDVFileTransfer* weakSelf = self;
+        [fs readFileAtURL:sourceURL start:0 end:-1 callback:^(NSData *fileData, NSString *mimeType, CDVFileError err) {
+            if (err) {
+                // We couldn't find the asset.  Send the appropriate error.
+                CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[weakSelf createFileTransferError:NOT_FOUND_ERR AndSource:source AndTarget:server]];
+                [weakSelf.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+            }  else {
+                [weakSelf uploadData:fileData command:command];
+            }
+        }];
+        return;
+    } else {
+        // Extract the path part out of a file: URL.
+        NSString* filePath = [source hasPrefix:@"/"] ? [source copy] : [(NSURL *)[NSURL URLWithString:source] path];
+        if (filePath == nil) {
+            // We couldn't find the asset.  Send the appropriate error.
+            CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self createFileTransferError:NOT_FOUND_ERR AndSource:source AndTarget:server]];
+            [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+            return;
+        }
+
+        // Memory map the file so that it can be read efficiently even if it is large.
+        NSData* fileData = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&err];
+
+        if (err != nil) {
+            NSLog(@"Error opening file %@: %@", source, err);
+            CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self createFileTransferError:NOT_FOUND_ERR AndSource:source AndTarget:server]];
+            [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+        } else {
+            [self uploadData:fileData command:command];
+        }
+    }
+}
+
+- (void)upload:(CDVInvokedUrlCommand*)command
+{
+    // fileData and req are split into helper functions to ease the unit testing of delegateForUpload.
+    // First, get the file data.  This method will call `uploadData:command`.
+    [self fileDataForUploadCommand:command];
+}
+
+- (void)uploadData:(NSData*)fileData command:(CDVInvokedUrlCommand*)command
+{
+    NSURLRequest* req = [self requestForUploadCommand:command fileData:fileData];
+
+    if (req == nil) {
+        return;
+    }
+    CDVFileTransferDelegate* delegate = [self delegateForUploadCommand:command];
+    delegate.connection = [[NSURLConnection alloc] initWithRequest:req delegate:delegate startImmediately:NO];
+    if (self.queue == nil) {
+        self.queue = [[NSOperationQueue alloc] init];
+    }
+    [delegate.connection setDelegateQueue:self.queue];
+
+    // sets a background task ID for the transfer object.
+    delegate.backgroundTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
+        [delegate cancelTransfer:delegate.connection];
+    }];
+
+    @synchronized (activeTransfers) {
+        activeTransfers[delegate.objectId] = delegate;
+    }
+    [delegate.connection start];
+}
+
+- (void)abort:(CDVInvokedUrlCommand*)command
+{
+    NSString* objectId = [command argumentAtIndex:0];
+
+    @synchronized (activeTransfers) {
+        CDVFileTransferDelegate* delegate = activeTransfers[objectId];
+        if (delegate != nil) {
+            [delegate cancelTransfer:delegate.connection];
+            CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self createFileTransferError:CONNECTION_ABORTED AndSource:delegate.source AndTarget:delegate.target]];
+            [self.commandDelegate sendPluginResult:result callbackId:delegate.callbackId];
+        }
+    }
+}
+
+- (void)download:(CDVInvokedUrlCommand*)command
+{
+    DLog(@"File Transfer downloading file...");
+    NSString* source = [command argumentAtIndex:0];
+    NSString* target = [command argumentAtIndex:1];
+    BOOL trustAllHosts = [[command argumentAtIndex:2 withDefault:[NSNumber numberWithBool:NO]] boolValue]; // allow self-signed certs
+    NSString* objectId = [command argumentAtIndex:3];
+    NSDictionary* headers = [command argumentAtIndex:4 withDefault:nil];
+
+    CDVPluginResult* result = nil;
+    CDVFileTransferError errorCode = 0;
+
+    NSURL* targetURL;
+
+    if ([target hasPrefix:@"/"]) {
+        /* Backwards-compatibility:
+         * Check here to see if it looks like the user passed in a raw filesystem path. (Perhaps they had the path saved, and were previously using it with the old version of File). If so, normalize it by removing empty path segments, and check with File to see if any of the installed filesystems will handle it. If so, then we will end up with a filesystem url to use for the remainder of this operation.
+         */
+        target = [target stringByReplacingOccurrencesOfString:@"//" withString:@"/"];
+        targetURL = [[self.commandDelegate getCommandInstance:@"File"] fileSystemURLforLocalPath:target].url;
+    } else {
+        targetURL = [NSURL URLWithString:target];
+
+        if (targetURL == nil) {
+            NSString* targetUrlTextEscaped = [target stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]];
+            if (targetUrlTextEscaped) {
+                targetURL = [NSURL URLWithString:targetUrlTextEscaped];
+            }
+        }
+    }
+
+    NSURL* sourceURL = [NSURL URLWithString:source];
+
+    if (!sourceURL) {
+        errorCode = INVALID_URL_ERR;
+        NSLog(@"File Transfer Error: Invalid server URL %@", source);
+    } else if (!targetURL) {
+        errorCode = INVALID_URL_ERR;
+        NSLog(@"File Tranfer Error: Invalid target URL %@", target);
+    } else if (![targetURL isFileURL]) {
+        CDVFilesystemURL *fsURL = [CDVFilesystemURL fileSystemURLWithString:target];
+        if (!fsURL) {
+           errorCode = FILE_NOT_FOUND_ERR;
+           NSLog(@"File Transfer Error: Invalid file path or URL %@", target);
+        }
+    }
+
+    if (errorCode > 0) {
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self createFileTransferError:errorCode AndSource:source AndTarget:target]];
+        [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+        return;
+    }
+
+    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:sourceURL];
+    [self applyRequestHeaders:headers toRequest:req];
+
+    CDVFileTransferDelegate* delegate = [[CDVFileTransferDelegate alloc] init];
+    delegate.command = self;
+    delegate.direction = CDV_TRANSFER_DOWNLOAD;
+    delegate.callbackId = command.callbackId;
+    delegate.objectId = objectId;
+    delegate.source = source;
+    delegate.target = [targetURL absoluteString];
+    delegate.targetURL = targetURL;
+    delegate.trustAllHosts = trustAllHosts;
+    delegate.filePlugin = [self.commandDelegate getCommandInstance:@"File"];
+    delegate.backgroundTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
+        [delegate cancelTransfer:delegate.connection];
+    }];
+
+    delegate.connection = [[NSURLConnection alloc] initWithRequest:req delegate:delegate startImmediately:NO];
+
+    if (self.queue == nil) {
+        self.queue = [[NSOperationQueue alloc] init];
+    }
+    [delegate.connection setDelegateQueue:self.queue];
+
+    @synchronized (activeTransfers) {
+        activeTransfers[delegate.objectId] = delegate;
+    }
+    // Downloads can take time
+    // sending this to a new thread calling the download_async method
+    dispatch_async(
+                   dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL),
+                   ^(void) { [delegate.connection start];}
+                   );
+}
+
+- (NSMutableDictionary*)createFileTransferError:(int)code AndSource:(NSString*)source AndTarget:(NSString*)target
+{
+    NSMutableDictionary* result = [NSMutableDictionary dictionaryWithCapacity:3];
+
+    [result setObject:[NSNumber numberWithInt:code] forKey:@"code"];
+    if (source != nil) {
+        [result setObject:source forKey:@"source"];
+    }
+    if (target != nil) {
+        [result setObject:target forKey:@"target"];
+    }
+    NSLog(@"FileTransferError %@", result);
+
+    return result;
+}
+
+- (NSMutableDictionary*)createFileTransferError:(int)code
+                                      AndSource:(NSString*)source
+                                      AndTarget:(NSString*)target
+                                  AndHttpStatus:(int)httpStatus
+                                        AndBody:(NSString*)body
+{
+    NSMutableDictionary* result = [NSMutableDictionary dictionaryWithCapacity:5];
+
+    [result setObject:[NSNumber numberWithInt:code] forKey:@"code"];
+    if (source != nil) {
+        [result setObject:source forKey:@"source"];
+    }
+    if (target != nil) {
+        [result setObject:target forKey:@"target"];
+    }
+    [result setObject:[NSNumber numberWithInt:httpStatus] forKey:@"http_status"];
+    if (body != nil) {
+        [result setObject:body forKey:@"body"];
+    }
+    NSLog(@"FileTransferError %@", result);
+
+    return result;
+}
+
+- (void)onReset {
+    @synchronized (activeTransfers) {
+        while ([activeTransfers count] > 0) {
+            CDVFileTransferDelegate* delegate = [activeTransfers allValues][0];
+            [delegate cancelTransfer:delegate.connection];
+        }
+    }
+}
+
+@end
+
+@interface CDVFileTransferEntityLengthRequest : NSObject {
+    NSURLConnection* _connection;
+    CDVFileTransferDelegate* __weak _originalDelegate;
+}
+
+- (CDVFileTransferEntityLengthRequest*)initWithOriginalRequest:(NSURLRequest*)originalRequest andDelegate:(CDVFileTransferDelegate*)originalDelegate;
+
+@end
+
+@implementation CDVFileTransferEntityLengthRequest
+
+- (CDVFileTransferEntityLengthRequest*)initWithOriginalRequest:(NSURLRequest*)originalRequest andDelegate:(CDVFileTransferDelegate*)originalDelegate
+{
+    if (self) {
+        DLog(@"Requesting entity length for GZIPped content...");
+
+        NSMutableURLRequest* req = [originalRequest mutableCopy];
+        [req setHTTPMethod:@"HEAD"];
+        [req setValue:@"identity" forHTTPHeaderField:@"Accept-Encoding"];
+
+        _originalDelegate = originalDelegate;
+        _connection = [NSURLConnection connectionWithRequest:req delegate:self];
+    }
+    return self;
+}
+
+- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response
+{
+    DLog(@"HEAD request returned; content-length is %lld", [response expectedContentLength]);
+    [_originalDelegate updateBytesExpected:[response expectedContentLength]];
+}
+
+- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
+{}
+
+- (void)connectionDidFinishLoading:(NSURLConnection*)connection
+{}
+
+@end
+
+@implementation CDVFileTransferDelegate
+
+@synthesize callbackId, connection = _connection, source, target, responseData, responseHeaders, command, bytesTransfered, bytesExpected, direction, responseCode, objectId, targetFileHandle, filePlugin;
+
+- (void)connectionDidFinishLoading:(NSURLConnection*)connection
+{
+    NSString* uploadResponse = nil;
+    NSString* downloadResponse = nil;
+    NSMutableDictionary* uploadResult;
+    CDVPluginResult* result = nil;
+
+    NSLog(@"File Transfer Finished with response code %d", self.responseCode);
+
+    if (self.direction == CDV_TRANSFER_UPLOAD) {
+        uploadResponse = [[NSString alloc] initWithData:self.responseData encoding:NSUTF8StringEncoding];
+        if (uploadResponse == nil) {
+            uploadResponse = [[NSString alloc] initWithData: self.responseData encoding:NSISOLatin1StringEncoding];
+        }
+
+        if ((self.responseCode >= 200) && (self.responseCode < 300)) {
+            // create dictionary to return FileUploadResult object
+            uploadResult = [NSMutableDictionary dictionaryWithCapacity:3];
+            if (uploadResponse != nil) {
+                [uploadResult setObject:uploadResponse forKey:@"response"];
+                [uploadResult setObject:self.responseHeaders forKey:@"headers"];
+            }
+            [uploadResult setObject:[NSNumber numberWithLongLong:self.bytesTransfered] forKey:@"bytesSent"];
+            [uploadResult setObject:[NSNumber numberWithInt:self.responseCode] forKey:@"responseCode"];
+            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:uploadResult];
+        } else {
+            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[command createFileTransferError:CONNECTION_ERR AndSource:source AndTarget:target AndHttpStatus:self.responseCode AndBody:uploadResponse]];
+        }
+    }
+    if (self.direction == CDV_TRANSFER_DOWNLOAD) {
+        if (self.targetFileHandle) {
+            [self.targetFileHandle closeFile];
+            self.targetFileHandle = nil;
+            DLog(@"File Transfer Download success");
+
+            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self.filePlugin makeEntryForURL:self.targetURL]];
+        } else {
+            downloadResponse = [[NSString alloc] initWithData:self.responseData encoding:NSUTF8StringEncoding];
+            if (downloadResponse == nil) {
+                downloadResponse = [[NSString alloc] initWithData: self.responseData encoding:NSISOLatin1StringEncoding];
+            }
+
+            CDVFileTransferError errorCode = self.responseCode == 404 ? FILE_NOT_FOUND_ERR
+                : (self.responseCode == 304 ? NOT_MODIFIED : CONNECTION_ERR);
+            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[command createFileTransferError:errorCode AndSource:source AndTarget:target AndHttpStatus:self.responseCode AndBody:downloadResponse]];
+        }
+    }
+
+    [self.command.commandDelegate sendPluginResult:result callbackId:callbackId];
+
+    // remove connection for activeTransfers
+    @synchronized (command.activeTransfers) {
+        [command.activeTransfers removeObjectForKey:objectId];
+        // remove background id task in case our upload was done in the background
+        [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskID];
+        self.backgroundTaskID = UIBackgroundTaskInvalid;
+    }
+}
+
+- (void)removeTargetFile
+{
+    NSFileManager* fileMgr = [NSFileManager defaultManager];
+
+    NSString *targetPath = [self targetFilePath];
+    if ([fileMgr fileExistsAtPath:targetPath])
+    {
+        [fileMgr removeItemAtPath:targetPath error:nil];
+    }
+}
+
+- (void)cancelTransfer:(NSURLConnection*)connection
+{
+    [connection cancel];
+    @synchronized (self.command.activeTransfers) {
+        CDVFileTransferDelegate* delegate = self.command.activeTransfers[self.objectId];
+        [self.command.activeTransfers removeObjectForKey:self.objectId];
+        [[UIApplication sharedApplication] endBackgroundTask:delegate.backgroundTaskID];
+        delegate.backgroundTaskID = UIBackgroundTaskInvalid;
+    }
+
+    if (self.direction == CDV_TRANSFER_DOWNLOAD) {
+        [self removeTargetFile];
+    }
+}
+
+- (void)cancelTransferWithError:(NSURLConnection*)connection errorMessage:(NSString*)errorMessage
+{
+    CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsDictionary:[self.command createFileTransferError:FILE_NOT_FOUND_ERR AndSource:self.source AndTarget:self.target AndHttpStatus:self.responseCode AndBody:errorMessage]];
+
+    NSLog(@"File Transfer Error: %@", errorMessage);
+    [self cancelTransfer:connection];
+    [self.command.commandDelegate sendPluginResult:result callbackId:callbackId];
+}
+
+- (NSString *)targetFilePath
+{
+    NSString *path = nil;
+    CDVFilesystemURL *sourceURL = [CDVFilesystemURL fileSystemURLWithString:self.target];
+    if (sourceURL && sourceURL.fileSystemName != nil) {
+        // This requires talking to the current CDVFile plugin
+        NSObject<CDVFileSystem> *fs = [self.filePlugin filesystemForURL:sourceURL];
+        path = [fs filesystemPathForURL:sourceURL];
+    } else {
+        // Extract the path part out of a file: URL.
+        path = [self.target hasPrefix:@"/"] ? [self.target copy] : [(NSURL *)[NSURL URLWithString:self.target] path];
+    }
+    return path;
+}
+
+- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response
+{
+    NSError* __autoreleasing error = nil;
+
+    self.mimeType = [response MIMEType];
+    self.targetFileHandle = nil;
+
+    // required for iOS 4.3, for some reason; response is
+    // a plain NSURLResponse, not the HTTP subclass
+    if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
+        NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
+
+        self.responseCode = (int)[httpResponse statusCode];
+        self.bytesExpected = [response expectedContentLength];
+        self.responseHeaders = [httpResponse allHeaderFields];
+        if ((self.direction == CDV_TRANSFER_DOWNLOAD) && (self.responseCode == 200) && (self.bytesExpected == NSURLResponseUnknownLength)) {
+            // Kick off HEAD request to server to get real length
+            // bytesExpected will be updated when that response is returned
+            self.entityLengthRequest = [[CDVFileTransferEntityLengthRequest alloc] initWithOriginalRequest:connection.currentRequest andDelegate:self];
+        }
+    } else if ([response.URL isFileURL]) {
+        NSDictionary* attr = [[NSFileManager defaultManager] attributesOfItemAtPath:[response.URL path] error:nil];
+        self.responseCode = 200;
+        self.bytesExpected = [attr[NSFileSize] longLongValue];
+    } else {
+        self.responseCode = 200;
+        self.bytesExpected = NSURLResponseUnknownLength;
+    }
+    if ((self.direction == CDV_TRANSFER_DOWNLOAD) && (self.responseCode >= 200) && (self.responseCode < 300)) {
+        // Download response is okay; begin streaming output to file
+        NSString *filePath = [self targetFilePath];
+        if (filePath == nil) {
+            // We couldn't find the asset.  Send the appropriate error.
+            [self cancelTransferWithError:connection errorMessage:[NSString stringWithFormat:@"Could not create target file"]];
+            return;
+        }
+
+        NSString* parentPath = [filePath stringByDeletingLastPathComponent];
+
+        // create parent directories if needed
+        if ([[NSFileManager defaultManager] createDirectoryAtPath:parentPath withIntermediateDirectories:YES attributes:nil error:&error] == NO) {
+            if (error) {
+                [self cancelTransferWithError:connection errorMessage:[NSString stringWithFormat:@"Could not create path to save downloaded file: %@", [error localizedDescription]]];
+            } else {
+                [self cancelTransferWithError:connection errorMessage:@"Could not create path to save downloaded file"];
+            }
+            return;
+        }
+        // create target file
+        if ([[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil] == NO) {
+            [self cancelTransferWithError:connection errorMessage:@"Could not create target file"];
+            return;
+        }
+        // open target file for writing
+        self.targetFileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
+        if (self.targetFileHandle == nil) {
+            [self cancelTransferWithError:connection errorMessage:@"Could not open target file for writing"];
+        }
+        DLog(@"Streaming to file %@", filePath);
+    }
+}
+
+- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
+{
+    NSString* body = [[NSString alloc] initWithData:self.responseData encoding:NSUTF8StringEncoding];
+    CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[command createFileTransferError:CONNECTION_ERR AndSource:source AndTarget:target AndHttpStatus:self.responseCode AndBody:body]];
+
+    NSLog(@"File Transfer Error: %@", [error localizedDescription]);
+
+    [self cancelTransfer:connection];
+    [self.command.commandDelegate sendPluginResult:result callbackId:callbackId];
+}
+
+- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
+{
+    self.bytesTransfered += data.length;
+    if (self.targetFileHandle) {
+        [self.targetFileHandle writeData:data];
+    } else {
+        [self.responseData appendData:data];
+    }
+    [self updateProgress];
+}
+
+- (void)updateBytesExpected:(long long)newBytesExpected
+{
+    DLog(@"Updating bytesExpected to %lld", newBytesExpected);
+    self.bytesExpected = newBytesExpected;
+    [self updateProgress];
+}
+
+- (void)updateProgress
+{
+    if (self.direction == CDV_TRANSFER_DOWNLOAD) {
+        BOOL lengthComputable = (self.bytesExpected != NSURLResponseUnknownLength);
+        // If the response is GZipped, and we have an outstanding HEAD request to get
+        // the length, then hold off on sending progress events.
+        if (!lengthComputable && (self.entityLengthRequest != nil)) {
+            return;
+        }
+        NSMutableDictionary* downloadProgress = [NSMutableDictionary dictionaryWithCapacity:3];
+        [downloadProgress setObject:[NSNumber numberWithBool:lengthComputable] forKey:@"lengthComputable"];
+        [downloadProgress setObject:[NSNumber numberWithLongLong:self.bytesTransfered] forKey:@"loaded"];
+        [downloadProgress setObject:[NSNumber numberWithLongLong:self.bytesExpected] forKey:@"total"];
+        CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:downloadProgress];
+        [result setKeepCallbackAsBool:true];
+        [self.command.commandDelegate sendPluginResult:result callbackId:callbackId];
+    }
+}
+
+- (void)connection:(NSURLConnection*)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite
+{
+    if (self.direction == CDV_TRANSFER_UPLOAD) {
+        NSMutableDictionary* uploadProgress = [NSMutableDictionary dictionaryWithCapacity:3];
+
+        [uploadProgress setObject:[NSNumber numberWithBool:true] forKey:@"lengthComputable"];
+        [uploadProgress setObject:[NSNumber numberWithLongLong:totalBytesWritten] forKey:@"loaded"];
+        [uploadProgress setObject:[NSNumber numberWithLongLong:totalBytesExpectedToWrite] forKey:@"total"];
+        CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:uploadProgress];
+        [result setKeepCallbackAsBool:true];
+        [self.command.commandDelegate sendPluginResult:result callbackId:callbackId];
+    }
+    self.bytesTransfered = totalBytesWritten;
+}
+
+// for self signed certificates
+- (void)connection:(NSURLConnection*)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
+{
+    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
+        if (self.trustAllHosts) {
+            NSURLCredential* credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
+            [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
+        }
+        [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
+    } else {
+        [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge];
+    }
+}
+
+- (id)init
+{
+    if ((self = [super init])) {
+        self.responseData = [NSMutableData data];
+        self.targetFileHandle = nil;
+    }
+    return self;
+}
+
+@end

+ 30 - 0
cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file/CDVAssetLibraryFilesystem.h

@@ -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.
+ */
+
+#import "CDVFile.h"
+
+extern NSString* const kCDVAssetsLibraryPrefix;
+extern NSString* const kCDVAssetsLibraryScheme;
+
+@interface CDVAssetLibraryFilesystem : NSObject<CDVFileSystem> {
+}
+
+- (id) initWithName:(NSString *)name;
+
+@end

+ 253 - 0
cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file/CDVAssetLibraryFilesystem.m

@@ -0,0 +1,253 @@
+/*
+ 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 "CDVFile.h"
+#import "CDVAssetLibraryFilesystem.h"
+#import <Cordova/CDV.h>
+#import <AssetsLibrary/ALAsset.h>
+#import <AssetsLibrary/ALAssetRepresentation.h>
+#import <AssetsLibrary/ALAssetsLibrary.h>
+#import <MobileCoreServices/MobileCoreServices.h>
+
+NSString* const kCDVAssetsLibraryPrefix = @"assets-library://";
+NSString* const kCDVAssetsLibraryScheme = @"assets-library";
+
+@implementation CDVAssetLibraryFilesystem
+@synthesize name=_name, urlTransformer;
+
+
+/*
+ The CDVAssetLibraryFilesystem works with resources which are identified
+ by iOS as
+   asset-library://<path>
+ and represents them internally as URLs of the form
+   cdvfile://localhost/assets-library/<path>
+ */
+
+- (NSURL *)assetLibraryURLForLocalURL:(CDVFilesystemURL *)url
+{
+    if ([url.url.scheme isEqualToString:kCDVFilesystemURLPrefix]) {
+        NSString *path = [[url.url absoluteString] substringFromIndex:[@"cdvfile://localhost/assets-library" length]];
+        return [NSURL URLWithString:[NSString stringWithFormat:@"assets-library:/%@", path]];
+    }
+    return url.url;
+}
+
+- (CDVPluginResult *)entryForLocalURI:(CDVFilesystemURL *)url
+{
+    NSDictionary* entry = [self makeEntryForLocalURL:url];
+    return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:entry];
+}
+
+- (NSDictionary *)makeEntryForLocalURL:(CDVFilesystemURL *)url {
+    return [self makeEntryForPath:url.fullPath isDirectory:NO];
+}
+
+- (NSDictionary*)makeEntryForPath:(NSString*)fullPath isDirectory:(BOOL)isDir
+{
+    NSMutableDictionary* dirEntry = [NSMutableDictionary dictionaryWithCapacity:5];
+    NSString* lastPart = [fullPath lastPathComponent];
+    if (isDir && ![fullPath hasSuffix:@"/"]) {
+        fullPath = [fullPath stringByAppendingString:@"/"];
+    }
+    [dirEntry setObject:[NSNumber numberWithBool:!isDir]  forKey:@"isFile"];
+    [dirEntry setObject:[NSNumber numberWithBool:isDir]  forKey:@"isDirectory"];
+    [dirEntry setObject:fullPath forKey:@"fullPath"];
+    [dirEntry setObject:lastPart forKey:@"name"];
+    [dirEntry setObject:self.name forKey: @"filesystemName"];
+
+    NSURL* nativeURL = [NSURL URLWithString:[NSString stringWithFormat:@"assets-library:/%@",fullPath]];
+    if (self.urlTransformer) {
+        nativeURL = self.urlTransformer(nativeURL);
+    }
+    dirEntry[@"nativeURL"] = [nativeURL absoluteString];
+
+    return dirEntry;
+}
+
+/* helper function to get the mimeType from the file extension
+ * IN:
+ *	NSString* fullPath - filename (may include path)
+ * OUT:
+ *	NSString* the mime type as type/subtype.  nil if not able to determine
+ */
++ (NSString*)getMimeTypeFromPath:(NSString*)fullPath
+{
+    NSString* mimeType = nil;
+
+    if (fullPath) {
+        CFStringRef typeId = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[fullPath pathExtension], NULL);
+        if (typeId) {
+            mimeType = (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass(typeId, kUTTagClassMIMEType);
+            if (!mimeType) {
+                // special case for m4a
+                if ([(__bridge NSString*)typeId rangeOfString : @"m4a-audio"].location != NSNotFound) {
+                    mimeType = @"audio/mp4";
+                } else if ([[fullPath pathExtension] rangeOfString:@"wav"].location != NSNotFound) {
+                    mimeType = @"audio/wav";
+                } else if ([[fullPath pathExtension] rangeOfString:@"css"].location != NSNotFound) {
+                    mimeType = @"text/css";
+                }
+            }
+            CFRelease(typeId);
+        }
+    }
+    return mimeType;
+}
+
+- (id)initWithName:(NSString *)name
+{
+    if (self) {
+        self.name = name;
+    }
+    return self;
+}
+
+- (CDVPluginResult *)getFileForURL:(CDVFilesystemURL *)baseURI requestedPath:(NSString *)requestedPath options:(NSDictionary *)options
+{
+    // return unsupported result for assets-library URLs
+   return [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"getFile not supported for assets-library URLs."];
+}
+
+- (CDVPluginResult*)getParentForURL:(CDVFilesystemURL *)localURI
+{
+    // we don't (yet?) support getting the parent of an asset
+    return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_READABLE_ERR];
+}
+
+- (CDVPluginResult*)setMetadataForURL:(CDVFilesystemURL *)localURI withObject:(NSDictionary *)options
+{
+    // setMetadata doesn't make sense for asset library files
+    return [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR];
+}
+
+- (CDVPluginResult *)removeFileAtURL:(CDVFilesystemURL *)localURI
+{
+    // return error for assets-library URLs
+    return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:INVALID_MODIFICATION_ERR];
+}
+
+- (CDVPluginResult *)recursiveRemoveFileAtURL:(CDVFilesystemURL *)localURI
+{
+    // return error for assets-library URLs
+    return [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"removeRecursively not supported for assets-library URLs."];
+}
+
+- (CDVPluginResult *)readEntriesAtURL:(CDVFilesystemURL *)localURI
+{
+    // return unsupported result for assets-library URLs
+    return [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"readEntries not supported for assets-library URLs."];
+}
+
+- (CDVPluginResult *)truncateFileAtURL:(CDVFilesystemURL *)localURI atPosition:(unsigned long long)pos
+{
+    // assets-library files can't be truncated
+    return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NO_MODIFICATION_ALLOWED_ERR];
+}
+
+- (CDVPluginResult *)writeToFileAtURL:(CDVFilesystemURL *)localURL withData:(NSData*)encData append:(BOOL)shouldAppend
+{
+    // text can't be written into assets-library files
+    return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NO_MODIFICATION_ALLOWED_ERR];
+}
+
+- (void)copyFileToURL:(CDVFilesystemURL *)destURL withName:(NSString *)newName fromFileSystem:(NSObject<CDVFileSystem> *)srcFs atURL:(CDVFilesystemURL *)srcURL copy:(BOOL)bCopy callback:(void (^)(CDVPluginResult *))callback
+{
+    // Copying to an assets library file is not doable, since we can't write it.
+    CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:INVALID_MODIFICATION_ERR];
+    callback(result);
+}
+
+- (NSString *)filesystemPathForURL:(CDVFilesystemURL *)url
+{
+    NSString *path = nil;
+    if ([[url.url scheme] isEqualToString:kCDVAssetsLibraryScheme]) {
+        path = [url.url path];
+    } else {
+       path = url.fullPath;
+    }
+    if ([path hasSuffix:@"/"]) {
+      path = [path substringToIndex:([path length]-1)];
+    }
+    return path;
+}
+
+- (void)readFileAtURL:(CDVFilesystemURL *)localURL start:(NSInteger)start end:(NSInteger)end callback:(void (^)(NSData*, NSString* mimeType, CDVFileError))callback
+{
+    ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) {
+        if (asset) {
+            // We have the asset!  Get the data and send it off.
+            ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation];
+            NSUInteger size = (end > start) ? (end - start) : [assetRepresentation size];
+            Byte* buffer = (Byte*)malloc(size);
+            NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:start length:size error:nil];
+            NSData* data = [NSData dataWithBytesNoCopy:buffer length:bufferSize freeWhenDone:YES];
+            NSString* MIMEType = (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)[assetRepresentation UTI], kUTTagClassMIMEType);
+
+            callback(data, MIMEType, NO_ERROR);
+        } else {
+            callback(nil, nil, NOT_FOUND_ERR);
+        }
+    };
+
+    ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) {
+        // Retrieving the asset failed for some reason.  Send the appropriate error.
+        NSLog(@"Error: %@", error);
+        callback(nil, nil, SECURITY_ERR);
+    };
+
+    ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init];
+    [assetsLibrary assetForURL:[self assetLibraryURLForLocalURL:localURL] resultBlock:resultBlock failureBlock:failureBlock];
+}
+
+- (void)getFileMetadataForURL:(CDVFilesystemURL *)localURL callback:(void (^)(CDVPluginResult *))callback
+{
+    // In this case, we need to use an asynchronous method to retrieve the file.
+    // Because of this, we can't just assign to `result` and send it at the end of the method.
+    // Instead, we return after calling the asynchronous method and send `result` in each of the blocks.
+    ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) {
+        if (asset) {
+            // We have the asset!  Populate the dictionary and send it off.
+            NSMutableDictionary* fileInfo = [NSMutableDictionary dictionaryWithCapacity:5];
+            ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation];
+            [fileInfo setObject:[NSNumber numberWithUnsignedLongLong:[assetRepresentation size]] forKey:@"size"];
+            [fileInfo setObject:localURL.fullPath forKey:@"fullPath"];
+            NSString* filename = [assetRepresentation filename];
+            [fileInfo setObject:filename forKey:@"name"];
+            [fileInfo setObject:[CDVAssetLibraryFilesystem getMimeTypeFromPath:filename] forKey:@"type"];
+            NSDate* creationDate = [asset valueForProperty:ALAssetPropertyDate];
+            NSNumber* msDate = [NSNumber numberWithDouble:[creationDate timeIntervalSince1970] * 1000];
+            [fileInfo setObject:msDate forKey:@"lastModifiedDate"];
+
+            callback([CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileInfo]);
+        } else {
+            // We couldn't find the asset.  Send the appropriate error.
+            callback([CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]);
+        }
+    };
+    ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) {
+        // Retrieving the asset failed for some reason.  Send the appropriate error.
+        callback([CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[error localizedDescription]]);
+    };
+
+    ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init];
+    [assetsLibrary assetForURL:[self assetLibraryURLForLocalURL:localURL] resultBlock:resultBlock failureBlock:failureBlock];
+    return;
+}
+@end

+ 157 - 0
cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file/CDVFile.h

@@ -0,0 +1,157 @@
+/*
+ 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 <Foundation/Foundation.h>
+#import <Cordova/CDVPlugin.h>
+
+extern NSString* const kCDVAssetsLibraryPrefix;
+extern NSString* const kCDVFilesystemURLPrefix;
+
+enum CDVFileError {
+    NO_ERROR = 0,
+    NOT_FOUND_ERR = 1,
+    SECURITY_ERR = 2,
+    ABORT_ERR = 3,
+    NOT_READABLE_ERR = 4,
+    ENCODING_ERR = 5,
+    NO_MODIFICATION_ALLOWED_ERR = 6,
+    INVALID_STATE_ERR = 7,
+    SYNTAX_ERR = 8,
+    INVALID_MODIFICATION_ERR = 9,
+    QUOTA_EXCEEDED_ERR = 10,
+    TYPE_MISMATCH_ERR = 11,
+    PATH_EXISTS_ERR = 12
+};
+typedef int CDVFileError;
+
+@interface CDVFilesystemURL : NSObject  {
+    NSURL *_url;
+    NSString *_fileSystemName;
+    NSString *_fullPath;
+}
+
+- (id) initWithString:(NSString*)strURL;
+- (id) initWithURL:(NSURL*)URL;
++ (CDVFilesystemURL *)fileSystemURLWithString:(NSString *)strURL;
++ (CDVFilesystemURL *)fileSystemURLWithURL:(NSURL *)URL;
+
+- (NSString *)absoluteURL;
+
+@property (atomic) NSURL *url;
+@property (atomic) NSString *fileSystemName;
+@property (atomic) NSString *fullPath;
+
+@end
+
+@interface CDVFilesystemURLProtocol : NSURLProtocol
+@end
+
+@protocol CDVFileSystem
+- (CDVPluginResult *)entryForLocalURI:(CDVFilesystemURL *)url;
+- (CDVPluginResult *)getFileForURL:(CDVFilesystemURL *)baseURI requestedPath:(NSString *)requestedPath options:(NSDictionary *)options;
+- (CDVPluginResult *)getParentForURL:(CDVFilesystemURL *)localURI;
+- (CDVPluginResult *)setMetadataForURL:(CDVFilesystemURL *)localURI withObject:(NSDictionary *)options;
+- (CDVPluginResult *)removeFileAtURL:(CDVFilesystemURL *)localURI;
+- (CDVPluginResult *)recursiveRemoveFileAtURL:(CDVFilesystemURL *)localURI;
+- (CDVPluginResult *)readEntriesAtURL:(CDVFilesystemURL *)localURI;
+- (CDVPluginResult *)truncateFileAtURL:(CDVFilesystemURL *)localURI atPosition:(unsigned long long)pos;
+- (CDVPluginResult *)writeToFileAtURL:(CDVFilesystemURL *)localURL withData:(NSData*)encData append:(BOOL)shouldAppend;
+- (void)copyFileToURL:(CDVFilesystemURL *)destURL withName:(NSString *)newName fromFileSystem:(NSObject<CDVFileSystem> *)srcFs atURL:(CDVFilesystemURL *)srcURL copy:(BOOL)bCopy callback:(void (^)(CDVPluginResult *))callback;
+- (void)readFileAtURL:(CDVFilesystemURL *)localURL start:(NSInteger)start end:(NSInteger)end callback:(void (^)(NSData*, NSString* mimeType, CDVFileError))callback;
+- (void)getFileMetadataForURL:(CDVFilesystemURL *)localURL callback:(void (^)(CDVPluginResult *))callback;
+
+- (NSDictionary *)makeEntryForLocalURL:(CDVFilesystemURL *)url;
+- (NSDictionary*)makeEntryForPath:(NSString*)fullPath isDirectory:(BOOL)isDir;
+
+@property (nonatomic,strong) NSString *name;
+@property (nonatomic, copy) NSURL*(^urlTransformer)(NSURL*);
+
+@optional
+- (NSString *)filesystemPathForURL:(CDVFilesystemURL *)localURI;
+- (CDVFilesystemURL *)URLforFilesystemPath:(NSString *)path;
+
+@end
+
+@interface CDVFile : CDVPlugin {
+    NSString* rootDocsPath;
+    NSString* appDocsPath;
+    NSString* appLibraryPath;
+    NSString* appTempPath;
+
+    NSMutableArray* fileSystems_;
+    BOOL userHasAllowed;
+}
+
+- (NSNumber*)checkFreeDiskSpace:(NSString*)appPath;
+- (NSDictionary*)makeEntryForPath:(NSString*)fullPath fileSystemName:(NSString *)fsName isDirectory:(BOOL)isDir;
+- (NSDictionary *)makeEntryForURL:(NSURL *)URL;
+- (CDVFilesystemURL *)fileSystemURLforLocalPath:(NSString *)localPath;
+
+- (NSObject<CDVFileSystem> *)filesystemForURL:(CDVFilesystemURL *)localURL;
+
+/* Native Registration API */
+- (void)registerFilesystem:(NSObject<CDVFileSystem> *)fs;
+- (NSObject<CDVFileSystem> *)fileSystemByName:(NSString *)fsName;
+
+/* Exec API */
+- (void)requestFileSystem:(CDVInvokedUrlCommand*)command;
+- (void)resolveLocalFileSystemURI:(CDVInvokedUrlCommand*)command;
+- (void)getDirectory:(CDVInvokedUrlCommand*)command;
+- (void)getFile:(CDVInvokedUrlCommand*)command;
+- (void)getParent:(CDVInvokedUrlCommand*)command;
+- (void)removeRecursively:(CDVInvokedUrlCommand*)command;
+- (void)remove:(CDVInvokedUrlCommand*)command;
+- (void)copyTo:(CDVInvokedUrlCommand*)command;
+- (void)moveTo:(CDVInvokedUrlCommand*)command;
+- (void)getFileMetadata:(CDVInvokedUrlCommand*)command;
+- (void)readEntries:(CDVInvokedUrlCommand*)command;
+- (void)readAsText:(CDVInvokedUrlCommand*)command;
+- (void)readAsDataURL:(CDVInvokedUrlCommand*)command;
+- (void)readAsArrayBuffer:(CDVInvokedUrlCommand*)command;
+- (void)write:(CDVInvokedUrlCommand*)command;
+- (void)testFileExists:(CDVInvokedUrlCommand*)command;
+- (void)testDirectoryExists:(CDVInvokedUrlCommand*)command;
+- (void)getFreeDiskSpace:(CDVInvokedUrlCommand*)command;
+- (void)truncate:(CDVInvokedUrlCommand*)command;
+- (void)doCopyMove:(CDVInvokedUrlCommand*)command isCopy:(BOOL)bCopy;
+
+/* Compatibilty with older File API */
+- (NSString*)getMimeTypeFromPath:(NSString*)fullPath;
+- (NSDictionary *)getDirectoryEntry:(NSString *)target isDirectory:(BOOL)bDirRequest;
+
+/* Conversion between filesystem paths and URLs */
+- (NSString *)filesystemPathForURL:(CDVFilesystemURL *)URL;
+
+/* Internal methods for testing */
+- (void)_getLocalFilesystemPath:(CDVInvokedUrlCommand*)command;
+
+@property (nonatomic, strong) NSString* rootDocsPath;
+@property (nonatomic, strong) NSString* appDocsPath;
+@property (nonatomic, strong) NSString* appLibraryPath;
+@property (nonatomic, strong) NSString* appTempPath;
+@property (nonatomic, strong) NSString* persistentPath;
+@property (nonatomic, strong) NSString* temporaryPath;
+@property (nonatomic, strong) NSMutableArray* fileSystems;
+
+@property BOOL userHasAllowed;
+
+@end
+
+#define kW3FileTemporary @"temporary"
+#define kW3FilePersistent @"persistent"

+ 1119 - 0
cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file/CDVFile.m

@@ -0,0 +1,1119 @@
+/*
+ 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 <Cordova/CDV.h>
+#import "CDVFile.h"
+#import "CDVLocalFilesystem.h"
+#import "CDVAssetLibraryFilesystem.h"
+#import <objc/message.h>
+
+static NSString* toBase64(NSData* data) {
+    SEL s1 = NSSelectorFromString(@"cdv_base64EncodedString");
+    SEL s2 = NSSelectorFromString(@"base64EncodedString");
+    SEL s3 = NSSelectorFromString(@"base64EncodedStringWithOptions:");
+    
+    if ([data respondsToSelector:s1]) {
+        NSString* (*func)(id, SEL) = (void *)[data methodForSelector:s1];
+        return func(data, s1);
+    } else if ([data respondsToSelector:s2]) {
+        NSString* (*func)(id, SEL) = (void *)[data methodForSelector:s2];
+        return func(data, s2);
+    } else if ([data respondsToSelector:s3]) {
+        NSString* (*func)(id, SEL, NSUInteger) = (void *)[data methodForSelector:s3];
+        return func(data, s3, 0);
+    } else {
+        return nil;
+    }
+}
+
+CDVFile *filePlugin = nil;
+
+extern NSString * const NSURLIsExcludedFromBackupKey __attribute__((weak_import));
+
+#ifndef __IPHONE_5_1
+    NSString* const NSURLIsExcludedFromBackupKey = @"NSURLIsExcludedFromBackupKey";
+#endif
+
+NSString* const kCDVFilesystemURLPrefix = @"cdvfile";
+
+@implementation CDVFilesystemURL
+@synthesize url=_url;
+@synthesize fileSystemName=_fileSystemName;
+@synthesize fullPath=_fullPath;
+
+- (id) initWithString:(NSString *)strURL
+{
+    if ( self = [super init] ) {
+        NSURL *decodedURL = [NSURL URLWithString:strURL];
+        return [self initWithURL:decodedURL];
+    }
+    return nil;
+}
+
+-(id) initWithURL:(NSURL *)URL
+{
+    if ( self = [super init] ) {
+        self.url = URL;
+        self.fileSystemName = [self filesystemNameForLocalURI:URL];
+        self.fullPath = [self fullPathForLocalURI:URL];
+    }
+    return self;
+}
+
+/*
+ * IN
+ *  NSString localURI
+ * OUT
+ *  NSString FileSystem Name for this URI, or nil if it is not recognized.
+ */
+- (NSString *)filesystemNameForLocalURI:(NSURL *)uri
+{
+    if ([[uri scheme] isEqualToString:kCDVFilesystemURLPrefix] && [[uri host] isEqualToString:@"localhost"]) {
+        NSArray *pathComponents = [uri pathComponents];
+        if (pathComponents != nil && pathComponents.count > 1) {
+            return [pathComponents objectAtIndex:1];
+        }
+    } else if ([[uri scheme] isEqualToString:kCDVAssetsLibraryScheme]) {
+        return @"assets-library";
+    }
+    return nil;
+}
+
+/*
+ * IN
+ *  NSString localURI
+ * OUT
+ *  NSString fullPath component suitable for an Entry object.
+ * The incoming URI should be properly escaped. The returned fullPath is unescaped.
+ */
+- (NSString *)fullPathForLocalURI:(NSURL *)uri
+{
+    if ([[uri scheme] isEqualToString:kCDVFilesystemURLPrefix] && [[uri host] isEqualToString:@"localhost"]) {
+        NSString *path = [uri path];
+        if ([uri query]) {
+            path = [NSString stringWithFormat:@"%@?%@", path, [uri query]];
+        }
+        NSRange slashRange = [path rangeOfString:@"/" options:0 range:NSMakeRange(1, path.length-1)];
+        if (slashRange.location == NSNotFound) {
+            return @"";
+        }
+        return [path substringFromIndex:slashRange.location];
+    } else if ([[uri scheme] isEqualToString:kCDVAssetsLibraryScheme]) {
+        return [[uri absoluteString] substringFromIndex:[kCDVAssetsLibraryScheme length]+2];
+    }
+    return nil;
+}
+
++ (CDVFilesystemURL *)fileSystemURLWithString:(NSString *)strURL
+{
+    return [[CDVFilesystemURL alloc] initWithString:strURL];
+}
+
++ (CDVFilesystemURL *)fileSystemURLWithURL:(NSURL *)URL
+{
+    return [[CDVFilesystemURL alloc] initWithURL:URL];
+}
+
+- (NSString *)absoluteURL
+{
+    return [NSString stringWithFormat:@"cdvfile://localhost/%@%@", self.fileSystemName, self.fullPath];
+}
+
+@end
+
+@implementation CDVFilesystemURLProtocol
+
++ (BOOL)canInitWithRequest:(NSURLRequest*)request
+{
+    NSURL* url = [request URL];
+    return [[url scheme] isEqualToString:kCDVFilesystemURLPrefix];
+}
+
++ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request
+{
+    return request;
+}
+
++ (BOOL)requestIsCacheEquivalent:(NSURLRequest*)requestA toRequest:(NSURLRequest*)requestB
+{
+    return [[[requestA URL] resourceSpecifier] isEqualToString:[[requestB URL] resourceSpecifier]];
+}
+
+- (void)startLoading
+{
+    CDVFilesystemURL* url = [CDVFilesystemURL fileSystemURLWithURL:[[self request] URL]];
+    NSObject<CDVFileSystem> *fs = [filePlugin filesystemForURL:url];
+    __weak CDVFilesystemURLProtocol* weakSelf = self;
+    
+    [fs readFileAtURL:url start:0 end:-1 callback:^void(NSData *data, NSString *mimetype, CDVFileError error) {
+        NSMutableDictionary* responseHeaders = [[NSMutableDictionary alloc] init];
+        responseHeaders[@"Cache-Control"] = @"no-cache";
+
+        if (!error) {
+            responseHeaders[@"Content-Type"] = mimetype;
+            NSURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url.url statusCode:200 HTTPVersion:@"HTTP/1.1"headerFields:responseHeaders];
+            [[weakSelf client] URLProtocol:weakSelf didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
+            [[weakSelf client] URLProtocol:weakSelf didLoadData:data];
+            [[weakSelf client] URLProtocolDidFinishLoading:weakSelf];
+        } else {
+            NSURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url.url statusCode:404 HTTPVersion:@"HTTP/1.1"headerFields:responseHeaders];
+            [[weakSelf client] URLProtocol:weakSelf didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
+            [[weakSelf client] URLProtocolDidFinishLoading:weakSelf];
+        }
+    }];
+}
+
+- (void)stopLoading
+{}
+
+- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
+                  willCacheResponse:(NSCachedURLResponse*)cachedResponse {
+    return nil;
+}
+
+@end
+
+
+@implementation CDVFile
+
+@synthesize rootDocsPath, appDocsPath, appLibraryPath, appTempPath, userHasAllowed, fileSystems=fileSystems_;
+
+- (void)registerFilesystem:(NSObject<CDVFileSystem> *)fs {
+    __weak CDVFile* weakSelf = self;
+    SEL sel = NSSelectorFromString(@"urlTransformer");
+    // for backwards compatibility - we check if this property is there
+    // we create a wrapper block because the urlTransformer property
+    // on the commandDelegate might be set dynamically at a future time
+    // (and not dependent on plugin loading order)
+    if ([self.commandDelegate respondsToSelector:sel]) {
+        fs.urlTransformer = ^NSURL*(NSURL* urlToTransform) {
+            // grab the block from the commandDelegate
+            NSURL* (^urlTransformer)(NSURL*) = ((id(*)(id, SEL))objc_msgSend)(weakSelf.commandDelegate, sel);
+            // if block is not null, we call it
+            if (urlTransformer) {
+                return urlTransformer(urlToTransform);
+            } else { // else we return the same url
+                return urlToTransform;
+            }
+        };
+    }
+    [fileSystems_ addObject:fs];
+}
+
+- (NSObject<CDVFileSystem> *)fileSystemByName:(NSString *)fsName
+{
+    if (self.fileSystems != nil) {
+        for (NSObject<CDVFileSystem> *fs in self.fileSystems) {
+            if ([fs.name isEqualToString:fsName]) {
+                return fs;
+            }
+        }
+    }
+    return nil;
+}
+
+- (NSObject<CDVFileSystem> *)filesystemForURL:(CDVFilesystemURL *)localURL {
+    if (localURL.fileSystemName == nil) return nil;
+    @try {
+        return [self fileSystemByName:localURL.fileSystemName];
+    }
+    @catch (NSException *e) {
+        return nil;
+    }
+}
+
+- (NSArray *)getExtraFileSystemsPreference:(UIViewController *)vc
+{
+    NSString *filesystemsStr = nil;
+    if([self.viewController isKindOfClass:[CDVViewController class]]) {
+        CDVViewController *vc = (CDVViewController *)self.viewController;
+        NSDictionary *settings = [vc settings];
+        filesystemsStr = [settings[@"iosextrafilesystems"] lowercaseString];
+    }
+    if (!filesystemsStr) {
+        filesystemsStr = @"library,library-nosync,documents,documents-nosync,cache,bundle,root";
+    }
+    return [filesystemsStr componentsSeparatedByString:@","];
+}
+
+- (void)makeNonSyncable:(NSString*)path {
+    [[NSFileManager defaultManager] createDirectoryAtPath:path
+              withIntermediateDirectories:YES
+                               attributes:nil
+                                    error:nil];
+    NSURL* url = [NSURL fileURLWithPath:path];
+    [url setResourceValue: [NSNumber numberWithBool: YES]
+                   forKey: NSURLIsExcludedFromBackupKey error:nil];
+
+}
+
+- (void)registerExtraFileSystems:(NSArray *)filesystems fromAvailableSet:(NSDictionary *)availableFileSystems
+{
+    NSMutableSet *installedFilesystems = [[NSMutableSet alloc] initWithCapacity:7];
+
+    /* Build non-syncable directories as necessary */
+    for (NSString *nonSyncFS in @[@"library-nosync", @"documents-nosync"]) {
+        if ([filesystems containsObject:nonSyncFS]) {
+            [self makeNonSyncable:availableFileSystems[nonSyncFS]];
+        }
+    }
+
+    /* Register filesystems in order */
+    for (NSString *fsName in filesystems) {
+        if (![installedFilesystems containsObject:fsName]) {
+            NSString *fsRoot = availableFileSystems[fsName];
+            if (fsRoot) {
+                [filePlugin registerFilesystem:[[CDVLocalFilesystem alloc] initWithName:fsName root:fsRoot]];
+                [installedFilesystems addObject:fsName];
+            } else {
+                NSLog(@"Unrecognized extra filesystem identifier: %@", fsName);
+            }
+        }
+    }
+}
+
+- (NSDictionary *)getAvailableFileSystems
+{
+    NSString *libPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
+    NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
+    return @{
+        @"library": libPath,
+        @"library-nosync": [libPath stringByAppendingPathComponent:@"NoCloud"],
+        @"documents": docPath,
+        @"documents-nosync": [docPath stringByAppendingPathComponent:@"NoCloud"],
+        @"cache": [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0],
+        @"bundle": [[NSBundle mainBundle] bundlePath],
+        @"root": @"/"
+    };
+}
+
+- (void)pluginInitialize
+{
+    filePlugin = self;
+    [NSURLProtocol registerClass:[CDVFilesystemURLProtocol class]];
+
+    fileSystems_ = [[NSMutableArray alloc] initWithCapacity:3];
+
+    // Get the Library directory path
+    NSArray* paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
+    self.appLibraryPath = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"files"];
+
+    // Get the Temporary directory path
+    self.appTempPath = [NSTemporaryDirectory()stringByStandardizingPath];   // remove trailing slash from NSTemporaryDirectory()
+
+    // Get the Documents directory path
+    paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+    self.rootDocsPath = [paths objectAtIndex:0];
+    self.appDocsPath = [self.rootDocsPath stringByAppendingPathComponent:@"files"];
+
+
+    NSString *location = nil;
+    if([self.viewController isKindOfClass:[CDVViewController class]]) {
+        CDVViewController *vc = (CDVViewController *)self.viewController;
+        NSMutableDictionary *settings = vc.settings;
+        location = [[settings objectForKey:@"iospersistentfilelocation"] lowercaseString];
+    }
+    if (location == nil) {
+        // Compatibilty by default (if the config preference is not set, or
+        // if we're not embedded in a CDVViewController somehow.)
+        location = @"compatibility";
+    }
+
+    NSError *error;
+    if ([[NSFileManager defaultManager] createDirectoryAtPath:self.appTempPath
+                                  withIntermediateDirectories:YES
+                                                   attributes:nil
+                                                        error:&error]) {
+        [self registerFilesystem:[[CDVLocalFilesystem alloc] initWithName:@"temporary" root:self.appTempPath]];
+    } else {
+        NSLog(@"Unable to create temporary directory: %@", error);
+    }
+    if ([location isEqualToString:@"library"]) {
+        if ([[NSFileManager defaultManager] createDirectoryAtPath:self.appLibraryPath
+                                      withIntermediateDirectories:YES
+                                                       attributes:nil
+                                                            error:&error]) {
+            [self registerFilesystem:[[CDVLocalFilesystem alloc] initWithName:@"persistent" root:self.appLibraryPath]];
+        } else {
+            NSLog(@"Unable to create library directory: %@", error);
+        }
+    } else if ([location isEqualToString:@"compatibility"]) {
+        /*
+         *  Fall-back to compatibility mode -- this is the logic implemented in
+         *  earlier versions of this plugin, and should be maintained here so
+         *  that apps which were originally deployed with older versions of the
+         *  plugin can continue to provide access to files stored under those
+         *  versions.
+         */
+        [self registerFilesystem:[[CDVLocalFilesystem alloc] initWithName:@"persistent" root:self.rootDocsPath]];
+    } else {
+        NSAssert(false,
+            @"File plugin configuration error: Please set iosPersistentFileLocation in config.xml to one of \"library\" (for new applications) or \"compatibility\" (for compatibility with previous versions)");
+    }
+    [self registerFilesystem:[[CDVAssetLibraryFilesystem alloc] initWithName:@"assets-library"]];
+
+    [self registerExtraFileSystems:[self getExtraFileSystemsPreference:self.viewController]
+                  fromAvailableSet:[self getAvailableFileSystems]];
+
+}
+
+- (CDVFilesystemURL *)fileSystemURLforArg:(NSString *)urlArg
+{
+    CDVFilesystemURL* ret = nil;
+    if ([urlArg hasPrefix:@"file://"]) {
+        /* This looks like a file url. Get the path, and see if any handlers recognize it. */
+        NSURL *fileURL = [NSURL URLWithString:urlArg];
+        NSURL *resolvedFileURL = [fileURL URLByResolvingSymlinksInPath];
+        NSString *path = [resolvedFileURL path];
+        ret = [self fileSystemURLforLocalPath:path];
+    } else {
+        ret = [CDVFilesystemURL fileSystemURLWithString:urlArg];
+    }
+    return ret;
+}
+
+- (CDVFilesystemURL *)fileSystemURLforLocalPath:(NSString *)localPath
+{
+    CDVFilesystemURL *localURL = nil;
+    NSUInteger shortestFullPath = 0;
+
+    // Try all installed filesystems, in order. Return the most match url.
+    for (id object in self.fileSystems) {
+        if ([object respondsToSelector:@selector(URLforFilesystemPath:)]) {
+            CDVFilesystemURL *url = [object URLforFilesystemPath:localPath];
+            if (url){
+                // A shorter fullPath would imply that the filesystem is a better match for the local path
+                if (!localURL || ([[url fullPath] length] < shortestFullPath)) {
+                    localURL = url;
+                    shortestFullPath = [[url fullPath] length];
+                }
+            }
+        }
+    }
+    return localURL;
+}
+
+- (NSNumber*)checkFreeDiskSpace:(NSString*)appPath
+{
+    NSFileManager* fMgr = [[NSFileManager alloc] init];
+
+    NSError* __autoreleasing pError = nil;
+
+    NSDictionary* pDict = [fMgr attributesOfFileSystemForPath:appPath error:&pError];
+    NSNumber* pNumAvail = (NSNumber*)[pDict objectForKey:NSFileSystemFreeSize];
+
+    return pNumAvail;
+}
+
+/* Request the File System info
+ *
+ * IN:
+ * arguments[0] - type (number as string)
+ *	TEMPORARY = 0, PERSISTENT = 1;
+ * arguments[1] - size
+ *
+ * OUT:
+ *	Dictionary representing FileSystem object
+ *		name - the human readable directory name
+ *		root = DirectoryEntry object
+ *			bool isDirectory
+ *			bool isFile
+ *			string name
+ *			string fullPath
+ *			fileSystem = FileSystem object - !! ignored because creates circular reference !!
+ */
+
+- (void)requestFileSystem:(CDVInvokedUrlCommand*)command
+{
+    // arguments
+    NSString* strType = [command argumentAtIndex:0];
+    unsigned long long size = [[command argumentAtIndex:1] longLongValue];
+
+    int type = [strType intValue];
+    CDVPluginResult* result = nil;
+
+    if (type >= self.fileSystems.count) {
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:NOT_FOUND_ERR];
+        NSLog(@"No filesystem of type requested");
+    } else {
+        NSString* fullPath = @"/";
+        // check for avail space for size request
+        NSNumber* pNumAvail = [self checkFreeDiskSpace:self.rootDocsPath];
+        // NSLog(@"Free space: %@", [NSString stringWithFormat:@"%qu", [ pNumAvail unsignedLongLongValue ]]);
+        if (pNumAvail && ([pNumAvail unsignedLongLongValue] < size)) {
+            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:QUOTA_EXCEEDED_ERR];
+        } else {
+            NSObject<CDVFileSystem> *rootFs = [self.fileSystems objectAtIndex:type];
+            if (rootFs == nil) {
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:NOT_FOUND_ERR];
+                NSLog(@"No filesystem of type requested");
+            } else {
+                NSMutableDictionary* fileSystem = [NSMutableDictionary dictionaryWithCapacity:2];
+                [fileSystem setObject:rootFs.name forKey:@"name"];
+                NSDictionary* dirEntry = [self makeEntryForPath:fullPath fileSystemName:rootFs.name isDirectory:YES];
+                [fileSystem setObject:dirEntry forKey:@"root"];
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileSystem];
+            }
+        }
+    }
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+
+- (void)requestAllFileSystems:(CDVInvokedUrlCommand*)command
+{
+    NSMutableArray* ret = [[NSMutableArray alloc] init];
+    for (NSObject<CDVFileSystem>* root in fileSystems_) {
+        [ret addObject:[self makeEntryForPath:@"/" fileSystemName:root.name isDirectory:YES]];
+    }
+    CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:ret];
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+- (void)requestAllPaths:(CDVInvokedUrlCommand*)command
+{
+    NSString* libPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES)[0];
+    NSString* libPathSync = [libPath stringByAppendingPathComponent:@"Cloud"];
+    NSString* libPathNoSync = [libPath stringByAppendingPathComponent:@"NoCloud"];
+    NSString* docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
+    NSString* storagePath = [libPath stringByDeletingLastPathComponent];
+    NSString* cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
+
+    // Create the directories if necessary.
+    [[NSFileManager defaultManager] createDirectoryAtPath:libPathSync withIntermediateDirectories:YES attributes:nil error:nil];
+    [[NSFileManager defaultManager] createDirectoryAtPath:libPathNoSync withIntermediateDirectories:YES attributes:nil error:nil];
+    // Mark NoSync as non-iCloud.
+    [[NSURL fileURLWithPath:libPathNoSync] setResourceValue: [NSNumber numberWithBool: YES]
+                                                     forKey: NSURLIsExcludedFromBackupKey error:nil];
+
+    NSDictionary* ret = @{
+        @"applicationDirectory": [[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]] absoluteString],
+        @"applicationStorageDirectory": [[NSURL fileURLWithPath:storagePath] absoluteString],
+        @"dataDirectory": [[NSURL fileURLWithPath:libPathNoSync] absoluteString],
+        @"syncedDataDirectory": [[NSURL fileURLWithPath:libPathSync] absoluteString],
+        @"documentsDirectory": [[NSURL fileURLWithPath:docPath] absoluteString],
+        @"cacheDirectory": [[NSURL fileURLWithPath:cachePath] absoluteString],
+        @"tempDirectory": [[NSURL fileURLWithPath:NSTemporaryDirectory()] absoluteString]
+    };
+
+    CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:ret];
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+/* Creates and returns a dictionary representing an Entry Object
+ *
+ * IN:
+ * NSString* fullPath of the entry
+ * int fsType - FileSystem type
+ * BOOL isDirectory - YES if this is a directory, NO if is a file
+ * OUT:
+ * NSDictionary* Entry object
+ *		bool as NSNumber isDirectory
+ *		bool as NSNumber isFile
+ *		NSString*  name - last part of path
+ *		NSString* fullPath
+ *		NSString* filesystemName - FileSystem name -- actual filesystem will be created on the JS side if necessary, to avoid
+ *         creating circular reference (FileSystem contains DirectoryEntry which contains FileSystem.....!!)
+ */
+- (NSDictionary*)makeEntryForPath:(NSString*)fullPath fileSystemName:(NSString *)fsName isDirectory:(BOOL)isDir
+{
+    NSObject<CDVFileSystem> *fs = [self fileSystemByName:fsName];
+    return [fs makeEntryForPath:fullPath isDirectory:isDir];
+}
+
+- (NSDictionary *)makeEntryForLocalURL:(CDVFilesystemURL *)localURL
+{
+    NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURL];
+    return [fs makeEntryForLocalURL:localURL];
+}
+
+- (NSDictionary *)makeEntryForURL:(NSURL *)URL
+{
+    CDVFilesystemURL* fsURL = [self fileSystemURLforArg:[URL absoluteString]];
+    return [self makeEntryForLocalURL:fsURL];
+}
+
+/*
+ * Given a URI determine the File System information associated with it and return an appropriate W3C entry object
+ * IN
+ *	NSString* localURI: Should be an escaped local filesystem URI
+ * OUT
+ *	Entry object
+ *		bool isDirectory
+ *		bool isFile
+ *		string name
+ *		string fullPath
+ *		fileSystem = FileSystem object - !! ignored because creates circular reference FileSystem contains DirectoryEntry which contains FileSystem.....!!
+ */
+- (void)resolveLocalFileSystemURI:(CDVInvokedUrlCommand*)command
+{
+    // arguments
+    NSString* localURIstr = [command argumentAtIndex:0];
+    CDVPluginResult* result;
+
+    localURIstr = [self encodePath:localURIstr]; //encode path before resolving
+    CDVFilesystemURL* inputURI = [self fileSystemURLforArg:localURIstr];
+
+    if (inputURI == nil || inputURI.fileSystemName == nil) {
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:ENCODING_ERR];
+    } else {
+        NSObject<CDVFileSystem> *fs = [self filesystemForURL:inputURI];
+        if (fs == nil) {
+            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:ENCODING_ERR];
+        } else {
+            result = [fs entryForLocalURI:inputURI];
+        }
+    }
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+//encode path with percent escapes
+-(NSString *)encodePath:(NSString *)path
+{
+    NSString *decodedPath = [path stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; //decode incase it's already encoded to avoid encoding twice
+    return [decodedPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+}
+
+
+/* Part of DirectoryEntry interface,  creates or returns the specified directory
+ * IN:
+ *	NSString* localURI - local filesystem URI for this directory
+ *	NSString* path - directory to be created/returned; may be full path or relative path
+ *	NSDictionary* - Flags object
+ *		boolean as NSNumber create -
+ *			if create is true and directory does not exist, create dir and return directory entry
+ *			if create is true and exclusive is true and directory does exist, return error
+ *			if create is false and directory does not exist, return error
+ *			if create is false and the path represents a file, return error
+ *		boolean as NSNumber exclusive - used in conjunction with create
+ *			if exclusive is true and create is true - specifies failure if directory already exists
+ *
+ *
+ */
+- (void)getDirectory:(CDVInvokedUrlCommand*)command
+{
+    NSMutableArray* arguments = [NSMutableArray arrayWithArray:command.arguments];
+    NSMutableDictionary* options = nil;
+
+    if ([arguments count] >= 3) {
+        options = [command argumentAtIndex:2 withDefault:nil];
+    }
+    // add getDir to options and call getFile()
+    if (options != nil) {
+        options = [NSMutableDictionary dictionaryWithDictionary:options];
+    } else {
+        options = [NSMutableDictionary dictionaryWithCapacity:1];
+    }
+    [options setObject:[NSNumber numberWithInt:1] forKey:@"getDir"];
+    if ([arguments count] >= 3) {
+        [arguments replaceObjectAtIndex:2 withObject:options];
+    } else {
+        [arguments addObject:options];
+    }
+    CDVInvokedUrlCommand* subCommand =
+        [[CDVInvokedUrlCommand alloc] initWithArguments:arguments
+                                             callbackId:command.callbackId
+                                              className:command.className
+                                             methodName:command.methodName];
+
+    [self getFile:subCommand];
+}
+
+/* Part of DirectoryEntry interface,  creates or returns the specified file
+ * IN:
+ *	NSString* baseURI - local filesytem URI for the base directory to search
+ *	NSString* requestedPath - file to be created/returned; may be absolute path or relative path
+ *	NSDictionary* options - Flags object
+ *		boolean as NSNumber create -
+ *			if create is true and file does not exist, create file and return File entry
+ *			if create is true and exclusive is true and file does exist, return error
+ *			if create is false and file does not exist, return error
+ *			if create is false and the path represents a directory, return error
+ *		boolean as NSNumber exclusive - used in conjunction with create
+ *			if exclusive is true and create is true - specifies failure if file already exists
+ */
+- (void)getFile:(CDVInvokedUrlCommand*)command
+{
+    NSString* baseURIstr = [command argumentAtIndex:0];
+    CDVFilesystemURL* baseURI = [self fileSystemURLforArg:baseURIstr];
+    NSString* requestedPath = [command argumentAtIndex:1];
+    NSDictionary* options = [command argumentAtIndex:2 withDefault:nil];
+
+    NSObject<CDVFileSystem> *fs = [self filesystemForURL:baseURI];
+    CDVPluginResult* result = [fs getFileForURL:baseURI requestedPath:requestedPath options:options];
+
+
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+/*
+ * Look up the parent Entry containing this Entry.
+ * If this Entry is the root of its filesystem, its parent is itself.
+ * IN:
+ * NSArray* arguments
+ *	0 - NSString* localURI
+ * NSMutableDictionary* options
+ *	empty
+ */
+- (void)getParent:(CDVInvokedUrlCommand*)command
+{
+    // arguments are URL encoded
+    CDVFilesystemURL* localURI = [self fileSystemURLforArg:command.arguments[0]];
+
+    NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURI];
+    CDVPluginResult* result = [fs getParentForURL:localURI];
+
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+/*
+ * set MetaData of entry
+ * Currently we only support "com.apple.MobileBackup" (boolean)
+ */
+- (void)setMetadata:(CDVInvokedUrlCommand*)command
+{
+    // arguments
+    CDVFilesystemURL* localURI = [self fileSystemURLforArg:command.arguments[0]];
+    NSDictionary* options = [command argumentAtIndex:1 withDefault:nil];
+    NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURI];
+    CDVPluginResult* result = [fs setMetadataForURL:localURI withObject:options];
+
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+/* removes the directory or file entry
+ * IN:
+ * NSArray* arguments
+ *	0 - NSString* localURI
+ *
+ * returns NO_MODIFICATION_ALLOWED_ERR  if is top level directory or no permission to delete dir
+ * returns INVALID_MODIFICATION_ERR if is non-empty dir or asset library file
+ * returns NOT_FOUND_ERR if file or dir is not found
+*/
+- (void)remove:(CDVInvokedUrlCommand*)command
+{
+    // arguments
+    CDVFilesystemURL* localURI = [self fileSystemURLforArg:command.arguments[0]];
+    CDVPluginResult* result = nil;
+
+    if ([localURI.fullPath isEqualToString:@""]) {
+        // error if try to remove top level (documents or tmp) dir
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NO_MODIFICATION_ALLOWED_ERR];
+    } else {
+        NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURI];
+        result = [fs removeFileAtURL:localURI];
+    }
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+/* recursively removes the directory
+ * IN:
+ * NSArray* arguments
+ *	0 - NSString* localURI
+ *
+ * returns NO_MODIFICATION_ALLOWED_ERR  if is top level directory or no permission to delete dir
+ * returns NOT_FOUND_ERR if file or dir is not found
+ */
+- (void)removeRecursively:(CDVInvokedUrlCommand*)command
+{
+    // arguments
+    CDVFilesystemURL* localURI = [self fileSystemURLforArg:command.arguments[0]];
+    CDVPluginResult* result = nil;
+
+    if ([localURI.fullPath isEqualToString:@""]) {
+        // error if try to remove top level (documents or tmp) dir
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NO_MODIFICATION_ALLOWED_ERR];
+    } else {
+        NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURI];
+        result = [fs recursiveRemoveFileAtURL:localURI];
+    }
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+- (void)copyTo:(CDVInvokedUrlCommand*)command
+{
+    [self doCopyMove:command isCopy:YES];
+}
+
+- (void)moveTo:(CDVInvokedUrlCommand*)command
+{
+    [self doCopyMove:command isCopy:NO];
+}
+
+/* Copy/move a file or directory to a new location
+ * IN:
+ * NSArray* arguments
+ *	0 - NSString* URL of entry to copy
+ *  1 - NSString* URL of the directory into which to copy/move the entry
+ *  2 - Optionally, the new name of the entry, defaults to the current name
+ *	BOOL - bCopy YES if copy, NO if move
+ */
+- (void)doCopyMove:(CDVInvokedUrlCommand*)command isCopy:(BOOL)bCopy
+{
+    NSArray* arguments = command.arguments;
+
+    // arguments
+    NSString* srcURLstr = [command argumentAtIndex:0];
+    NSString* destURLstr = [command argumentAtIndex:1];
+
+    CDVPluginResult *result;
+
+    if (!srcURLstr || !destURLstr) {
+        // either no source or no destination provided
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR];
+        [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+        return;
+    }
+
+    CDVFilesystemURL* srcURL = [self fileSystemURLforArg:srcURLstr];
+    CDVFilesystemURL* destURL = [self fileSystemURLforArg:destURLstr];
+
+    NSObject<CDVFileSystem> *srcFs = [self filesystemForURL:srcURL];
+    NSObject<CDVFileSystem> *destFs = [self filesystemForURL:destURL];
+
+    // optional argument; use last component from srcFullPath if new name not provided
+    NSString* newName = ([arguments count] > 2) ? [command argumentAtIndex:2] : [srcURL.url lastPathComponent];
+    if ([newName rangeOfString:@":"].location != NSNotFound) {
+        // invalid chars in new name
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:ENCODING_ERR];
+        [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+        return;
+    }
+
+    __weak CDVFile* weakSelf = self;
+    [self.commandDelegate runInBackground:^ {
+        [destFs copyFileToURL:destURL withName:newName fromFileSystem:srcFs atURL:srcURL copy:bCopy callback:^(CDVPluginResult* result) {
+            [weakSelf.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+        }];
+    }];
+
+}
+
+- (void)getFileMetadata:(CDVInvokedUrlCommand*)command
+{
+    // arguments
+    CDVFilesystemURL* localURI = [self fileSystemURLforArg:command.arguments[0]];
+    NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURI];
+    __weak CDVFile* weakSelf = self;
+    [fs getFileMetadataForURL:localURI callback:^(CDVPluginResult* result) {
+        [weakSelf.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+    }];
+}
+
+- (void)readEntries:(CDVInvokedUrlCommand*)command
+{
+    CDVFilesystemURL* localURI = [self fileSystemURLforArg:command.arguments[0]];
+    NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURI];
+    CDVPluginResult *result = [fs readEntriesAtURL:localURI];
+
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+/* read and return file data
+ * IN:
+ * NSArray* arguments
+ *	0 - NSString* fullPath
+ *	1 - NSString* encoding
+ *	2 - NSString* start
+ *	3 - NSString* end
+ */
+- (void)readAsText:(CDVInvokedUrlCommand*)command
+{
+    // arguments
+    CDVFilesystemURL* localURI = [self fileSystemURLforArg:command.arguments[0]];
+    NSString* encoding = [command argumentAtIndex:1];
+    NSInteger start = [[command argumentAtIndex:2] integerValue];
+    NSInteger end = [[command argumentAtIndex:3] integerValue];
+
+    NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURI];
+
+    if (fs == nil) {
+        CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR];
+        [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+        return;
+    }
+
+    // TODO: implement
+    if ([@"UTF-8" caseInsensitiveCompare : encoding] != NSOrderedSame) {
+        NSLog(@"Only UTF-8 encodings are currently supported by readAsText");
+        CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:ENCODING_ERR];
+        [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+        return;
+    }
+
+    __weak CDVFile* weakSelf = self;
+
+    [self.commandDelegate runInBackground:^ {
+        [fs readFileAtURL:localURI start:start end:end callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) {
+            CDVPluginResult* result = nil;
+            if (data != nil) {
+                NSString* str = [[NSString alloc] initWithBytesNoCopy:(void*)[data bytes] length:[data length] encoding:NSUTF8StringEncoding freeWhenDone:NO];
+                // Check that UTF8 conversion did not fail.
+                if (str != nil) {
+                    result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:str];
+                    result.associatedObject = data;
+                } else {
+                    errorCode = ENCODING_ERR;
+                }
+            }
+            if (result == nil) {
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode];
+            }
+
+            [weakSelf.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+        }];
+    }];
+}
+
+/* Read content of text file and return as base64 encoded data url.
+ * IN:
+ * NSArray* arguments
+ *	0 - NSString* fullPath
+ *	1 - NSString* start
+ *	2 - NSString* end
+ *
+ * Determines the mime type from the file extension, returns ENCODING_ERR if mimetype can not be determined.
+ */
+
+- (void)readAsDataURL:(CDVInvokedUrlCommand*)command
+{
+    CDVFilesystemURL* localURI = [self fileSystemURLforArg:command.arguments[0]];
+    NSInteger start = [[command argumentAtIndex:1] integerValue];
+    NSInteger end = [[command argumentAtIndex:2] integerValue];
+
+    NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURI];
+
+    __weak CDVFile* weakSelf = self;
+    [self.commandDelegate runInBackground:^ {
+        [fs readFileAtURL:localURI start:start end:end callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) {
+            CDVPluginResult* result = nil;
+            if (data != nil) {
+                NSString* b64Str = toBase64(data);
+                NSString* output = [NSString stringWithFormat:@"data:%@;base64,%@", mimeType, b64Str];
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:output];
+            } else {
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode];
+            }
+
+            [weakSelf.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+        }];
+    }];
+}
+
+/* Read content of text file and return as an arraybuffer
+ * IN:
+ * NSArray* arguments
+ *	0 - NSString* fullPath
+ *	1 - NSString* start
+ *	2 - NSString* end
+ */
+
+- (void)readAsArrayBuffer:(CDVInvokedUrlCommand*)command
+{
+    CDVFilesystemURL* localURI = [self fileSystemURLforArg:command.arguments[0]];
+    NSInteger start = [[command argumentAtIndex:1] integerValue];
+    NSInteger end = [[command argumentAtIndex:2] integerValue];
+
+    NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURI];
+
+    __weak CDVFile* weakSelf = self;
+
+    [self.commandDelegate runInBackground:^ {
+        [fs readFileAtURL:localURI start:start end:end callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) {
+            CDVPluginResult* result = nil;
+            if (data != nil) {
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArrayBuffer:data];
+            } else {
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode];
+            }
+
+            [weakSelf.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+        }];
+    }];
+}
+
+- (void)readAsBinaryString:(CDVInvokedUrlCommand*)command
+{
+    CDVFilesystemURL* localURI = [self fileSystemURLforArg:command.arguments[0]];
+    NSInteger start = [[command argumentAtIndex:1] integerValue];
+    NSInteger end = [[command argumentAtIndex:2] integerValue];
+
+    NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURI];
+
+    __weak CDVFile* weakSelf = self;
+
+    [self.commandDelegate runInBackground:^ {
+        [fs readFileAtURL:localURI start:start end:end callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) {
+            CDVPluginResult* result = nil;
+            if (data != nil) {
+                NSString* payload = [[NSString alloc] initWithBytesNoCopy:(void*)[data bytes] length:[data length] encoding:NSASCIIStringEncoding freeWhenDone:NO];
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:payload];
+                result.associatedObject = data;
+            } else {
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode];
+            }
+
+            [weakSelf.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+        }];
+    }];
+}
+
+
+- (void)truncate:(CDVInvokedUrlCommand*)command
+{
+    // arguments
+    CDVFilesystemURL* localURI = [self fileSystemURLforArg:command.arguments[0]];
+    unsigned long long pos = (unsigned long long)[[command argumentAtIndex:1] longLongValue];
+
+    NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURI];
+    CDVPluginResult *result = [fs truncateFileAtURL:localURI atPosition:pos];
+
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+/* write
+ * IN:
+ * NSArray* arguments
+ *  0 - NSString* localURI of file to write to
+ *  1 - NSString* or NSData* data to write
+ *  2 - NSNumber* position to begin writing
+ */
+- (void)write:(CDVInvokedUrlCommand*)command
+{
+    __weak CDVFile* weakSelf = self;
+
+    [self.commandDelegate runInBackground:^ {
+        NSString* callbackId = command.callbackId;
+
+        // arguments
+        CDVFilesystemURL* localURI = [self fileSystemURLforArg:command.arguments[0]];
+        id argData = [command argumentAtIndex:1];
+        unsigned long long pos = (unsigned long long)[[command argumentAtIndex:2] longLongValue];
+
+        NSObject<CDVFileSystem> *fs = [self filesystemForURL:localURI];
+
+
+        [fs truncateFileAtURL:localURI atPosition:pos];
+        CDVPluginResult *result;
+        if ([argData isKindOfClass:[NSString class]]) {
+            NSData *encData = [argData dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
+            result = [fs writeToFileAtURL:localURI withData:encData append:YES];
+        } else if ([argData isKindOfClass:[NSData class]]) {
+            result = [fs writeToFileAtURL:localURI withData:argData append:YES];
+        } else {
+            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Invalid parameter type"];
+        }
+        [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId];
+    }];
+}
+
+#pragma mark Methods for converting between URLs and paths
+
+- (NSString *)filesystemPathForURL:(CDVFilesystemURL *)localURL
+{
+    for (NSObject<CDVFileSystem> *fs in self.fileSystems) {
+        if ([fs.name isEqualToString:localURL.fileSystemName]) {
+            if ([fs respondsToSelector:@selector(filesystemPathForURL:)]) {
+                return [fs filesystemPathForURL:localURL];
+            }
+        }
+    }
+    return nil;
+}
+
+#pragma mark Undocumented Filesystem API
+
+- (void)testFileExists:(CDVInvokedUrlCommand*)command
+{
+    // arguments
+    NSString* argPath = [command argumentAtIndex:0];
+
+    // Get the file manager
+    NSFileManager* fMgr = [NSFileManager defaultManager];
+    NSString* appFile = argPath; // [ self getFullPath: argPath];
+
+    BOOL bExists = [fMgr fileExistsAtPath:appFile];
+    CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:(bExists ? 1 : 0)];
+
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+- (void)testDirectoryExists:(CDVInvokedUrlCommand*)command
+{
+    // arguments
+    NSString* argPath = [command argumentAtIndex:0];
+
+    // Get the file manager
+    NSFileManager* fMgr = [[NSFileManager alloc] init];
+    NSString* appFile = argPath; // [self getFullPath: argPath];
+    BOOL bIsDir = NO;
+    BOOL bExists = [fMgr fileExistsAtPath:appFile isDirectory:&bIsDir];
+
+    CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:((bExists && bIsDir) ? 1 : 0)];
+
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+// Returns number of bytes available via callback
+- (void)getFreeDiskSpace:(CDVInvokedUrlCommand*)command
+{
+    // no arguments
+    
+    NSNumber* pNumAvail = [self checkFreeDiskSpace:self.rootDocsPath];
+
+    NSString* strFreeSpace = [NSString stringWithFormat:@"%qu", [pNumAvail unsignedLongLongValue]];
+    // NSLog(@"Free space is %@", strFreeSpace );
+
+    CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:strFreeSpace];
+
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+#pragma mark Compatibility with older File API
+
+- (NSString*)getMimeTypeFromPath:(NSString*)fullPath
+{
+    return [CDVLocalFilesystem getMimeTypeFromPath:fullPath];
+}
+
+- (NSDictionary *)getDirectoryEntry:(NSString *)localPath isDirectory:(BOOL)bDirRequest
+{
+    CDVFilesystemURL *localURL = [self fileSystemURLforLocalPath:localPath];
+    return [self makeEntryForPath:localURL.fullPath fileSystemName:localURL.fileSystemName isDirectory:bDirRequest];
+}
+
+#pragma mark Internal methods for testing
+// Internal methods for testing: Get the on-disk location of a local filesystem url.
+// [Currently used for testing file-transfer]
+
+- (void)_getLocalFilesystemPath:(CDVInvokedUrlCommand*)command
+{
+    CDVFilesystemURL* localURL = [self fileSystemURLforArg:command.arguments[0]];
+
+    NSString* fsPath = [self filesystemPathForURL:localURL];
+    CDVPluginResult* result;
+    if (fsPath) {
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:fsPath];
+    } else {
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Cannot resolve URL to a file"];
+    }
+    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
+}
+
+@end

+ 32 - 0
cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file/CDVLocalFilesystem.h

@@ -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.
+ */
+
+#import "CDVFile.h"
+
+@interface CDVLocalFilesystem : NSObject<CDVFileSystem> {
+    NSString *_name;
+    NSString *_fsRoot;
+}
+
+- (id) initWithName:(NSString *)name root:(NSString *)fsRoot;
++ (NSString*)getMimeTypeFromPath:(NSString*)fullPath;
+
+@property (nonatomic,strong) NSString *fsRoot;
+
+@end

+ 750 - 0
cordova/platforms/ios/美天旺/Plugins/cordova-plugin-file/CDVLocalFilesystem.m

@@ -0,0 +1,750 @@
+/*
+ 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 "CDVFile.h"
+#import "CDVLocalFilesystem.h"
+#import <Cordova/CDV.h>
+#import <MobileCoreServices/MobileCoreServices.h>
+#import <sys/xattr.h>
+
+@implementation CDVLocalFilesystem
+@synthesize name=_name, fsRoot=_fsRoot, urlTransformer;
+
+- (id) initWithName:(NSString *)name root:(NSString *)fsRoot
+{
+    if (self) {
+        self.name = name;
+        self.fsRoot = fsRoot;
+    }
+    return self;
+}
+
+/*
+ * IN
+ *  NSString localURI
+ * OUT
+ *  CDVPluginResult result containing a file or directoryEntry for the localURI, or an error if the
+ *   URI represents a non-existent path, or is unrecognized or otherwise malformed.
+ */
+- (CDVPluginResult *)entryForLocalURI:(CDVFilesystemURL *)url
+{
+    CDVPluginResult* result = nil;
+    NSDictionary* entry = [self makeEntryForLocalURL:url];
+    if (entry) {
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:entry];
+    } else {
+        // return NOT_FOUND_ERR
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR];
+    }
+    return result;
+}
+- (NSDictionary *)makeEntryForLocalURL:(CDVFilesystemURL *)url {
+    NSString *path = [self filesystemPathForURL:url];
+    NSFileManager* fileMgr = [[NSFileManager alloc] init];
+    BOOL isDir = NO;
+    // see if exists and is file or dir
+    BOOL bExists = [fileMgr fileExistsAtPath:path isDirectory:&isDir];
+    if (bExists) {
+        return [self makeEntryForPath:url.fullPath isDirectory:isDir];
+    } else {
+        return nil;
+    }
+}
+- (NSDictionary*)makeEntryForPath:(NSString*)fullPath isDirectory:(BOOL)isDir
+{
+    NSMutableDictionary* dirEntry = [NSMutableDictionary dictionaryWithCapacity:5];
+    NSString* lastPart = [[self stripQueryParametersFromPath:fullPath] lastPathComponent];
+    if (isDir && ![fullPath hasSuffix:@"/"]) {
+        fullPath = [fullPath stringByAppendingString:@"/"];
+    }
+    [dirEntry setObject:[NSNumber numberWithBool:!isDir]  forKey:@"isFile"];
+    [dirEntry setObject:[NSNumber numberWithBool:isDir]  forKey:@"isDirectory"];
+    [dirEntry setObject:fullPath forKey:@"fullPath"];
+    [dirEntry setObject:lastPart forKey:@"name"];
+    [dirEntry setObject:self.name forKey: @"filesystemName"];
+
+    NSURL* nativeURL = [NSURL fileURLWithPath:[self filesystemPathForFullPath:fullPath]];
+    if (self.urlTransformer) {
+        nativeURL = self.urlTransformer(nativeURL);
+    }
+
+    dirEntry[@"nativeURL"] = [nativeURL absoluteString];
+
+    return dirEntry;
+}
+
+- (NSString *)stripQueryParametersFromPath:(NSString *)fullPath
+{
+    NSRange questionMark = [fullPath rangeOfString:@"?"];
+    if (questionMark.location != NSNotFound) {
+        return [fullPath substringWithRange:NSMakeRange(0,questionMark.location)];
+    }
+    return fullPath;
+}
+
+- (NSString *)filesystemPathForFullPath:(NSString *)fullPath
+{
+    NSString *path = nil;
+    NSString *strippedFullPath = [self stripQueryParametersFromPath:fullPath];
+    path = [NSString stringWithFormat:@"%@%@", self.fsRoot, strippedFullPath];
+    if ([path length] > 1 && [path hasSuffix:@"/"]) {
+      path = [path substringToIndex:([path length]-1)];
+    }
+    return path;
+}
+/*
+ * IN
+ *  NSString localURI
+ * OUT
+ *  NSString full local filesystem path for the represented file or directory, or nil if no such path is possible
+ *  The file or directory does not necessarily have to exist. nil is returned if the filesystem type is not recognized,
+ *  or if the URL is malformed.
+ * The incoming URI should be properly escaped (no raw spaces, etc. URI percent-encoding is expected).
+ */
+- (NSString *)filesystemPathForURL:(CDVFilesystemURL *)url
+{
+    return [self filesystemPathForFullPath:url.fullPath];
+}
+
+- (CDVFilesystemURL *)URLforFullPath:(NSString *)fullPath
+{
+    if (fullPath) {
+        NSString* escapedPath = [fullPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+        if ([fullPath hasPrefix:@"/"]) {
+            return [CDVFilesystemURL fileSystemURLWithString:[NSString stringWithFormat:@"%@://localhost/%@%@", kCDVFilesystemURLPrefix, self.name, escapedPath]];
+        }
+        return [CDVFilesystemURL fileSystemURLWithString:[NSString stringWithFormat:@"%@://localhost/%@/%@", kCDVFilesystemURLPrefix, self.name, escapedPath]];
+    }
+    return nil;
+}
+
+- (CDVFilesystemURL *)URLforFilesystemPath:(NSString *)path
+{
+    return [self URLforFullPath:[self fullPathForFileSystemPath:path]];
+
+}
+
+- (NSString *)normalizePath:(NSString *)rawPath
+{
+    // If this is an absolute path, the first path component will be '/'. Skip it if that's the case
+    BOOL isAbsolutePath = [rawPath hasPrefix:@"/"];
+    if (isAbsolutePath) {
+        rawPath = [rawPath substringFromIndex:1];
+    }
+    NSMutableArray *components = [NSMutableArray arrayWithArray:[rawPath pathComponents]];
+    for (int index = 0; index < [components count]; ++index) {
+        if ([[components objectAtIndex:index] isEqualToString:@".."]) {
+            [components removeObjectAtIndex:index];
+            if (index > 0) {
+                [components removeObjectAtIndex:index-1];
+                --index;
+            }
+        }
+    }
+
+    if (isAbsolutePath) {
+        return [NSString stringWithFormat:@"/%@", [components componentsJoinedByString:@"/"]];
+    } else {
+        return [components componentsJoinedByString:@"/"];
+    }
+
+
+}
+
+- (BOOL)valueForKeyIsNumber:(NSDictionary*)dict key:(NSString*)key
+{
+    BOOL bNumber = NO;
+    NSObject* value = dict[key];
+    if (value) {
+        bNumber = [value isKindOfClass:[NSNumber class]];
+    }
+    return bNumber;
+}
+
+- (CDVPluginResult *)getFileForURL:(CDVFilesystemURL *)baseURI requestedPath:(NSString *)requestedPath options:(NSDictionary *)options
+{
+    CDVPluginResult* result = nil;
+    BOOL bDirRequest = NO;
+    BOOL create = NO;
+    BOOL exclusive = NO;
+    int errorCode = 0;  // !!! risky - no error code currently defined for 0
+
+    if ([self valueForKeyIsNumber:options key:@"create"]) {
+        create = [(NSNumber*)[options valueForKey:@"create"] boolValue];
+    }
+    if ([self valueForKeyIsNumber:options key:@"exclusive"]) {
+        exclusive = [(NSNumber*)[options valueForKey:@"exclusive"] boolValue];
+    }
+    if ([self valueForKeyIsNumber:options key:@"getDir"]) {
+        // this will not exist for calls directly to getFile but will have been set by getDirectory before calling this method
+        bDirRequest = [(NSNumber*)[options valueForKey:@"getDir"] boolValue];
+    }
+    // see if the requested path has invalid characters - should we be checking for  more than just ":"?
+    if ([requestedPath rangeOfString:@":"].location != NSNotFound) {
+        errorCode = ENCODING_ERR;
+    } else {
+        // Build new fullPath for the requested resource.
+        // We concatenate the two paths together, and then scan the resulting string to remove
+        // parent ("..") references. Any parent references at the beginning of the string are
+        // silently removed.
+        NSString *combinedPath = [baseURI.fullPath stringByAppendingPathComponent:requestedPath];
+        combinedPath = [self normalizePath:combinedPath];
+        CDVFilesystemURL* requestedURL = [self URLforFullPath:combinedPath];
+
+        NSFileManager* fileMgr = [[NSFileManager alloc] init];
+        BOOL bIsDir;
+        BOOL bExists = [fileMgr fileExistsAtPath:[self filesystemPathForURL:requestedURL] isDirectory:&bIsDir];
+        if (bExists && (create == NO) && (bIsDir == !bDirRequest)) {
+            // path exists and is not of requested type  - return TYPE_MISMATCH_ERR
+            errorCode = TYPE_MISMATCH_ERR;
+        } else if (!bExists && (create == NO)) {
+            // path does not exist and create is false - return NOT_FOUND_ERR
+            errorCode = NOT_FOUND_ERR;
+        } else if (bExists && (create == YES) && (exclusive == YES)) {
+            // file/dir already exists and exclusive and create are both true - return PATH_EXISTS_ERR
+            errorCode = PATH_EXISTS_ERR;
+        } else {
+            // if bExists and create == YES - just return data
+            // if bExists and create == NO  - just return data
+            // if !bExists and create == YES - create and return data
+            BOOL bSuccess = YES;
+            NSError __autoreleasing* pError = nil;
+            if (!bExists && (create == YES)) {
+                if (bDirRequest) {
+                    // create the dir
+                    bSuccess = [fileMgr createDirectoryAtPath:[self filesystemPathForURL:requestedURL] withIntermediateDirectories:NO attributes:nil error:&pError];
+                } else {
+                    // create the empty file
+                    bSuccess = [fileMgr createFileAtPath:[self filesystemPathForURL:requestedURL] contents:nil attributes:nil];
+                }
+            }
+            if (!bSuccess) {
+                errorCode = ABORT_ERR;
+                if (pError) {
+                    NSLog(@"error creating directory: %@", [pError localizedDescription]);
+                }
+            } else {
+                // NSLog(@"newly created file/dir (%@) exists: %d", reqFullPath, [fileMgr fileExistsAtPath:reqFullPath]);
+                // file existed or was created
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self makeEntryForPath:requestedURL.fullPath isDirectory:bDirRequest]];
+            }
+        } // are all possible conditions met?
+    }
+
+    if (errorCode > 0) {
+        // create error callback
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode];
+    }
+    return result;
+
+}
+
+- (CDVPluginResult*)getParentForURL:(CDVFilesystemURL *)localURI
+{
+    CDVPluginResult* result = nil;
+    CDVFilesystemURL *newURI = nil;
+    if ([localURI.fullPath isEqualToString:@""]) {
+        // return self
+        newURI = localURI;
+    } else {
+        newURI = [CDVFilesystemURL fileSystemURLWithURL:[localURI.url URLByDeletingLastPathComponent]]; /* TODO: UGLY - FIX */
+    }
+    NSFileManager* fileMgr = [[NSFileManager alloc] init];
+    BOOL bIsDir;
+    BOOL bExists = [fileMgr fileExistsAtPath:[self filesystemPathForURL:newURI] isDirectory:&bIsDir];
+    if (bExists) {
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self makeEntryForPath:newURI.fullPath isDirectory:bIsDir]];
+    } else {
+        // invalid path or file does not exist
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR];
+    }
+    return result;
+}
+
+- (CDVPluginResult*)setMetadataForURL:(CDVFilesystemURL *)localURI withObject:(NSDictionary *)options
+{
+    BOOL ok = NO;
+
+    NSString* filePath = [self filesystemPathForURL:localURI];
+    // we only care about this iCloud key for now.
+    // set to 1/true to skip backup, set to 0/false to back it up (effectively removing the attribute)
+    NSString* iCloudBackupExtendedAttributeKey = @"com.apple.MobileBackup";
+    id iCloudBackupExtendedAttributeValue = [options objectForKey:iCloudBackupExtendedAttributeKey];
+
+    if ((iCloudBackupExtendedAttributeValue != nil) && [iCloudBackupExtendedAttributeValue isKindOfClass:[NSNumber class]]) {
+        if (IsAtLeastiOSVersion(@"5.1")) {
+            NSURL* url = [NSURL fileURLWithPath:filePath];
+            NSError* __autoreleasing error = nil;
+
+            ok = [url setResourceValue:[NSNumber numberWithBool:[iCloudBackupExtendedAttributeValue boolValue]] forKey:NSURLIsExcludedFromBackupKey error:&error];
+        } else { // below 5.1 (deprecated - only really supported in 5.01)
+            u_int8_t value = [iCloudBackupExtendedAttributeValue intValue];
+            if (value == 0) { // remove the attribute (allow backup, the default)
+                ok = (removexattr([filePath fileSystemRepresentation], [iCloudBackupExtendedAttributeKey cStringUsingEncoding:NSUTF8StringEncoding], 0) == 0);
+            } else { // set the attribute (skip backup)
+                ok = (setxattr([filePath fileSystemRepresentation], [iCloudBackupExtendedAttributeKey cStringUsingEncoding:NSUTF8StringEncoding], &value, sizeof(value), 0, 0) == 0);
+            }
+        }
+    }
+
+    if (ok) {
+        return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
+    } else {
+        return [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR];
+    }
+}
+
+/* remove the file or directory (recursively)
+ * IN:
+ * NSString* fullPath - the full path to the file or directory to be removed
+ * NSString* callbackId
+ * called from remove and removeRecursively - check all pubic api specific error conditions (dir not empty, etc) before calling
+ */
+
+- (CDVPluginResult*)doRemove:(NSString*)fullPath
+{
+    CDVPluginResult* result = nil;
+    BOOL bSuccess = NO;
+    NSError* __autoreleasing pError = nil;
+    NSFileManager* fileMgr = [[NSFileManager alloc] init];
+
+    @try {
+        bSuccess = [fileMgr removeItemAtPath:fullPath error:&pError];
+        if (bSuccess) {
+            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
+        } else {
+            // see if we can give a useful error
+            CDVFileError errorCode = ABORT_ERR;
+            NSLog(@"error removing filesystem entry at %@: %@", fullPath, [pError localizedDescription]);
+            if ([pError code] == NSFileNoSuchFileError) {
+                errorCode = NOT_FOUND_ERR;
+            } else if ([pError code] == NSFileWriteNoPermissionError) {
+                errorCode = NO_MODIFICATION_ALLOWED_ERR;
+            }
+
+            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode];
+        }
+    } @catch(NSException* e) {  // NSInvalidArgumentException if path is . or ..
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:SYNTAX_ERR];
+    }
+
+    return result;
+}
+
+- (CDVPluginResult *)removeFileAtURL:(CDVFilesystemURL *)localURI
+{
+    NSString *fileSystemPath = [self filesystemPathForURL:localURI];
+
+    NSFileManager* fileMgr = [[NSFileManager alloc] init];
+    BOOL bIsDir = NO;
+    BOOL bExists = [fileMgr fileExistsAtPath:fileSystemPath isDirectory:&bIsDir];
+    if (!bExists) {
+        return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR];
+    }
+    if (bIsDir && ([[fileMgr contentsOfDirectoryAtPath:fileSystemPath error:nil] count] != 0)) {
+        // dir is not empty
+        return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:INVALID_MODIFICATION_ERR];
+    }
+    return [self doRemove:fileSystemPath];
+}
+
+- (CDVPluginResult *)recursiveRemoveFileAtURL:(CDVFilesystemURL *)localURI
+{
+    NSString *fileSystemPath = [self filesystemPathForURL:localURI];
+    return [self doRemove:fileSystemPath];
+}
+
+/*
+ * IN
+ *  NSString localURI
+ * OUT
+ *  NSString full local filesystem path for the represented file or directory, or nil if no such path is possible
+ *  The file or directory does not necessarily have to exist. nil is returned if the filesystem type is not recognized,
+ *  or if the URL is malformed.
+ * The incoming URI should be properly escaped (no raw spaces, etc. URI percent-encoding is expected).
+ */
+- (NSString *)fullPathForFileSystemPath:(NSString *)fsPath
+{
+    if ([fsPath hasPrefix:self.fsRoot]) {
+        return [fsPath substringFromIndex:[self.fsRoot length]];
+    }
+    return nil;
+}
+
+
+- (CDVPluginResult *)readEntriesAtURL:(CDVFilesystemURL *)localURI
+{
+    NSFileManager* fileMgr = [[NSFileManager alloc] init];
+    NSError* __autoreleasing error = nil;
+    NSString *fileSystemPath = [self filesystemPathForURL:localURI];
+
+    NSArray* contents = [fileMgr contentsOfDirectoryAtPath:fileSystemPath error:&error];
+
+    if (contents) {
+        NSMutableArray* entries = [NSMutableArray arrayWithCapacity:1];
+        if ([contents count] > 0) {
+            // create an Entry (as JSON) for each file/dir
+            for (NSString* name in contents) {
+                // see if is dir or file
+                NSString* entryPath = [fileSystemPath stringByAppendingPathComponent:name];
+                BOOL bIsDir = NO;
+                [fileMgr fileExistsAtPath:entryPath isDirectory:&bIsDir];
+                NSDictionary* entryDict = [self makeEntryForPath:[self fullPathForFileSystemPath:entryPath] isDirectory:bIsDir];
+                [entries addObject:entryDict];
+            }
+        }
+        return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:entries];
+    } else {
+        // assume not found but could check error for more specific error conditions
+        return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR];
+    }
+}
+
+- (unsigned long long)truncateFile:(NSString*)filePath atPosition:(unsigned long long)pos
+{
+    unsigned long long newPos = 0UL;
+
+    NSFileHandle* file = [NSFileHandle fileHandleForWritingAtPath:filePath];
+
+    if (file) {
+        [file truncateFileAtOffset:(unsigned long long)pos];
+        newPos = [file offsetInFile];
+        [file synchronizeFile];
+        [file closeFile];
+    }
+    return newPos;
+}
+
+- (CDVPluginResult *)truncateFileAtURL:(CDVFilesystemURL *)localURI atPosition:(unsigned long long)pos
+{
+    unsigned long long newPos = [self truncateFile:[self filesystemPathForURL:localURI] atPosition:pos];
+    return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:(int)newPos];
+}
+
+- (CDVPluginResult *)writeToFileAtURL:(CDVFilesystemURL *)localURL withData:(NSData*)encData append:(BOOL)shouldAppend
+{
+    NSString *filePath = [self filesystemPathForURL:localURL];
+
+    CDVPluginResult* result = nil;
+    CDVFileError errCode = INVALID_MODIFICATION_ERR;
+    int bytesWritten = 0;
+
+    if (filePath) {
+        NSOutputStream* fileStream = [NSOutputStream outputStreamToFileAtPath:filePath append:shouldAppend];
+        if (fileStream) {
+            NSUInteger len = [encData length];
+            if (len == 0) {
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:(double)len];
+            } else {
+                [fileStream open];
+
+                bytesWritten = (int)[fileStream write:[encData bytes] maxLength:len];
+
+                [fileStream close];
+                if (bytesWritten > 0) {
+                    result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:bytesWritten];
+                    // } else {
+                    // can probably get more detailed error info via [fileStream streamError]
+                    // errCode already set to INVALID_MODIFICATION_ERR;
+                    // bytesWritten = 0; // may be set to -1 on error
+                }
+            }
+        } // else fileStream not created return INVALID_MODIFICATION_ERR
+    } else {
+        // invalid filePath
+        errCode = NOT_FOUND_ERR;
+    }
+    if (!result) {
+        // was an error
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errCode];
+    }
+    return result;
+}
+
+/**
+ * Helper function to check to see if the user attempted to copy an entry into its parent without changing its name,
+ * or attempted to copy a directory into a directory that it contains directly or indirectly.
+ *
+ * IN:
+ *  NSString* srcDir
+ *  NSString* destinationDir
+ * OUT:
+ *  YES copy/ move is allows
+ *  NO move is onto itself
+ */
+- (BOOL)canCopyMoveSrc:(NSString*)src ToDestination:(NSString*)dest
+{
+    // This weird test is to determine if we are copying or moving a directory into itself.
+    // Copy /Documents/myDir to /Documents/myDir-backup is okay but
+    // Copy /Documents/myDir to /Documents/myDir/backup not okay
+    BOOL copyOK = YES;
+    NSRange range = [dest rangeOfString:src];
+
+    if (range.location != NSNotFound) {
+        NSRange testRange = {range.length - 1, ([dest length] - range.length)};
+        NSRange resultRange = [dest rangeOfString:@"/" options:0 range:testRange];
+        if (resultRange.location != NSNotFound) {
+            copyOK = NO;
+        }
+    }
+    return copyOK;
+}
+
+- (void)copyFileToURL:(CDVFilesystemURL *)destURL withName:(NSString *)newName fromFileSystem:(NSObject<CDVFileSystem> *)srcFs atURL:(CDVFilesystemURL *)srcURL copy:(BOOL)bCopy callback:(void (^)(CDVPluginResult *))callback
+{
+    NSFileManager *fileMgr = [[NSFileManager alloc] init];
+    NSString *destRootPath = [self filesystemPathForURL:destURL];
+    BOOL bDestIsDir = NO;
+    BOOL bDestExists = [fileMgr fileExistsAtPath:destRootPath isDirectory:&bDestIsDir];
+
+    NSString *newFileSystemPath = [destRootPath stringByAppendingPathComponent:newName];
+    NSString *newFullPath = [self fullPathForFileSystemPath:newFileSystemPath];
+
+    BOOL bNewIsDir = NO;
+    BOOL bNewExists = [fileMgr fileExistsAtPath:newFileSystemPath isDirectory:&bNewIsDir];
+
+    CDVPluginResult *result = nil;
+    int errCode = 0;
+
+    if (!bDestExists) {
+        // the destination root does not exist
+        errCode = NOT_FOUND_ERR;
+    }
+
+    else if ([srcFs isKindOfClass:[CDVLocalFilesystem class]]) {
+        /* Same FS, we can shortcut with NSFileManager operations */
+        NSString *srcFullPath = [srcFs filesystemPathForURL:srcURL];
+
+        BOOL bSrcIsDir = NO;
+        BOOL bSrcExists = [fileMgr fileExistsAtPath:srcFullPath isDirectory:&bSrcIsDir];
+
+        if (!bSrcExists) {
+            // the source does not exist
+            errCode = NOT_FOUND_ERR;
+        } else if ([newFileSystemPath isEqualToString:srcFullPath]) {
+            // source and destination can not be the same
+            errCode = INVALID_MODIFICATION_ERR;
+        } else if (bSrcIsDir && (bNewExists && !bNewIsDir)) {
+            // can't copy/move dir to file
+            errCode = INVALID_MODIFICATION_ERR;
+        } else { // no errors yet
+            NSError* __autoreleasing error = nil;
+            BOOL bSuccess = NO;
+            if (bCopy) {
+                if (bSrcIsDir && ![self canCopyMoveSrc:srcFullPath ToDestination:newFileSystemPath]) {
+                    // can't copy dir into self
+                    errCode = INVALID_MODIFICATION_ERR;
+                } else if (bNewExists) {
+                    // the full destination should NOT already exist if a copy
+                    errCode = PATH_EXISTS_ERR;
+                } else {
+                    bSuccess = [fileMgr copyItemAtPath:srcFullPath toPath:newFileSystemPath error:&error];
+                }
+            } else { // move
+                // iOS requires that destination must not exist before calling moveTo
+                // is W3C INVALID_MODIFICATION_ERR error if destination dir exists and has contents
+                //
+                if (!bSrcIsDir && (bNewExists && bNewIsDir)) {
+                    // can't move a file to directory
+                    errCode = INVALID_MODIFICATION_ERR;
+                } else if (bSrcIsDir && ![self canCopyMoveSrc:srcFullPath ToDestination:newFileSystemPath]) {
+                    // can't move a dir into itself
+                    errCode = INVALID_MODIFICATION_ERR;
+                } else if (bNewExists) {
+                    if (bNewIsDir && ([[fileMgr contentsOfDirectoryAtPath:newFileSystemPath error:NULL] count] != 0)) {
+                        // can't move dir to a dir that is not empty
+                        errCode = INVALID_MODIFICATION_ERR;
+                        newFileSystemPath = nil;  // so we won't try to move
+                    } else {
+                        // remove destination so can perform the moveItemAtPath
+                        bSuccess = [fileMgr removeItemAtPath:newFileSystemPath error:NULL];
+                        if (!bSuccess) {
+                            errCode = INVALID_MODIFICATION_ERR; // is this the correct error?
+                            newFileSystemPath = nil;
+                        }
+                    }
+                } else if (bNewIsDir && [newFileSystemPath hasPrefix:srcFullPath]) {
+                    // can't move a directory inside itself or to any child at any depth;
+                    errCode = INVALID_MODIFICATION_ERR;
+                    newFileSystemPath = nil;
+                }
+
+                if (newFileSystemPath != nil) {
+                    bSuccess = [fileMgr moveItemAtPath:srcFullPath toPath:newFileSystemPath error:&error];
+                }
+            }
+            if (bSuccess) {
+                // should verify it is there and of the correct type???
+                NSDictionary* newEntry = [self makeEntryForPath:newFullPath isDirectory:bSrcIsDir];
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:newEntry];
+            } else {
+                if (error) {
+                    if (([error code] == NSFileReadUnknownError) || ([error code] == NSFileReadTooLargeError)) {
+                        errCode = NOT_READABLE_ERR;
+                    } else if ([error code] == NSFileWriteOutOfSpaceError) {
+                        errCode = QUOTA_EXCEEDED_ERR;
+                    } else if ([error code] == NSFileWriteNoPermissionError) {
+                        errCode = NO_MODIFICATION_ALLOWED_ERR;
+                    }
+                }
+            }
+        }
+    } else {
+        // Need to copy the hard way
+        [srcFs readFileAtURL:srcURL start:0 end:-1 callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) {
+            CDVPluginResult* result = nil;
+            if (data != nil) {
+                BOOL bSuccess = [data writeToFile:newFileSystemPath atomically:YES];
+                if (bSuccess) {
+                    // should verify it is there and of the correct type???
+                    NSDictionary* newEntry = [self makeEntryForPath:newFullPath isDirectory:NO];
+                    result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:newEntry];
+                } else {
+                    result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:ABORT_ERR];
+                }
+            } else {
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode];
+            }
+            callback(result);
+        }];
+        return; // Async IO; return without callback.
+    }
+    if (result == nil) {
+        if (!errCode) {
+            errCode = INVALID_MODIFICATION_ERR; // Catch-all default
+        }
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errCode];
+    }
+    callback(result);
+}
+
+/* helper function to get the mimeType from the file extension
+ * IN:
+ *	NSString* fullPath - filename (may include path)
+ * OUT:
+ *	NSString* the mime type as type/subtype.  nil if not able to determine
+ */
++ (NSString*)getMimeTypeFromPath:(NSString*)fullPath
+{
+    NSString* mimeType = nil;
+
+    if (fullPath) {
+        CFStringRef typeId = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[fullPath pathExtension], NULL);
+        if (typeId) {
+            mimeType = (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass(typeId, kUTTagClassMIMEType);
+            if (!mimeType) {
+                // special case for m4a
+                if ([(__bridge NSString*)typeId rangeOfString : @"m4a-audio"].location != NSNotFound) {
+                    mimeType = @"audio/mp4";
+                } else if ([[fullPath pathExtension] rangeOfString:@"wav"].location != NSNotFound) {
+                    mimeType = @"audio/wav";
+                } else if ([[fullPath pathExtension] rangeOfString:@"css"].location != NSNotFound) {
+                    mimeType = @"text/css";
+                }
+            }
+            CFRelease(typeId);
+        }
+    }
+    return mimeType;
+}
+
+- (void)readFileAtURL:(CDVFilesystemURL *)localURL start:(NSInteger)start end:(NSInteger)end callback:(void (^)(NSData*, NSString* mimeType, CDVFileError))callback
+{
+    NSString *path = [self filesystemPathForURL:localURL];
+
+    NSString* mimeType = [CDVLocalFilesystem getMimeTypeFromPath:path];
+    if (mimeType == nil) {
+        mimeType = @"*/*";
+    }
+    NSFileHandle* file = [NSFileHandle fileHandleForReadingAtPath:path];
+    if (start > 0) {
+        [file seekToFileOffset:start];
+    }
+
+    NSData* readData;
+    if (end < 0) {
+        readData = [file readDataToEndOfFile];
+    } else {
+        readData = [file readDataOfLength:(end - start)];
+    }
+    [file closeFile];
+
+    callback(readData, mimeType, readData != nil ? NO_ERROR : NOT_FOUND_ERR);
+}
+
+- (void)getFileMetadataForURL:(CDVFilesystemURL *)localURL callback:(void (^)(CDVPluginResult *))callback
+{
+    NSString *path = [self filesystemPathForURL:localURL];
+    CDVPluginResult *result;
+    NSFileManager* fileMgr = [[NSFileManager alloc] init];
+
+    NSError* __autoreleasing error = nil;
+    NSDictionary* fileAttrs = [fileMgr attributesOfItemAtPath:path error:&error];
+
+    if (fileAttrs) {
+
+        // create dictionary of file info
+        NSMutableDictionary* fileInfo = [NSMutableDictionary dictionaryWithCapacity:5];
+
+        [fileInfo setObject:localURL.fullPath forKey:@"fullPath"];
+        [fileInfo setObject:[self mimeTypeForFileAtPath: path] forKey:@"type"];
+        [fileInfo setObject:[path lastPathComponent] forKey:@"name"];
+
+        // Ensure that directories (and other non-regular files) report size of 0
+        unsigned long long size = ([fileAttrs fileType] == NSFileTypeRegular ? [fileAttrs fileSize] : 0);
+        [fileInfo setObject:[NSNumber numberWithUnsignedLongLong:size] forKey:@"size"];
+
+        NSDate* modDate = [fileAttrs fileModificationDate];
+        if (modDate) {
+            [fileInfo setObject:[NSNumber numberWithDouble:[modDate timeIntervalSince1970] * 1000] forKey:@"lastModifiedDate"];
+        }
+
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileInfo];
+
+    } else {
+        // didn't get fileAttribs
+        CDVFileError errorCode = ABORT_ERR;
+        NSLog(@"error getting metadata: %@", [error localizedDescription]);
+        if ([error code] == NSFileNoSuchFileError || [error code] == NSFileReadNoSuchFileError) {
+            errorCode = NOT_FOUND_ERR;
+        }
+        // log [NSNumber numberWithDouble: theMessage] objCtype to see what it returns
+        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errorCode];
+    }
+
+    callback(result);
+}
+
+// fix errors that base on Alexsander Akers from http://stackoverflow.com/a/5998683/2613194
+- (NSString*) mimeTypeForFileAtPath: (NSString *) path {
+    if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
+        return nil;
+    }
+    
+    CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[path pathExtension], NULL);
+    CFStringRef mimeType = UTTypeCopyPreferredTagWithClass (UTI, kUTTagClassMIMEType);
+    CFRelease(UTI);
+    
+    if (!mimeType) {
+        return @"application/octet-stream";
+    }
+    return (__bridge NSString *)mimeType;
+}
+
+@end

+ 20 - 2
cordova/platforms/ios/美天旺/config.xml

@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='utf-8'?>
-<widget id="com.shotshock.twong" version="1.2.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+<widget id="com.shotshock.twong" version="1.2.8" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
     <feature name="CDVWebViewEngine">
         <param name="ios-package" value="CDVWebViewEngine" />
     </feature>
@@ -52,6 +52,16 @@
     <feature name="Keyboard">
         <param name="ios-package" onload="true" value="IonicKeyboard" />
     </feature>
+    <feature name="File">
+        <param name="ios-package" value="CDVFile" />
+        <param name="onload" value="true" />
+    </feature>
+    <feature name="FileTransfer">
+        <param name="ios-package" value="CDVFileTransfer" />
+    </feature>
+    <feature name="FileOpener2">
+        <param name="ios-package" value="FileOpener2" />
+    </feature>
     <name>美天旺</name>
     <description>
         美天旺是完璧时空旗下新型垂直电商App,专注于新经济下的购物服务!
@@ -85,7 +95,10 @@
     <icon height="50" src="res/ios/icon-50.png" width="50" />
     <icon height="100" src="res/ios/icon-50@2x.png" width="100" />
     <icon height="1024" src="res/ios/ios-marketing-1024x1024" width="024" />
-    <preference name="AllowInlineMediaPlayback" value="false" />
+    <splash src="res/screen/ios/Default@2x~universal~anyany.png" />
+    <splash src="res/screen/ios/Default@2x~universal~comany.png" />
+    <splash src="res/screen/ios/Default@3x~universal~comany.png" />
+    <preference name="AllowInlineMediaPlayback" value="true" />
     <preference name="BackupWebStorage" value="cloud" />
     <preference name="DisallowOverscroll" value="true" />
     <preference name="EnableViewportScale" value="false" />
@@ -105,6 +118,11 @@
     <preference name="alipayid" value="2021001196660263" />
     <preference name="Fullscreen" value="true" />
     <preference name="WebViewBounce" value="false" />
+    <preference name="FadeSplashScreen" value="false" />
+    <preference name="ShowSplashScreenSpinner" value="false" />
+    <preference name="SplashMaintainAspectRatio" value="true" />
+    <preference name="SplashScreenSpinnerColor" value="white" />
     <preference name="CodePushDeploymentKey" value="YOUR-ANDROID-DEPLOYMENT-KEY" />
     <preference name="CodePushPublicKey" value="YOUR-PUBLIC-KEY" />
+    <preference name="SplashScreenDelay" value="1500" />
 </widget>

+ 5 - 5
cordova/platforms/ios/美天旺/美天旺-Info.plist

@@ -17,7 +17,7 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>$(MARKETING_VERSION)</string>
+	<string>1.2.8</string>
 	<key>CFBundleSignature</key>
 	<string>????</string>
 	<key>CFBundleURLTypes</key>
@@ -56,7 +56,7 @@
 		</dict>
 	</array>
 	<key>CFBundleVersion</key>
-	<string>$(CURRENT_PROJECT_VERSION)</string>
+	<string>1.2.8</string>
 	<key>LSApplicationQueriesSchemes</key>
 	<array>
 		<string>weixin</string>
@@ -73,9 +73,9 @@
 	<key>NSCameraUsageDescription</key>
 	<string>是否允许App访问相机使用拍照获取图片上传至App</string>
 	<key>NSMainNibFile</key>
-	<string></string>
+	<string/>
 	<key>NSMainNibFile~ipad</key>
-	<string></string>
+	<string/>
 	<key>NSPhotoLibraryAddUsageDescription</key>
 	<string>App需要您的同意,才能写入相册</string>
 	<key>NSPhotoLibraryUsageDescription</key>
@@ -100,4 +100,4 @@
 		<string>UIInterfaceOrientationLandscapeRight</string>
 	</array>
 </dict>
-</plist>
+</plist>

+ 13 - 0
cordova/plugins/android.json

@@ -48,6 +48,16 @@
     },
     "ionic-plugin-keyboard": {
       "PACKAGE_NAME": "com.shotshock.twong"
+    },
+    "cordova-plugin-file-transfer": {
+      "PACKAGE_NAME": "com.shotshock.twong"
+    },
+    "cordova-plugin-file-opener2": {
+      "ANDROID_SUPPORT_V4_VERSION": "27.+",
+      "PACKAGE_NAME": "com.shotshock.twong"
+    },
+    "cordova-plugin-splashscreen": {
+      "PACKAGE_NAME": "com.shotshock.twong"
     }
   },
   "dependent_plugins": {
@@ -56,6 +66,9 @@
     },
     "cordova-plugin-jcore": {
       "PACKAGE_NAME": "com.shotshock.twong"
+    },
+    "cordova-plugin-file": {
+      "PACKAGE_NAME": "com.shotshock.twong"
     }
   }
 }

+ 44 - 0
cordova/plugins/cordova-plugin-file-opener2/.github/ISSUE_TEMPLATE/bug_report.md

@@ -0,0 +1,44 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+### Expected Behaviour
+
+### Actual Behaviour
+
+### Reproduce Scenario (including but not limited to)
+
+#### Steps to Reproduce
+
+#### Platform and Version (eg. Android 5.0 or iOS 9.2.1)
+
+#### (Android) What device vendor (e.g. Samsung, HTC, Sony...)
+
+#### Cordova CLI info
+
+    cordova info
+
+Here is the output:
+
+
+#### Plugin version
+
+    cordova plugin version | grep cordova-plugin-file-opener2
+
+Here is the output:
+
+
+#### Sample Code that illustrates the problem
+
+#### Logs taken while reproducing problem
+Run 
+
+`adb logcat PluginManager:V CordovaPlugin:V CordovaLog:V chromium:V *:S` 
+
+while testing the bug in your app to get some output from your error. This will help you to understand more about the nature of your problem.
+

+ 20 - 0
cordova/plugins/cordova-plugin-file-opener2/LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 pwlin - pwlin05@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 213 - 0
cordova/plugins/cordova-plugin-file-opener2/README.md

@@ -0,0 +1,213 @@
+# A File Opener Plugin for Cordova
+
+[![Latest Stable Version](https://img.shields.io/npm/v/cordova-plugin-file-opener2.svg)](https://www.npmjs.com/package/cordova-plugin-file-opener2) [![Total Downloads](https://img.shields.io/npm/dt/cordova-plugin-file-opener2.svg)](https://npm-stat.com/charts.html?package=cordova-plugin-file-opener2)
+
+This plugin will open a file on your device file system with its default application.
+
+```js
+cordova.plugins.fileOpener2.open(
+    filePath,
+    fileMIMEType,
+    {
+        error : function(){ },
+        success : function(){ }
+    }
+);
+```
+
+## Installation
+
+```shell
+$ cordova plugin add cordova-plugin-file-opener2
+```
+
+### Optional variables
+
+This plugin requires the Android support library v4. From release `2.1.0` the version of this can be set at installation. The minimum version is `24.1.0`. Default value is `27.+`. [Check out the latest version](https://developer.android.com/topic/libraries/support-library/revisions.html).
+
+```shell
+$ cordova plugin add cordova-plugin-file-opener2  --variable ANDROID_SUPPORT_V4_VERSION="27.+"
+```
+
+If you are using the `cordova-android-support-gradle-release` plugin it should match the value you have set there.
+
+## Requirements
+
+The following platforms and versions are supported by the latest release:
+
+- Android 4.4+ / iOS 9+ / Windows / Electron
+- Cordova CLI 7.0 or higher
+
+Cordova CLI 6.0 is supported by 2.0.19, but there are a number of issues, particularly with Android builds (see [232](https://github.com/pwlin/cordova-plugin-file-opener2/issues/232) [203](https://github.com/pwlin/cordova-plugin-file-opener2/issues/203) [207](https://github.com/pwlin/cordova-plugin-file-opener2/issues/207)). Using the [cordova-android-support-gradle-release](https://github.com/dpa99c/cordova-android-support-gradle-release) plugin may help.
+
+### AndroidX Support
+
+Currently if your project requires AndroidX support, you need to add the following two plugins to your project:
+
+- [cordova-plugin-androidx](https://github.com/dpa99c/cordova-plugin-androidx/) and [cordova-plugin-androidx-adapter](https://github.com/dpa99c/cordova-plugin-androidx-adapter/)
+```shell
+$ cordova plugin add cordova-plugin-androidx
+$ cordova plugin add cordova-plugin-androidx-adapter
+```
+Just adding these plugins should be enough and no further changes are necessary.
+
+
+## fileOpener2.open(filePath, mimeType, options)
+
+Opens a file
+
+### Supported Platforms
+
+- Android 4.4+
+- iOS 9+
+- Windows
+- Electron
+
+### Quick Examples
+Open an APK install dialog:
+
+```javascript
+cordova.plugins.fileOpener2.open(
+    '/Downloads/gmail.apk',
+    'application/vnd.android.package-archive'
+);
+```
+
+Open a PDF document with the default PDF reader and optional callback object:
+
+```js
+cordova.plugins.fileOpener2.open(
+    '/Download/starwars.pdf', // You can also use a Cordova-style file uri: cdvfile://localhost/persistent/Downloads/starwars.pdf
+    'application/pdf',
+    {
+        error : function(e) {
+            console.log('Error status: ' + e.status + ' - Error message: ' + e.message);
+        },
+        success : function () {
+            console.log('file opened successfully');
+        }
+    }
+);
+```
+
+__Note on Electron:__ Do not forget to enable Node.js in your app by adding `"nodeIntegration": true` to `platforms/electron/platform_www/cdv-electron-settings.json` file, See [Cordova-Electron documentation](https://cordova.apache.org/docs/en/latest/guide/platforms/electron/index.html#customizing-the-application's-window-options).
+
+### Market place installation
+Install From Market: to install an APK from a market place, such as Google Play or the App Store, you can use an `<a>` tag in combination with the `market://` protocol:
+
+```html
+<a href="market://details?id=xxxx" target="_system">Install from Google Play</a>
+<a href="itms-apps://itunes.apple.com/app/my-app/idxxxxxxxx?mt=8" target="_system">Install from App Store</a>
+```
+or in code:
+
+```js
+window.open("[market:// or itms-apps:// link]","_system");
+```
+
+## fileOpener2.showOpenWithDialog(filePath, mimeType, options)
+
+Opens with system modal to open file with an already installed app.
+
+### Supported Platforms
+
+- Android 4.4+
+- iOS 9+
+
+### Quick Example
+
+```js
+cordova.plugins.fileOpener2.showOpenWithDialog(
+    '/Downloads/starwars.pdf', // You can also use a Cordova-style file uri: cdvfile://localhost/persistent/Downloads/starwars.pdf
+    'application/pdf',
+    {
+        error : function(e) {
+            console.log('Error status: ' + e.status + ' - Error message: ' + e.message);
+        },
+        success : function () {
+            console.log('file opened successfully');
+        },
+        position : [0, 0]
+    }
+);
+```
+`position` array of coordinates from top-left device screen, use for iOS dialog positioning.
+
+## fileOpener2.uninstall(packageId, callbackContext)
+
+Uninstall a package with its ID. 
+
+__Note__: You need to add `<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />` to your `AndroidManifest.xml`
+
+### Supported Platforms
+
+- Android 4.4+
+
+### Quick Example
+```js
+cordova.plugins.fileOpener2.uninstall('com.zynga.FarmVille2CountryEscape', {
+    error : function(e) {
+        console.log('Error status: ' + e.status + ' - Error message: ' + e.message);
+    },
+    success : function() {
+        console.log('Uninstall intent activity started.');
+    }
+});
+```
+
+## fileOpener2.appIsInstalled(packageId, callbackContext)
+
+Check if an app is already installed.
+
+### Supported Platforms
+
+- Android 4.4+
+
+### Quick Example
+```javascript
+cordova.plugins.fileOpener2.appIsInstalled('com.adobe.reader', {
+    success : function(res) {
+        if (res.status === 0) {
+            console.log('Adobe Reader is not installed.');
+        } else {
+            console.log('Adobe Reader is installed.')
+        }
+    }
+});
+```
+---
+
+## Android APK installation limitation
+
+The following limitations apply when opening an APK file for installation:
+- On Android 8+, your application must have the `ACTION_INSTALL_PACKAGE` permission. You can add it by adding this to your app's `config.xml` file:
+```xml
+<platform name="android">
+    <config-file parent="/manifest" target="AndroidManifest.xml" xmlns:android="http://schemas.android.com/apk/res/android">
+        <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+    </config-file>
+</platform>
+```
+
+- Before Android 7, you can only install APKs from the "external" partition. For example, you can install from `cordova.file.externalDataDirectory`, but **not** from `cordova.file.dataDirectory`. Android 7+ does not have this limitation.
+
+---
+
+## SD card limitation on Android
+
+It is not always possible to open a file from the SD Card using this plugin on Android. This is because the underlying  Android library used [does not support serving files from secondary external storage devices](https://stackoverflow.com/questions/40318116/fileprovider-and-secondary-external-storage). Whether or not your the SD card is treated as a secondary external device depends on your particular phone's set up.
+
+---
+
+## Notes
+
+- For properly opening _any_ file, you must already have a suitable reader for that particular file type installed on your device. Otherwise this will not work.
+
+- [It is reported](https://github.com/pwlin/cordova-plugin-file-opener2/issues/2#issuecomment-41295793) that in iOS, you might need to remove `<preference name="iosPersistentFileLocation" value="Library" />` from your `config.xml`
+
+- If you are wondering what MIME-type should you pass as the second argument to `open` function, [here is a list of all known MIME-types](http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types?view=co)
+
+
+---
+
+

+ 62 - 0
cordova/plugins/cordova-plugin-file-opener2/package.json

@@ -0,0 +1,62 @@
+{
+  "_from": "cordova-plugin-file-opener2",
+  "_id": "cordova-plugin-file-opener2@3.0.5",
+  "_inBundle": false,
+  "_integrity": "sha1-O/SBpyS87Jz6IlenGRUwnfXqW00=",
+  "_location": "/cordova-plugin-file-opener2",
+  "_phantomChildren": {},
+  "_requested": {
+    "type": "tag",
+    "registry": true,
+    "raw": "cordova-plugin-file-opener2",
+    "name": "cordova-plugin-file-opener2",
+    "escapedName": "cordova-plugin-file-opener2",
+    "rawSpec": "",
+    "saveSpec": null,
+    "fetchSpec": "latest"
+  },
+  "_requiredBy": [
+    "#DEV:/",
+    "#USER"
+  ],
+  "_resolved": "https://registry.npm.taobao.org/cordova-plugin-file-opener2/download/cordova-plugin-file-opener2-3.0.5.tgz",
+  "_shasum": "3bf481a724bcec9cfa2257a71915309df5ea5b4d",
+  "_spec": "cordova-plugin-file-opener2",
+  "_where": "/Users/x/Documents/twong-h5/cordova",
+  "author": {
+    "name": "pwlin05@gmail.com"
+  },
+  "bugs": {
+    "url": "https://github.com/pwlin/cordova-plugin-file-opener2/issues"
+  },
+  "bundleDependencies": false,
+  "cordova": {
+    "id": "cordova-plugin-file-opener2",
+    "platforms": [
+      "android",
+      "ios",
+      "windows",
+      "electron"
+    ]
+  },
+  "deprecated": false,
+  "description": "A File Opener Plugin for Cordova. (The Original Version)",
+  "engines": {
+    "cordova": ">=6.0.0"
+  },
+  "homepage": "https://github.com/pwlin/cordova-plugin-file-opener2#readme",
+  "keywords": [
+    "ecosystem:cordova",
+    "cordova-android",
+    "cordova-ios",
+    "cordova-windows",
+    "cordova-electron"
+  ],
+  "license": "MIT",
+  "name": "cordova-plugin-file-opener2",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/pwlin/cordova-plugin-file-opener2.git"
+  },
+  "version": "3.0.5"
+}

+ 97 - 0
cordova/plugins/cordova-plugin-file-opener2/plugin.xml

@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<plugin xmlns="http://www.phonegap.com/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" id="cordova-plugin-file-opener2" version="3.0.5">
+
+    <name>File Opener2</name>
+    <description>A File Opener Plugin for Cordova. (The Original Version)</description>
+    <license>MIT</license>
+
+    <engines>
+        <engine name="cordova" version=">=3.0.0" />
+    </engines>
+
+    <js-module src="www/plugins.FileOpener2.js" name="FileOpener2">
+        <clobbers target="cordova.plugins.fileOpener2" />
+    </js-module>
+
+    <!-- Android -->
+    <platform name="android">
+        <source-file src="src/android/io/github/pwlin/cordova/plugins/fileopener2/FileOpener2.java" target-dir="src/io/github/pwlin/cordova/plugins/fileopener2" />
+        <source-file src="src/android/io/github/pwlin/cordova/plugins/fileopener2/FileProvider.java" target-dir="src/io/github/pwlin/cordova/plugins/fileopener2" />
+        <config-file target="res/xml/config.xml" parent="/*">
+            <feature name="FileOpener2">
+                <param name="android-package" value="io.github.pwlin.cordova.plugins.fileopener2.FileOpener2" />
+            </feature>
+        </config-file>
+        <config-file target="AndroidManifest.xml" parent="/*">
+            <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+            <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+        </config-file>
+        <config-file target="AndroidManifest.xml" parent="application">
+          <provider android:name="io.github.pwlin.cordova.plugins.fileopener2.FileProvider" android:authorities="${applicationId}.fileOpener2.provider" android:exported="false" android:grantUriPermissions="true">
+            <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/opener_paths" />
+          </provider>
+        </config-file>
+        <source-file src="src/android/res/xml/opener_paths.xml" target-dir="res/xml" />
+        <preference name="ANDROID_SUPPORT_V4_VERSION" default="27.+"/>
+        <framework src="com.android.support:support-v4:$ANDROID_SUPPORT_V4_VERSION"/>
+    </platform>
+
+    <!-- iOS -->
+    <platform name="ios">
+        <config-file target="config.xml" parent="/*">
+            <feature name="FileOpener2">
+                <param name="ios-package" value="FileOpener2" />
+            </feature>
+        </config-file>
+        <source-file src="src/ios/FileOpener2.m" />
+        <header-file src="src/ios/FileOpener2.h" />
+    </platform>
+
+	<!-- WP8 -->
+	<platform name="wp8">
+		<config-file target="config.xml" parent="/*">
+		  <feature name="FileOpener2">
+			<param name="wp-package" value="FileOpener2" />
+		  </feature>
+		</config-file>
+		<source-file src="src/wp8/FileOpener2.cs" />
+	</platform>
+
+	<!-- windows -->
+    <platform name="windows">
+        <js-module src="src/windows/fileOpener2Proxy.js" name="fileOpener2Proxy">
+            <merges target="" />
+        </js-module>
+    </platform>
+
+    <!-- browser -->
+    <platform name="browser">
+      <config-file parent="/*" target="config.xml">
+          <feature name="FileOpener2">
+              <param name="browser-package" value="FileOpener2"/>
+          </feature>
+      </config-file>
+
+      <!-- Required for browserify: we always link module below as there is conditional reference
+      to this module from requestFileSystem and resolveLocalFileSystemURI modules. -->
+      <js-module src="www/browser/isChrome.js" name="isChrome">
+          <runs />
+      </js-module>
+
+      <js-module src="src/browser/FileSaver.min.js" name="FileSaver">
+          <clobbers target="FileSaver"/>
+      </js-module>
+
+      <js-module src="src/browser/FileOpener2.js" name="FileOpener2Proxy">
+          <runs/>
+      </js-module>
+    </platform>
+
+    <!-- electron -->
+    <platform name="electron">
+        <js-module src="src/electron/FileOpener2.js" name="fileOpener2">
+            <merges target="" />
+        </js-module>
+    </platform>
+
+</plugin>

+ 199 - 0
cordova/plugins/cordova-plugin-file-opener2/src/android/io/github/pwlin/cordova/plugins/fileopener2/FileOpener2.java

@@ -0,0 +1,199 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2013 pwlin - pwlin05@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+package io.github.pwlin.cordova.plugins.fileopener2;
+
+import java.io.File;
+import java.util.List;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.webkit.MimeTypeMap;
+
+import io.github.pwlin.cordova.plugins.fileopener2.FileProvider;
+
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.PluginResult;
+import org.apache.cordova.CordovaResourceApi;
+
+public class FileOpener2 extends CordovaPlugin {
+
+	/**
+	 * Executes the request and returns a boolean.
+	 *
+	 * @param action
+	 *            The action to execute.
+	 * @param args
+	 *            JSONArry of arguments for the plugin.
+	 * @param callbackContext
+	 *            The callback context used when calling back into JavaScript.
+	 * @return boolean.
+	 */
+	public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+		if (action.equals("open")) {
+			String fileUrl = args.getString(0);
+			String contentType = args.getString(1);
+			Boolean openWithDefault = true;
+			if(args.length() > 2){
+				openWithDefault = args.getBoolean(2);
+			}
+			this._open(fileUrl, contentType, openWithDefault, callbackContext);
+		}
+		else if (action.equals("uninstall")) {
+			this._uninstall(args.getString(0), callbackContext);
+		}
+		else if (action.equals("appIsInstalled")) {
+			JSONObject successObj = new JSONObject();
+			if (this._appIsInstalled(args.getString(0))) {
+				successObj.put("status", PluginResult.Status.OK.ordinal());
+				successObj.put("message", "Installed");
+			}
+			else {
+				successObj.put("status", PluginResult.Status.NO_RESULT.ordinal());
+				successObj.put("message", "Not installed");
+			}
+			callbackContext.success(successObj);
+		}
+		else {
+			JSONObject errorObj = new JSONObject();
+			errorObj.put("status", PluginResult.Status.INVALID_ACTION.ordinal());
+			errorObj.put("message", "Invalid action");
+			callbackContext.error(errorObj);
+		}
+		return true;
+	}
+
+	private void _open(String fileArg, String contentType, Boolean openWithDefault, CallbackContext callbackContext) throws JSONException {
+		String fileName = "";
+		try {
+			CordovaResourceApi resourceApi = webView.getResourceApi();
+			Uri fileUri = resourceApi.remapUri(Uri.parse(fileArg));
+			fileName = fileUri.getPath();
+		} catch (Exception e) {
+			fileName = fileArg;
+		}
+		File file = new File(fileName);
+		if (file.exists()) {
+			try {
+				if (contentType == null || contentType.trim().equals("")) {
+				    contentType = _getMimeType(fileName);
+				}
+
+				Intent intent;
+				if (contentType.equals("application/vnd.android.package-archive")) {
+					// https://stackoverflow.com/questions/9637629/can-we-install-an-apk-from-a-contentprovider/9672282#9672282
+					intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
+					Uri path;
+					if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+						path = Uri.fromFile(file);
+					} else {
+						Context context = cordova.getActivity().getApplicationContext();
+						path = FileProvider.getUriForFile(context, cordova.getActivity().getPackageName() + ".fileOpener2.provider", file);
+					}
+					intent.setDataAndType(path, contentType);
+					intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);
+
+				} else {
+					intent = new Intent(Intent.ACTION_VIEW);
+					Context context = cordova.getActivity().getApplicationContext();
+					Uri path = FileProvider.getUriForFile(context, cordova.getActivity().getPackageName() + ".fileOpener2.provider", file);
+					intent.setDataAndType(path, contentType);
+					intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
+				}
+
+				/*
+				 * @see
+				 * http://stackoverflow.com/questions/14321376/open-an-activity-from-a-cordovaplugin
+				 */
+				 if(openWithDefault){
+					 cordova.getActivity().startActivity(intent);
+				 }
+				 else{
+					 cordova.getActivity().startActivity(Intent.createChooser(intent, "Open File in..."));
+				 }
+
+				callbackContext.success();
+			} catch (android.content.ActivityNotFoundException e) {
+				JSONObject errorObj = new JSONObject();
+				errorObj.put("status", PluginResult.Status.ERROR.ordinal());
+				errorObj.put("message", "Activity not found: " + e.getMessage());
+				callbackContext.error(errorObj);
+			}
+		} else {
+			JSONObject errorObj = new JSONObject();
+			errorObj.put("status", PluginResult.Status.ERROR.ordinal());
+			errorObj.put("message", "File not found");
+			callbackContext.error(errorObj);
+		}
+	}
+
+	private String _getMimeType(String url) {
+	    String mimeType = "*/*";
+	    int extensionIndex = url.lastIndexOf('.');
+	    if (extensionIndex > 0) {
+		String extMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(url.substring(extensionIndex+1));
+		if (extMimeType != null) {
+		    mimeType = extMimeType;
+		}
+	    }
+	    return mimeType;
+	}
+
+	private void _uninstall(String packageId, CallbackContext callbackContext) throws JSONException {
+		if (this._appIsInstalled(packageId)) {
+			Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE);
+			intent.setData(Uri.parse("package:" + packageId));
+			cordova.getActivity().startActivity(intent);
+			callbackContext.success();
+		}
+		else {
+			JSONObject errorObj = new JSONObject();
+			errorObj.put("status", PluginResult.Status.ERROR.ordinal());
+			errorObj.put("message", "This package is not installed");
+			callbackContext.error(errorObj);
+		}
+	}
+
+	private boolean _appIsInstalled(String packageId) {
+		PackageManager pm = cordova.getActivity().getPackageManager();
+        boolean appInstalled = false;
+        try {
+            pm.getPackageInfo(packageId, PackageManager.GET_ACTIVITIES);
+            appInstalled = true;
+        } catch (PackageManager.NameNotFoundException e) {
+            appInstalled = false;
+        }
+        return appInstalled;
+	}
+
+}
+

+ 29 - 0
cordova/plugins/cordova-plugin-file-opener2/src/android/io/github/pwlin/cordova/plugins/fileopener2/FileProvider.java

@@ -0,0 +1,29 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2013 pwlin - pwlin05@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+package io.github.pwlin.cordova.plugins.fileopener2;
+
+/*
+ * http://stackoverflow.com/questions/40746144/error-with-duplicated-fileprovider-in-manifest-xml-with-cordova/41550634#41550634
+ */
+public class FileProvider extends android.support.v4.content.FileProvider {
+}

+ 14 - 0
cordova/plugins/cordova-plugin-file-opener2/src/android/res/xml/opener_paths.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- https://developer.android.com/reference/android/support/v4/content/FileProvider.html#SpecifyFiles -->
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- cordova.file.dataDirectory -->
+    <files-path name="files" path="." />
+    <!-- cordova.file.cacheDirectory -->
+    <cache-path name="cache" path="." />
+    <!-- cordova.file.externalDataDirectory -->
+    <external-files-path name="external-files" path="." />
+    <!-- cordova.file.externalCacheDirectory -->
+    <external-cache-path name="external-cache" path="." />
+    <!-- cordova.file.externalRootDirectory -->
+    <external-path name="external" path="." />
+</paths>

+ 125 - 0
cordova/plugins/cordova-plugin-file-opener2/src/browser/FileOpener2.js

@@ -0,0 +1,125 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2019 fefc - fefc.dev@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+const cacheDirectory = (require('./isChrome')()) ? 'filesystem:' + window.location.origin + '/temporary/' : 'file:///temporary/';
+const dataDirectory = (require('./isChrome')()) ? 'filesystem:' + window.location.origin + '/persistent/' : 'file:///persistent/';
+
+function open(successCallback, errorCallback, data) {
+  var fullFilePath = data[0];
+  //var contentType = data[1]; //Not needed in browser
+  //var openDialog = data[2]; //Not needed in browser
+
+  var dirPath = fullFilePath.substring(0, fullFilePath.lastIndexOf('/') + 1);
+  var fileName = fullFilePath.substring(fullFilePath.lastIndexOf('/') + 1, fullFilePath.length);
+  var fileSystemLocalPath = getLocalPathAndFileSystem(dirPath);
+
+  if (!fileSystemLocalPath.error) {
+    window.requestFileSystem(fileSystemLocalPath.fileSystem, 0, (fs) => {
+      readFile(fs.root, fileSystemLocalPath.localPath + fileName).then((blob) => {
+        FileSaver.saveAs(blob, fileName);
+        successCallback();
+      }).catch((error) => {
+        errorCallback(error);
+      });
+    }, (error) => {
+      errorCallback(error);
+    });
+  } else {
+    errorCallback('INVALID_PATH');
+  }
+}
+
+/**
+ *
+ * Gets the localPath according to the fileSystem (TEMPORARY or PERSISTENT).
+ *
+ * @param {String} Path to the file or directory to check
+ * @returns {Object} value with informations to requestFileSystem later
+ * @returns {string} value.localPath The localPath in relation with fileSystem.
+ * @returns {number} value.fileSystem the fileSystem (TEMPORARY or PERSISTENT).
+ * @returns {error} value.error if the path is not valid.
+ * @returns {message} value.message error message.
+ */
+function getLocalPathAndFileSystem(pathToCheck) {
+  let ret = {
+    localPath: '',
+    fileSystem: window.TEMPORARY
+  };
+
+  if (pathToCheck.startsWith(cacheDirectory)) {
+    ret.localPath = pathToCheck.replace(cacheDirectory, '');
+    ret.fileSystem = window.TEMPORARY;
+
+  } else if (pathToCheck.startsWith(dataDirectory)) {
+    ret.localPath = pathToCheck.replace(dataDirectory, '');
+    ret.fileSystem = window.PERSISTENT;
+
+  } else {
+    return {error: true, message: 'INVALID_PATH'};
+  }
+
+  if (!ret.localPath.endsWith('/')) ret.localPath += '/';
+
+  return ret;
+}
+
+/**
+ *
+ * Reads a file in the fileSystem as an DataURL.
+ *
+ * @param {String} Root is the root folder of the fileSystem.
+ * @param {String} Path is the file to be red.
+ * @returns {Promise} which resolves with an Object containing DataURL, rejects if something went wrong.
+ */
+function readFile(root, filePath) {
+  return new Promise((resolve, reject) => {
+    if (filePath.startsWith('/')) filePath = filePath.substring(1);
+
+    root.getFile(filePath, {}, (fileEntry) => {
+      fileEntry.file((file) => {
+        let reader = new FileReader();
+
+        reader.onload = function() {
+          resolve(reader.result);
+        };
+
+        reader.onerror = function() {
+          reject(reader.error);
+        }
+
+        reader.readAsDataURL(file);
+
+      }, (error) => {
+        reject(error);
+      });
+    }, (error) => {
+      reject(error);
+    });
+  });
+}
+
+module.exports = {
+  open: open
+};
+
+require( "cordova/exec/proxy" ).add( "FileOpener2", module.exports );

File diff suppressed because it is too large
+ 0 - 0
cordova/plugins/cordova-plugin-file-opener2/src/browser/FileSaver.min.js


+ 35 - 0
cordova/plugins/cordova-plugin-file-opener2/src/electron/FileOpener2.js

@@ -0,0 +1,35 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2020 pwlin - pwlin05@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+// https://www.electronjs.org/docs/api/shell
+const { shell } = global.require('electron');
+module.exports = {
+  open: function (onSuccess, onError, fileName) {
+    var opn = shell.openItem(fileName[0]);
+    if (opn === true) {
+      onSuccess(true);
+    } else {
+      onError({'status': 0, 'message': 'Failed opening file.'});
+    }
+  }
+};
+require('cordova/exec/proxy').add('FileOpener2', module.exports);

+ 35 - 0
cordova/plugins/cordova-plugin-file-opener2/src/ios/FileOpener2.h

@@ -0,0 +1,35 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2013 pwlin - pwlin05@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+#import <Cordova/CDV.h>
+
+@interface FileOpener2 : CDVPlugin <UIDocumentInteractionControllerDelegate> {
+    NSString *localFile;
+}
+
+@property(nonatomic, strong) UIDocumentInteractionController *controller;
+@property(nonatomic, strong) CDVViewController *cdvViewController;
+
+- (void) open: (CDVInvokedUrlCommand*)command;
+
+@end

+ 151 - 0
cordova/plugins/cordova-plugin-file-opener2/src/ios/FileOpener2.m

@@ -0,0 +1,151 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2013 pwlin - pwlin05@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#import "FileOpener2.h"
+#import <Cordova/CDV.h>
+
+#import <QuartzCore/QuartzCore.h>
+#import <MobileCoreServices/MobileCoreServices.h>
+
+@implementation FileOpener2
+@synthesize controller = docController;
+
+CDVPluginResult* pluginResult = nil;
+NSString* callbackId = nil;
+
+- (void) open: (CDVInvokedUrlCommand*)command {
+	callbackId = command.callbackId;
+	NSString *path = [command.arguments objectAtIndex:0];
+	NSString *contentType = [command.arguments objectAtIndex:1];
+	BOOL showPreview = YES;
+
+	if ([command.arguments count] >= 3) {
+		showPreview = [[command.arguments objectAtIndex:2] boolValue];
+	}
+
+	CDVViewController* cont = (CDVViewController*)[super viewController];
+	self.cdvViewController = cont;
+	NSString *uti = nil;
+
+	if ([contentType length] == 0) {
+		NSArray *dotParts = [path componentsSeparatedByString:@"."];
+		NSString *fileExt = [dotParts lastObject];
+
+		uti = (__bridge NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExt, NULL);
+	} else {
+		uti = (__bridge NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)contentType, NULL);
+	}
+
+	dispatch_async(dispatch_get_main_queue(), ^{
+		NSURL *fileURL = NULL;
+		NSString *decodedPath = [path stringByRemovingPercentEncoding];
+
+		if ([path isEqualToString:decodedPath]) {
+			NSLog(@"Path parameter not encoded. Building file URL encoding it...");
+			fileURL = [NSURL fileURLWithPath:[path stringByReplacingOccurrencesOfString:@"file://" withString:@""]];;
+		} else {
+			NSLog(@"Path parameter already encoded. Building file URL without encoding it...");
+			fileURL = [NSURL URLWithString:path];
+		}
+
+		localFile = fileURL.path;
+
+		NSLog(@"looking for file at %@", fileURL);
+		NSFileManager *fm = [NSFileManager defaultManager];
+
+    	if (![fm fileExistsAtPath:localFile]) {
+	    	NSDictionary *jsonObj = @{@"status" : @"9",
+	    	@"message" : @"File does not exist"};
+	    	CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:jsonObj];
+	    	[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+	    	return;
+    	}
+
+		docController = [UIDocumentInteractionController  interactionControllerWithURL:fileURL];
+		docController.delegate = self;
+		docController.UTI = uti;
+
+		//Opens the file preview
+		CGRect rect;
+
+		if ([command.arguments count] >= 4) {
+			NSArray *positionValues = [command.arguments objectAtIndex:3];
+
+			if (![positionValues isEqual:[NSNull null]] && [positionValues count] >= 2) {
+				rect = CGRectMake(0, 0, [[positionValues objectAtIndex:0] floatValue], [[positionValues objectAtIndex:1] floatValue]);
+			} else {
+				rect = CGRectMake(0, 0, 0, 0);
+			}
+		} else {
+			rect = CGRectMake(0, 0, cont.view.bounds.size.width, cont.view.bounds.size.height);
+		}
+
+		BOOL wasOpened = NO;
+
+		if (showPreview) {
+			wasOpened = [docController presentPreviewAnimated: NO];
+		} else {
+			CDVViewController* cont = self.cdvViewController;
+			wasOpened = [docController presentOpenInMenuFromRect:rect inView:cont.view animated:YES];
+		}
+
+		if (wasOpened) {
+			pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString: @""];
+		} else {
+			NSDictionary *jsonObj = [ [NSDictionary alloc]
+				initWithObjectsAndKeys :
+				@"9", @"status",
+				@"Could not handle UTI", @"message",
+				nil
+			];
+			pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:jsonObj];
+        	[self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
+		}
+	});
+}
+
+@end
+
+@implementation FileOpener2 (UIDocumentInteractionControllerDelegate)
+- (void)documentInteractionControllerDidDismissOpenInMenu:(UIDocumentInteractionController *)controller {
+	[self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
+}
+
+- (void)documentInteractionControllerDidEndPreview:(UIDocumentInteractionController *)controller {
+	[self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
+}
+
+- (UIViewController *)documentInteractionControllerViewControllerForPreview:(UIDocumentInteractionController *)controller {
+	UIViewController *presentingViewController = self.viewController;
+
+	if (presentingViewController.view.window != [UIApplication sharedApplication].keyWindow) {
+		presentingViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
+	}
+
+	while (presentingViewController.presentedViewController != nil && ![presentingViewController.presentedViewController isBeingDismissed]) {
+		presentingViewController = presentingViewController.presentedViewController;
+	}
+
+	return presentingViewController;
+}
+
+@end

+ 97 - 0
cordova/plugins/cordova-plugin-file-opener2/src/windows/fileOpener2Proxy.js

@@ -0,0 +1,97 @@
+
+	var cordova = require('cordova'),
+		fileOpener2 = require('./FileOpener2');
+
+	var schemes = [
+        { protocol: 'ms-app', getFile: getFileFromApplicationUri },
+        { protocol: 'cdvfile', getFile: getFileFromFileUri }    //protocol cdvfile
+	]
+
+	function nthIndex(str, pat, n) {
+	    var L = str.length, i = -1;
+	    while (n-- && i++ < L) {
+	        i = str.indexOf(pat, i);
+	        if (i < 0) break;
+	    }
+	    return i;
+	}
+
+	function getFileFromApplicationUri(uri) {
+	    /* bad path from a file entry due to the last '//' 
+               example: ms-appdata:///local//path/to/file
+            */
+	    var index = nthIndex(uri, "//", 3);
+	    var newUri = uri.substr(0, index) + uri.substr(index + 1);
+
+	    var applicationUri = new Windows.Foundation.Uri(newUri);
+
+	    return Windows.Storage.StorageFile.getFileFromApplicationUriAsync(applicationUri);
+	}
+
+	function getFileFromFileUri(uri) {
+	    /* uri example:
+               cdvfile://localhost/persistent|temporary|another-fs-root/path/to/file
+            */
+	    var indexFrom = nthIndex(uri, "/", 3) + 1;
+	    var indexTo = nthIndex(uri, "/", 4);
+	    var whichFolder = uri.substring(indexFrom, indexTo);
+	    var filePath = uri.substr(indexTo + 1);
+	    var path = "\\" + filePath;
+
+	    if (whichFolder == "persistent") {
+	        path = Windows.Storage.ApplicationData.current.localFolder.path + path;
+	    }
+	    else {  //temporary, note: no roaming management
+	        path = Windows.Storage.ApplicationData.current.temporaryFolder.path + path;
+	    }
+
+	    return getFileFromNativePath(path);
+	}
+
+	function getFileFromNativePath(path) {
+	    var nativePath = path.split("/").join("\\");
+
+	    return Windows.Storage.StorageFile.getFileFromPathAsync(nativePath);
+	}
+
+	function getFileLoaderForScheme(path) {
+	    var fileLoader = getFileFromNativePath;
+
+	    schemes.some(function (scheme) {
+	        return path.indexOf(scheme.protocol) === 0 ? ((fileLoader = scheme.getFile), true) : false;
+	    });
+
+	    return fileLoader;
+	}
+
+	module.exports = {
+
+	    open: function (successCallback, errorCallback, args) {
+	        
+	        var path = args[0];
+	        
+	        var getFile = getFileLoaderForScheme(path);
+	        
+	        getFile(path).then(function (file) {
+	            var options = new Windows.System.LauncherOptions();
+	            
+                try{
+	                Windows.System.Launcher.launchFileAsync(file, options).then(function (success) {
+	                    successCallback();
+	                }, function (error) {
+	                    errorCallback(error);
+	                });
+                }catch(error){
+                    errorCallback(error);
+                }
+
+	        }, function (error) {
+	            console.log("Error while opening the file: "+error);
+		    errorCallback(error);
+	        });
+		}
+		
+	};
+
+	require("cordova/exec/proxy").add("FileOpener2", module.exports);
+

+ 28 - 0
cordova/plugins/cordova-plugin-file-transfer/.appveyor.yml

@@ -0,0 +1,28 @@
+# appveyor file
+# http://www.appveyor.com/docs/appveyor-yml
+
+max_jobs: 1
+
+shallow_clone: true
+
+init:
+  - git config --global core.autocrlf true
+
+image:
+  - Visual Studio 2017
+
+environment:
+  nodejs_version: "4"
+  matrix:
+    - PLATFORM: windows-10-store
+
+install:
+  - npm cache clean -f
+  - node --version
+  - npm install -g cordova-paramedic@https://github.com/apache/cordova-paramedic.git
+  - npm install -g cordova
+
+build: off
+
+test_script:
+  - cordova-paramedic --config pr\%PLATFORM% --plugin . --justBuild

+ 22 - 0
cordova/plugins/cordova-plugin-file-transfer/.github/PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,22 @@
+<!--
+Please make sure the checklist boxes are all checked before submitting the PR. The checklist
+is intended as a quick reference, for complete details please see our Contributor Guidelines:
+
+http://cordova.apache.org/contribute/contribute_guidelines.html
+
+Thanks!
+-->
+
+### Platforms affected
+
+
+### What does this PR do?
+
+
+### What testing has been done on this change?
+
+
+### Checklist
+- [ ] [Reported an issue](http://cordova.apache.org/contribute/issues.html) in the JIRA database
+- [ ] Commit message follows the format: "CB-3232: (android) Fix bug with resolving file paths", where CB-xxxx is the JIRA ID & "android" is the platform affected.
+- [ ] Added automated test coverage as appropriate for this change.

+ 23 - 0
cordova/plugins/cordova-plugin-file-transfer/.jscsrc

@@ -0,0 +1,23 @@
+{
+    "disallowMixedSpacesAndTabs": true,
+    "disallowTrailingWhitespace": true,
+    "validateIndentation": 4,
+    "requireLineFeedAtFileEnd": true,
+
+    "disallowSpaceAfterPrefixUnaryOperators": true,
+    "disallowSpaceBeforePostfixUnaryOperators": true,
+    "requireSpaceAfterLineComment": true,
+    "requireCapitalizedConstructors": true,
+
+    "disallowSpacesInNamedFunctionExpression": {
+        "beforeOpeningRoundBrace": true
+    },
+
+    "requireSpaceAfterKeywords": [
+      "if",
+      "else",
+      "for",
+      "while",
+      "do"
+    ]
+}

+ 19 - 0
cordova/plugins/cordova-plugin-file-transfer/.jshintrc

@@ -0,0 +1,19 @@
+{
+    "browser": true
+  , "devel": true
+  , "bitwise": true
+  , "undef": true
+  , "trailing": true
+  , "quotmark": false
+  , "indent": 4
+  , "unused": "vars"
+  , "latedef": "nofunc"
+  , "globals": {
+        "module": false,
+        "exports": false,
+        "require": false,
+        "FileTransferError": true,
+        "FileUploadResult": true,
+        "resolveLocalFileSystemURI": false
+    }
+}

Some files were not shown because too many files changed in this diff