summaryrefslogtreecommitdiff
path: root/plugins/cordova-plugin-file/src/android
diff options
context:
space:
mode:
authorPliablePixels <pliablepixels@gmail.com>2015-06-27 09:52:06 -0400
committerPliablePixels <pliablepixels@gmail.com>2015-06-27 09:52:06 -0400
commit319d4cb6670729708c19ad50b0146d1bcb7b4719 (patch)
treec61e9723a1fd217b1816c987bba66e470e73bf02 /plugins/cordova-plugin-file/src/android
parentfdc42fae48db0fef5fbdc9ef51a27d219aea3a72 (diff)
Added ability to log key events to file and email (useful for release debugging)
Diffstat (limited to 'plugins/cordova-plugin-file/src/android')
-rw-r--r--plugins/cordova-plugin-file/src/android/AssetFilesystem.java283
-rw-r--r--plugins/cordova-plugin-file/src/android/ContentFilesystem.java215
-rw-r--r--plugins/cordova-plugin-file/src/android/DirectoryManager.java133
-rw-r--r--plugins/cordova-plugin-file/src/android/EncodingException.java29
-rw-r--r--plugins/cordova-plugin-file/src/android/FileExistsException.java29
-rw-r--r--plugins/cordova-plugin-file/src/android/FileUtils.java1027
-rw-r--r--plugins/cordova-plugin-file/src/android/Filesystem.java325
-rw-r--r--plugins/cordova-plugin-file/src/android/InvalidModificationException.java30
-rw-r--r--plugins/cordova-plugin-file/src/android/LocalFilesystem.java505
-rw-r--r--plugins/cordova-plugin-file/src/android/LocalFilesystemURL.java64
-rw-r--r--plugins/cordova-plugin-file/src/android/NoModificationAllowedException.java29
-rw-r--r--plugins/cordova-plugin-file/src/android/TypeMismatchException.java30
-rw-r--r--plugins/cordova-plugin-file/src/android/build-extras.gradle47
13 files changed, 2746 insertions, 0 deletions
diff --git a/plugins/cordova-plugin-file/src/android/AssetFilesystem.java b/plugins/cordova-plugin-file/src/android/AssetFilesystem.java
new file mode 100644
index 00000000..f501b279
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/AssetFilesystem.java
@@ -0,0 +1,283 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT 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 android.util.Log;
+
+import org.apache.cordova.CordovaResourceApi;
+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 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) {
+ }
+ }
+ }
+ 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);
+ }
+ 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) {
+ throw new FileNotFoundException("File not found: " + assetPath);
+ } finally {
+ if (offr != null) {
+ try {
+ offr.inputStream.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+
+ 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) {
+ throw new FileNotFoundException();
+ }
+
+ 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 null;
+ }
+
+ @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");
+ }
+
+}
diff --git a/plugins/cordova-plugin-file/src/android/ContentFilesystem.java b/plugins/cordova-plugin-file/src/android/ContentFilesystem.java
new file mode 100644
index 00000000..883e7cf5
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/ContentFilesystem.java
@@ -0,0 +1,215 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT 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.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.cordova.CordovaResourceApi;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+
+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.
+ throw new NoModificationAllowedException("Deleting not supported for content uri: " + contentUri);
+ }
+ 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()) {
+ size = resourceSizeForCursor(cursor);
+ lastModified = lastModifiedDateForCursor(cursor);
+ } else {
+ // Some content providers don't support cursors at all!
+ CordovaResourceApi.OpenForReadResult offr = resourceApi.openForRead(nativeUri);
+ size = offr.length;
+ }
+ } catch (IOException e) {
+ throw new FileNotFoundException();
+ } 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) {
+ final String[] LOCAL_FILE_PROJECTION = { MediaStore.MediaColumns.DATE_MODIFIED };
+ int columnIndex = cursor.getColumnIndex(LOCAL_FILE_PROJECTION[0]);
+ 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;
+ }
+}
diff --git a/plugins/cordova-plugin-file/src/android/DirectoryManager.java b/plugins/cordova-plugin-file/src/android/DirectoryManager.java
new file mode 100644
index 00000000..bcc005b2
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/DirectoryManager.java
@@ -0,0 +1,133 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT 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 disk space
+ *
+ * @return Size in KB or -1 if not available
+ */
+ public static long getFreeDiskSpace(boolean checkInternal) {
+ String status = Environment.getExternalStorageState();
+ long freeSpace = 0;
+
+ // If SD card exists
+ if (status.equals(Environment.MEDIA_MOUNTED)) {
+ freeSpace = freeSpaceCalculation(Environment.getExternalStorageDirectory().getPath());
+ }
+ else if (checkInternal) {
+ freeSpace = freeSpaceCalculation("/");
+ }
+ // If no SD card and we haven't been asked to check the internal directory then return -1
+ else {
+ return -1;
+ }
+
+ return freeSpace;
+ }
+
+ /**
+ * Given a path return the number of free KB
+ *
+ * @param path to the file system
+ * @return free space in KB
+ */
+ private static long freeSpaceCalculation(String path) {
+ StatFs stat = new StatFs(path);
+ long blockSize = stat.getBlockSize();
+ long availableBlocks = stat.getAvailableBlocks();
+ return availableBlocks * blockSize / 1024;
+ }
+
+ /**
+ * Determine if SD card exists.
+ *
+ * @return T=exists, F=not found
+ */
+ public static boolean testSaveLocationExists() {
+ String sDCardStatus = Environment.getExternalStorageState();
+ boolean status;
+
+ // If SD card is mounted
+ if (sDCardStatus.equals(Environment.MEDIA_MOUNTED)) {
+ status = true;
+ }
+
+ // If no SD card
+ else {
+ status = false;
+ }
+ return status;
+ }
+
+ /**
+ * Create a new file object from two file paths.
+ *
+ * @param file1 Base file path
+ * @param file2 Remaining file path
+ * @return File object
+ */
+ private static File constructFilePaths (String file1, String file2) {
+ File newPath;
+ if (file2.startsWith(file1)) {
+ newPath = new File(file2);
+ }
+ else {
+ newPath = new File(file1 + "/" + file2);
+ }
+ return newPath;
+ }
+}
diff --git a/plugins/cordova-plugin-file/src/android/EncodingException.java b/plugins/cordova-plugin-file/src/android/EncodingException.java
new file mode 100644
index 00000000..e9e1653b
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/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);
+ }
+
+}
diff --git a/plugins/cordova-plugin-file/src/android/FileExistsException.java b/plugins/cordova-plugin-file/src/android/FileExistsException.java
new file mode 100644
index 00000000..5c4d83dc
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/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);
+ }
+
+}
diff --git a/plugins/cordova-plugin-file/src/android/FileUtils.java b/plugins/cordova-plugin-file/src/android/FileUtils.java
new file mode 100644
index 00000000..f57d26b3
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/FileUtils.java
@@ -0,0 +1,1027 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT 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.app.Activity;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Base64;
+import android.util.Log;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CordovaWebView;
+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.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;
+
+ public static int UNKNOWN_ERR = 1000;
+
+ private boolean configured = false;
+
+ // 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 = activity.getIntent().getStringExtra("androidextrafilesystems");
+ if (fileSystemsStr == null) {
+ fileSystemsStr = "files,files-external,documents,sdcard,cache,cache-external,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>();
+
+ String tempRoot = null;
+ String persistentRoot = null;
+
+ Activity activity = cordova.getActivity();
+ String packageName = activity.getPackageName();
+
+ String location = activity.getIntent().getStringExtra("androidpersistentfilelocation");
+ if (location == null) {
+ location = "compatibility";
+ }
+ 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) {
+ long l = DirectoryManager.getFreeDiskSpace(false);
+ 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 data=args.getString(1);
+ int offset=args.getInt(2);
+ Boolean isBinary=args.getBoolean(3);
+ 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 IOException, JSONException {
+ int fstype=args.getInt(0);
+ long size = args.optLong(1);
+ if (size != 0 && size > (DirectoryManager.getFreeDiskSpace(true) * 1024)) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FileUtils.QUOTA_EXCEEDED_ERR));
+ } else {
+ JSONObject obj = requestFileSystem(fstype);
+ callbackContext.success(obj);
+ }
+ }
+ }, 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);
+ 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);
+ 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;
+ }
+
+ 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) {
+ throw new MalformedURLException("Unrecognized filesystem URL");
+ }
+ }
+
+ 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 {
+ 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);
+
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(uri);
+ if (inputURL == null) {
+ /* Check for file://, content:// urls */
+ inputURL = resolveNativeUri(uri);
+ }
+
+ try {
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ throw new MalformedURLException("No installed handlers for this URL");
+ }
+ if (fs.exists(inputURL)) {
+ return fs.getEntryForLocalURL(inputURL);
+ }
+ } catch (IllegalArgumentException e) {
+ throw new MalformedURLException("Unrecognized filesystem URL");
+ }
+ 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) {
+ throw new MalformedURLException("Unrecognized filesystem URL");
+ }
+ }
+
+ /**
+ * 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) {
+ throw new MalformedURLException("Unrecognized filesystem URL");
+ }
+ }
+
+
+ /**
+ * 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) {
+ throw new MalformedURLException("Unrecognized filesystem URL");
+ }
+ }
+
+ /**
+ * 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) {
+ throw new MalformedURLException("Unrecognized filesystem URL");
+ }
+
+ }
+
+ /**
+ * 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) {
+ throw new MalformedURLException("Unrecognized filesystem URL");
+ }
+ }
+
+ /**
+ * 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) {
+ throw new MalformedURLException("Unrecognized filesystem URL");
+ }
+ }
+
+ /**
+ * Requests a filesystem in which to store application data.
+ *
+ * @param type of file system requested
+ * @return a JSONObject representing the file system
+ * @throws IOException
+ * @throws JSONException
+ */
+ private JSONObject requestFileSystem(int type) throws IOException, JSONException {
+ JSONObject fs = new JSONObject();
+ Filesystem rootFs = null;
+ try {
+ rootFs = this.filesystems.get(type);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // Pass null through
+ }
+ if (rootFs == null) {
+ throw new IOException("No filesystem of type requested");
+ }
+ fs.put("name", rootFs.name);
+ fs.put("root", rootFs.getRootEntry());
+ return 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) {
+ throw new MalformedURLException("Unrecognized filesystem URL");
+ } 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) {
+ throw new MalformedURLException("Unrecognized filesystem URL");
+ }
+
+ }
+
+ /**
+ * 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) {
+ throw new MalformedURLException("Unrecognized filesystem URL");
+ }
+ }
+}
diff --git a/plugins/cordova-plugin-file/src/android/Filesystem.java b/plugins/cordova-plugin-file/src/android/Filesystem.java
new file mode 100644
index 00000000..faf31d2a
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/Filesystem.java
@@ -0,0 +1,325 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT 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);
+ }
+ }
+
+
+
+ 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;
+ }
+ }
+}
diff --git a/plugins/cordova-plugin-file/src/android/InvalidModificationException.java b/plugins/cordova-plugin-file/src/android/InvalidModificationException.java
new file mode 100644
index 00000000..8f6bec59
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/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);
+ }
+
+}
diff --git a/plugins/cordova-plugin-file/src/android/LocalFilesystem.java b/plugins/cordova-plugin-file/src/android/LocalFilesystem.java
new file mode 100644
index 00000000..3b1ecca8
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/LocalFilesystem.java
@@ -0,0 +1,505 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT 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;
+
+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() || inputURL.getPath().endsWith("/")) {
+ // 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 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();
+ }
+ 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());
+ 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();
+ }
+ }
+ }
+}
diff --git a/plugins/cordova-plugin-file/src/android/LocalFilesystemURL.java b/plugins/cordova-plugin-file/src/android/LocalFilesystemURL.java
new file mode 100644
index 00000000..74f43db6
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/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();
+ }
+}
diff --git a/plugins/cordova-plugin-file/src/android/NoModificationAllowedException.java b/plugins/cordova-plugin-file/src/android/NoModificationAllowedException.java
new file mode 100644
index 00000000..627eafb5
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/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);
+ }
+
+}
diff --git a/plugins/cordova-plugin-file/src/android/TypeMismatchException.java b/plugins/cordova-plugin-file/src/android/TypeMismatchException.java
new file mode 100644
index 00000000..1315f9a9
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/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);
+ }
+
+}
diff --git a/plugins/cordova-plugin-file/src/android/build-extras.gradle b/plugins/cordova-plugin-file/src/android/build-extras.gradle
new file mode 100644
index 00000000..a0a7844a
--- /dev/null
+++ b/plugins/cordova-plugin-file/src/android/build-extras.gradle
@@ -0,0 +1,47 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ */
+ext.postBuildExtras = {
+ def inAssetsDir = file("assets")
+ def outAssetsDir = inAssetsDir
+ def outFile = new File(outAssetsDir, "cdvasset.manifest")
+
+ def newTask = task("cdvCreateAssetManifest") << {
+ def contents = new HashMap()
+ def sizes = new HashMap()
+ contents[""] = inAssetsDir.list()
+ def tree = fileTree(dir: inAssetsDir)
+ tree.visit { fileDetails ->
+ if (fileDetails.isDirectory()) {
+ contents[fileDetails.relativePath.toString()] = fileDetails.file.list()
+ } else {
+ sizes[fileDetails.relativePath.toString()] = fileDetails.file.length()
+ }
+ }
+
+ outAssetsDir.mkdirs()
+ outFile.withObjectOutputStream { oos ->
+ oos.writeObject(contents)
+ oos.writeObject(sizes)
+ }
+ }
+ newTask.inputs.dir inAssetsDir
+ newTask.outputs.file outFile
+ def preBuildTask = tasks["preBuild"]
+ preBuildTask.dependsOn(newTask)
+}