openEventsPlatformApp/plugins/cordova-plugin-file/src/android/FileUtils.java
plugins/cordova-plugin-file/src/android/FileUtils.javaRaw
1/*
2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied. See the License for the
16 specific language governing permissions and limitations
17 under the License.
18 */
19package org.apache.cordova.file;
20
21import android.Manifest;
22import android.app.Activity;
23import android.content.Context;
24import android.content.pm.PackageManager;
25import android.net.Uri;
26import android.os.Build;
27import android.os.Environment;
28import android.util.Base64;
29
30import org.apache.cordova.CallbackContext;
31import org.apache.cordova.CordovaInterface;
32import org.apache.cordova.CordovaPlugin;
33import org.apache.cordova.CordovaWebView;
34import org.apache.cordova.LOG;
35import org.apache.cordova.PermissionHelper;
36import org.apache.cordova.PluginResult;
37
38import org.json.JSONArray;
39import org.json.JSONException;
40import org.json.JSONObject;
41
42import java.io.ByteArrayOutputStream;
43import java.io.File;
44import java.io.FileNotFoundException;
45import java.io.IOException;
46import java.io.InputStream;
47import java.net.MalformedURLException;
48import java.security.Permission;
49import java.util.ArrayList;
50import java.util.HashMap;
51import java.util.HashSet;
52
53/**
54 * This class provides file and directory services to JavaScript.
55 */
56public class FileUtils extends CordovaPlugin {
57 private static final String LOG_TAG = "FileUtils";
58
59 public static int NOT_FOUND_ERR = 1;
60 public static int SECURITY_ERR = 2;
61 public static int ABORT_ERR = 3;
62
63 public static int NOT_READABLE_ERR = 4;
64 public static int ENCODING_ERR = 5;
65 public static int NO_MODIFICATION_ALLOWED_ERR = 6;
66 public static int INVALID_STATE_ERR = 7;
67 public static int SYNTAX_ERR = 8;
68 public static int INVALID_MODIFICATION_ERR = 9;
69 public static int QUOTA_EXCEEDED_ERR = 10;
70 public static int TYPE_MISMATCH_ERR = 11;
71 public static int PATH_EXISTS_ERR = 12;
72
73 /*
74 * Permission callback codes
75 */
76
77 public static final int ACTION_GET_FILE = 0;
78 public static final int ACTION_WRITE = 1;
79 public static final int ACTION_GET_DIRECTORY = 2;
80
81 public static final int WRITE = 3;
82 public static final int READ = 4;
83
84 public static int UNKNOWN_ERR = 1000;
85
86 private boolean configured = false;
87
88 private PendingRequests pendingRequests;
89
90
91
92 /*
93 * We need both read and write when accessing the storage, I think.
94 */
95
96 private String [] permissions = {
97 Manifest.permission.READ_EXTERNAL_STORAGE,
98 Manifest.permission.WRITE_EXTERNAL_STORAGE };
99
100 // This field exists only to support getEntry, below, which has been deprecated
101 private static FileUtils filePlugin;
102
103 private interface FileOp {
104 void run(JSONArray args) throws Exception;
105 }
106
107 private ArrayList<Filesystem> filesystems;
108
109 public void registerFilesystem(Filesystem fs) {
110 if (fs != null && filesystemForName(fs.name)== null) {
111 this.filesystems.add(fs);
112 }
113 }
114
115 private Filesystem filesystemForName(String name) {
116 for (Filesystem fs:filesystems) {
117 if (fs != null && fs.name != null && fs.name.equals(name)) {
118 return fs;
119 }
120 }
121 return null;
122 }
123
124 protected String[] getExtraFileSystemsPreference(Activity activity) {
125 String fileSystemsStr = preferences.getString("androidextrafilesystems", "files,files-external,documents,sdcard,cache,cache-external,assets,root");
126 return fileSystemsStr.split(",");
127 }
128
129 protected void registerExtraFileSystems(String[] filesystems, HashMap<String, String> availableFileSystems) {
130 HashSet<String> installedFileSystems = new HashSet<String>();
131
132 /* Register filesystems in order */
133 for (String fsName : filesystems) {
134 if (!installedFileSystems.contains(fsName)) {
135 String fsRoot = availableFileSystems.get(fsName);
136 if (fsRoot != null) {
137 File newRoot = new File(fsRoot);
138 if (newRoot.mkdirs() || newRoot.isDirectory()) {
139 registerFilesystem(new LocalFilesystem(fsName, webView.getContext(), webView.getResourceApi(), newRoot));
140 installedFileSystems.add(fsName);
141 } else {
142 LOG.d(LOG_TAG, "Unable to create root dir for filesystem \"" + fsName + "\", skipping");
143 }
144 } else {
145 LOG.d(LOG_TAG, "Unrecognized extra filesystem identifier: " + fsName);
146 }
147 }
148 }
149 }
150
151 protected HashMap<String, String> getAvailableFileSystems(Activity activity) {
152 Context context = activity.getApplicationContext();
153 HashMap<String, String> availableFileSystems = new HashMap<String,String>();
154
155 availableFileSystems.put("files", context.getFilesDir().getAbsolutePath());
156 availableFileSystems.put("documents", new File(context.getFilesDir(), "Documents").getAbsolutePath());
157 availableFileSystems.put("cache", context.getCacheDir().getAbsolutePath());
158 availableFileSystems.put("root", "/");
159 if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
160 try {
161 availableFileSystems.put("files-external", context.getExternalFilesDir(null).getAbsolutePath());
162 availableFileSystems.put("sdcard", Environment.getExternalStorageDirectory().getAbsolutePath());
163 availableFileSystems.put("cache-external", context.getExternalCacheDir().getAbsolutePath());
164 }
165 catch(NullPointerException e) {
166 LOG.d(LOG_TAG, "External storage unavailable, check to see if USB Mass Storage Mode is on");
167 }
168 }
169
170 return availableFileSystems;
171 }
172
173 @Override
174 public void initialize(CordovaInterface cordova, CordovaWebView webView) {
175 super.initialize(cordova, webView);
176 this.filesystems = new ArrayList<Filesystem>();
177 this.pendingRequests = new PendingRequests();
178
179 String tempRoot = null;
180 String persistentRoot = null;
181
182 Activity activity = cordova.getActivity();
183 String packageName = activity.getPackageName();
184
185 String location = preferences.getString("androidpersistentfilelocation", "internal");
186
187 tempRoot = activity.getCacheDir().getAbsolutePath();
188 if ("internal".equalsIgnoreCase(location)) {
189 persistentRoot = activity.getFilesDir().getAbsolutePath() + "/files/";
190 this.configured = true;
191 } else if ("compatibility".equalsIgnoreCase(location)) {
192 /*
193 * Fall-back to compatibility mode -- this is the logic implemented in
194 * earlier versions of this plugin, and should be maintained here so
195 * that apps which were originally deployed with older versions of the
196 * plugin can continue to provide access to files stored under those
197 * versions.
198 */
199 if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
200 persistentRoot = Environment.getExternalStorageDirectory().getAbsolutePath();
201 tempRoot = Environment.getExternalStorageDirectory().getAbsolutePath() +
202 "/Android/data/" + packageName + "/cache/";
203 } else {
204 persistentRoot = "/data/data/" + packageName;
205 }
206 this.configured = true;
207 }
208
209 if (this.configured) {
210 // Create the directories if they don't exist.
211 File tmpRootFile = new File(tempRoot);
212 File persistentRootFile = new File(persistentRoot);
213 tmpRootFile.mkdirs();
214 persistentRootFile.mkdirs();
215
216 // Register initial filesystems
217 // Note: The temporary and persistent filesystems need to be the first two
218 // registered, so that they will match window.TEMPORARY and window.PERSISTENT,
219 // per spec.
220 this.registerFilesystem(new LocalFilesystem("temporary", webView.getContext(), webView.getResourceApi(), tmpRootFile));
221 this.registerFilesystem(new LocalFilesystem("persistent", webView.getContext(), webView.getResourceApi(), persistentRootFile));
222 this.registerFilesystem(new ContentFilesystem(webView.getContext(), webView.getResourceApi()));
223 this.registerFilesystem(new AssetFilesystem(webView.getContext().getAssets(), webView.getResourceApi()));
224
225 registerExtraFileSystems(getExtraFileSystemsPreference(activity), getAvailableFileSystems(activity));
226
227 // Initialize static plugin reference for deprecated getEntry method
228 if (filePlugin == null) {
229 FileUtils.filePlugin = this;
230 }
231 } else {
232 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)");
233 activity.finish();
234 }
235 }
236
237 public static FileUtils getFilePlugin() {
238 return filePlugin;
239 }
240
241 private Filesystem filesystemForURL(LocalFilesystemURL localURL) {
242 if (localURL == null) return null;
243 return filesystemForName(localURL.fsName);
244 }
245
246 @Override
247 public Uri remapUri(Uri uri) {
248 // Remap only cdvfile: URLs (not content:).
249 if (!LocalFilesystemURL.FILESYSTEM_PROTOCOL.equals(uri.getScheme())) {
250 return null;
251 }
252 try {
253 LocalFilesystemURL inputURL = LocalFilesystemURL.parse(uri);
254 Filesystem fs = this.filesystemForURL(inputURL);
255 if (fs == null) {
256 return null;
257 }
258 String path = fs.filesystemPathForURL(inputURL);
259 if (path != null) {
260 return Uri.parse("file://" + fs.filesystemPathForURL(inputURL));
261 }
262 return null;
263 } catch (IllegalArgumentException e) {
264 return null;
265 }
266 }
267
268 public boolean execute(String action, final String rawArgs, final CallbackContext callbackContext) {
269 if (!configured) {
270 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"));
271 return true;
272 }
273 if (action.equals("testSaveLocationExists")) {
274 threadhelper(new FileOp() {
275 public void run(JSONArray args) {
276 boolean b = DirectoryManager.testSaveLocationExists();
277 callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
278 }
279 }, rawArgs, callbackContext);
280 }
281 else if (action.equals("getFreeDiskSpace")) {
282 threadhelper( new FileOp( ){
283 public void run(JSONArray args) {
284 // The getFreeDiskSpace plugin API is not documented, but some apps call it anyway via exec().
285 // For compatibility it always returns free space in the primary external storage, and
286 // does NOT fallback to internal store if external storage is unavailable.
287 long l = DirectoryManager.getFreeExternalStorageSpace();
288 callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, l));
289 }
290 }, rawArgs, callbackContext);
291 }
292 else if (action.equals("testFileExists")) {
293 threadhelper( new FileOp( ){
294 public void run(JSONArray args) throws JSONException {
295 String fname=args.getString(0);
296 boolean b = DirectoryManager.testFileExists(fname);
297 callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
298 }
299 }, rawArgs, callbackContext);
300 }
301 else if (action.equals("testDirectoryExists")) {
302 threadhelper( new FileOp( ){
303 public void run(JSONArray args) throws JSONException {
304 String fname=args.getString(0);
305 boolean b = DirectoryManager.testFileExists(fname);
306 callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
307 }
308 }, rawArgs, callbackContext);
309 }
310 else if (action.equals("readAsText")) {
311 threadhelper( new FileOp( ){
312 public void run(JSONArray args) throws JSONException, MalformedURLException {
313 String encoding = args.getString(1);
314 int start = args.getInt(2);
315 int end = args.getInt(3);
316 String fname=args.getString(0);
317 readFileAs(fname, start, end, callbackContext, encoding, PluginResult.MESSAGE_TYPE_STRING);
318 }
319 }, rawArgs, callbackContext);
320 }
321 else if (action.equals("readAsDataURL")) {
322 threadhelper( new FileOp( ){
323 public void run(JSONArray args) throws JSONException, MalformedURLException {
324 int start = args.getInt(1);
325 int end = args.getInt(2);
326 String fname=args.getString(0);
327 readFileAs(fname, start, end, callbackContext, null, -1);
328 }
329 }, rawArgs, callbackContext);
330 }
331 else if (action.equals("readAsArrayBuffer")) {
332 threadhelper( new FileOp( ){
333 public void run(JSONArray args) throws JSONException, MalformedURLException {
334 int start = args.getInt(1);
335 int end = args.getInt(2);
336 String fname=args.getString(0);
337 readFileAs(fname, start, end, callbackContext, null, PluginResult.MESSAGE_TYPE_ARRAYBUFFER);
338 }
339 }, rawArgs, callbackContext);
340 }
341 else if (action.equals("readAsBinaryString")) {
342 threadhelper( new FileOp( ){
343 public void run(JSONArray args) throws JSONException, MalformedURLException {
344 int start = args.getInt(1);
345 int end = args.getInt(2);
346 String fname=args.getString(0);
347 readFileAs(fname, start, end, callbackContext, null, PluginResult.MESSAGE_TYPE_BINARYSTRING);
348 }
349 }, rawArgs, callbackContext);
350 }
351 else if (action.equals("write")) {
352 threadhelper( new FileOp( ){
353 public void run(JSONArray args) throws JSONException, FileNotFoundException, IOException, NoModificationAllowedException {
354 String fname=args.getString(0);
355 String nativeURL = resolveLocalFileSystemURI(fname).getString("nativeURL");
356 String data=args.getString(1);
357 int offset=args.getInt(2);
358 Boolean isBinary=args.getBoolean(3);
359
360 if(needPermission(nativeURL, WRITE)) {
361 getWritePermission(rawArgs, ACTION_WRITE, callbackContext);
362 }
363 else {
364 long fileSize = write(fname, data, offset, isBinary);
365 callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
366 }
367
368 }
369 }, rawArgs, callbackContext);
370 }
371 else if (action.equals("truncate")) {
372 threadhelper( new FileOp( ){
373 public void run(JSONArray args) throws JSONException, FileNotFoundException, IOException, NoModificationAllowedException {
374 String fname=args.getString(0);
375 int offset=args.getInt(1);
376 long fileSize = truncateFile(fname, offset);
377 callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
378 }
379 }, rawArgs, callbackContext);
380 }
381 else if (action.equals("requestAllFileSystems")) {
382 threadhelper( new FileOp( ){
383 public void run(JSONArray args) throws IOException, JSONException {
384 callbackContext.success(requestAllFileSystems());
385 }
386 }, rawArgs, callbackContext);
387 } else if (action.equals("requestAllPaths")) {
388 cordova.getThreadPool().execute(
389 new Runnable() {
390 public void run() {
391 try {
392 callbackContext.success(requestAllPaths());
393 } catch (JSONException e) {
394 // TODO Auto-generated catch block
395 e.printStackTrace();
396 }
397 }
398 }
399 );
400 } else if (action.equals("requestFileSystem")) {
401 threadhelper( new FileOp( ){
402 public void run(JSONArray args) throws JSONException {
403 int fstype = args.getInt(0);
404 long requiredSize = args.optLong(1);
405 requestFileSystem(fstype, requiredSize, callbackContext);
406 }
407 }, rawArgs, callbackContext);
408 }
409 else if (action.equals("resolveLocalFileSystemURI")) {
410 threadhelper( new FileOp( ){
411 public void run(JSONArray args) throws IOException, JSONException {
412 String fname=args.getString(0);
413 JSONObject obj = resolveLocalFileSystemURI(fname);
414 callbackContext.success(obj);
415 }
416 }, rawArgs, callbackContext);
417 }
418 else if (action.equals("getFileMetadata")) {
419 threadhelper( new FileOp( ){
420 public void run(JSONArray args) throws FileNotFoundException, JSONException, MalformedURLException {
421 String fname=args.getString(0);
422 JSONObject obj = getFileMetadata(fname);
423 callbackContext.success(obj);
424 }
425 }, rawArgs, callbackContext);
426 }
427 else if (action.equals("getParent")) {
428 threadhelper( new FileOp( ){
429 public void run(JSONArray args) throws JSONException, IOException {
430 String fname=args.getString(0);
431 JSONObject obj = getParent(fname);
432 callbackContext.success(obj);
433 }
434 }, rawArgs, callbackContext);
435 }
436 else if (action.equals("getDirectory")) {
437 threadhelper( new FileOp( ){
438 public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
439 String dirname = args.getString(0);
440 String path = args.getString(1);
441 String nativeURL = resolveLocalFileSystemURI(dirname).getString("nativeURL");
442 boolean containsCreate = (args.isNull(2)) ? false : args.getJSONObject(2).optBoolean("create", false);
443
444 if(containsCreate && needPermission(nativeURL, WRITE)) {
445 getWritePermission(rawArgs, ACTION_GET_DIRECTORY, callbackContext);
446 }
447 else if(!containsCreate && needPermission(nativeURL, READ)) {
448 getReadPermission(rawArgs, ACTION_GET_DIRECTORY, callbackContext);
449 }
450 else {
451 JSONObject obj = getFile(dirname, path, args.optJSONObject(2), true);
452 callbackContext.success(obj);
453 }
454 }
455 }, rawArgs, callbackContext);
456 }
457 else if (action.equals("getFile")) {
458 threadhelper( new FileOp( ){
459 public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
460 String dirname = args.getString(0);
461 String path = args.getString(1);
462 String nativeURL = resolveLocalFileSystemURI(dirname).getString("nativeURL");
463 boolean containsCreate = (args.isNull(2)) ? false : args.getJSONObject(2).optBoolean("create", false);
464
465 if(containsCreate && needPermission(nativeURL, WRITE)) {
466 getWritePermission(rawArgs, ACTION_GET_FILE, callbackContext);
467 }
468 else if(!containsCreate && needPermission(nativeURL, READ)) {
469 getReadPermission(rawArgs, ACTION_GET_FILE, callbackContext);
470 }
471 else {
472 JSONObject obj = getFile(dirname, path, args.optJSONObject(2), false);
473 callbackContext.success(obj);
474 }
475 }
476 }, rawArgs, callbackContext);
477 }
478 else if (action.equals("remove")) {
479 threadhelper( new FileOp( ){
480 public void run(JSONArray args) throws JSONException, NoModificationAllowedException, InvalidModificationException, MalformedURLException {
481 String fname=args.getString(0);
482 boolean success = remove(fname);
483 if (success) {
484 callbackContext.success();
485 } else {
486 callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
487 }
488 }
489 }, rawArgs, callbackContext);
490 }
491 else if (action.equals("removeRecursively")) {
492 threadhelper( new FileOp( ){
493 public void run(JSONArray args) throws JSONException, FileExistsException, MalformedURLException, NoModificationAllowedException {
494 String fname=args.getString(0);
495 boolean success = removeRecursively(fname);
496 if (success) {
497 callbackContext.success();
498 } else {
499 callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
500 }
501 }
502 }, rawArgs, callbackContext);
503 }
504 else if (action.equals("moveTo")) {
505 threadhelper( new FileOp( ){
506 public void run(JSONArray args) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException {
507 String fname=args.getString(0);
508 String newParent=args.getString(1);
509 String newName=args.getString(2);
510 JSONObject entry = transferTo(fname, newParent, newName, true);
511 callbackContext.success(entry);
512 }
513 }, rawArgs, callbackContext);
514 }
515 else if (action.equals("copyTo")) {
516 threadhelper( new FileOp( ){
517 public void run(JSONArray args) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException {
518 String fname=args.getString(0);
519 String newParent=args.getString(1);
520 String newName=args.getString(2);
521 JSONObject entry = transferTo(fname, newParent, newName, false);
522 callbackContext.success(entry);
523 }
524 }, rawArgs, callbackContext);
525 }
526 else if (action.equals("readEntries")) {
527 threadhelper( new FileOp( ){
528 public void run(JSONArray args) throws FileNotFoundException, JSONException, MalformedURLException {
529 String fname=args.getString(0);
530 JSONArray entries = readEntries(fname);
531 callbackContext.success(entries);
532 }
533 }, rawArgs, callbackContext);
534 }
535 else if (action.equals("_getLocalFilesystemPath")) {
536 // Internal method for testing: Get the on-disk location of a local filesystem url.
537 // [Currently used for testing file-transfer]
538 threadhelper( new FileOp( ){
539 public void run(JSONArray args) throws FileNotFoundException, JSONException, MalformedURLException {
540 String localURLstr = args.getString(0);
541 String fname = filesystemPathForURL(localURLstr);
542 callbackContext.success(fname);
543 }
544 }, rawArgs, callbackContext);
545 }
546 else {
547 return false;
548 }
549 return true;
550 }
551
552 private void getReadPermission(String rawArgs, int action, CallbackContext callbackContext) {
553 int requestCode = pendingRequests.createRequest(rawArgs, action, callbackContext);
554 PermissionHelper.requestPermission(this, requestCode, Manifest.permission.READ_EXTERNAL_STORAGE);
555 }
556
557 private void getWritePermission(String rawArgs, int action, CallbackContext callbackContext) {
558 int requestCode = pendingRequests.createRequest(rawArgs, action, callbackContext);
559 PermissionHelper.requestPermission(this, requestCode, Manifest.permission.WRITE_EXTERNAL_STORAGE);
560 }
561
562 private boolean hasReadPermission() {
563 return PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
564 }
565
566 private boolean hasWritePermission() {
567 return PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
568 }
569
570 private boolean needPermission(String nativeURL, int permissionType) throws JSONException {
571 JSONObject j = requestAllPaths();
572 ArrayList<String> allowedStorageDirectories = new ArrayList<String>();
573 allowedStorageDirectories.add(j.getString("applicationStorageDirectory"));
574 if(j.has("externalApplicationStorageDirectory")) {
575 allowedStorageDirectories.add(j.getString("externalApplicationStorageDirectory"));
576 }
577
578 if(permissionType == READ && hasReadPermission()) {
579 return false;
580 }
581 else if(permissionType == WRITE && hasWritePermission()) {
582 return false;
583 }
584
585 // Permission required if the native url lies outside the allowed storage directories
586 for(String directory : allowedStorageDirectories) {
587 if(nativeURL.startsWith(directory)) {
588 return false;
589 }
590 }
591 return true;
592 }
593
594
595 public LocalFilesystemURL resolveNativeUri(Uri nativeUri) {
596 LocalFilesystemURL localURL = null;
597
598 // Try all installed filesystems. Return the best matching URL
599 // (determined by the shortest resulting URL)
600 for (Filesystem fs : filesystems) {
601 LocalFilesystemURL url = fs.toLocalUri(nativeUri);
602 if (url != null) {
603 // A shorter fullPath implies that the filesystem is a better
604 // match for the local path than the previous best.
605 if (localURL == null || (url.uri.toString().length() < localURL.toString().length())) {
606 localURL = url;
607 }
608 }
609 }
610 return localURL;
611 }
612
613 /*
614 * These two native-only methods can be used by other plugins to translate between
615 * device file system paths and URLs. By design, there is no direct JavaScript
616 * interface to these methods.
617 */
618
619 public String filesystemPathForURL(String localURLstr) throws MalformedURLException {
620 try {
621 LocalFilesystemURL inputURL = LocalFilesystemURL.parse(localURLstr);
622 Filesystem fs = this.filesystemForURL(inputURL);
623 if (fs == null) {
624 throw new MalformedURLException("No installed handlers for this URL");
625 }
626 return fs.filesystemPathForURL(inputURL);
627 } catch (IllegalArgumentException e) {
628 MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
629 mue.initCause(e);
630 throw mue;
631 }
632 }
633
634 public LocalFilesystemURL filesystemURLforLocalPath(String localPath) {
635 LocalFilesystemURL localURL = null;
636 int shortestFullPath = 0;
637
638 // Try all installed filesystems. Return the best matching URL
639 // (determined by the shortest resulting URL)
640 for (Filesystem fs: filesystems) {
641 LocalFilesystemURL url = fs.URLforFilesystemPath(localPath);
642 if (url != null) {
643 // A shorter fullPath implies that the filesystem is a better
644 // match for the local path than the previous best.
645 if (localURL == null || (url.path.length() < shortestFullPath)) {
646 localURL = url;
647 shortestFullPath = url.path.length();
648 }
649 }
650 }
651 return localURL;
652 }
653
654
655 /* helper to execute functions async and handle the result codes
656 *
657 */
658 private void threadhelper(final FileOp f, final String rawArgs, final CallbackContext callbackContext){
659 cordova.getThreadPool().execute(new Runnable() {
660 public void run() {
661 try {
662 JSONArray args = new JSONArray(rawArgs);
663 f.run(args);
664 } catch ( Exception e) {
665 if( e instanceof EncodingException){
666 callbackContext.error(FileUtils.ENCODING_ERR);
667 } else if(e instanceof FileNotFoundException) {
668 callbackContext.error(FileUtils.NOT_FOUND_ERR);
669 } else if(e instanceof FileExistsException) {
670 callbackContext.error(FileUtils.PATH_EXISTS_ERR);
671 } else if(e instanceof NoModificationAllowedException ) {
672 callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
673 } else if(e instanceof InvalidModificationException ) {
674 callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR);
675 } else if(e instanceof MalformedURLException ) {
676 callbackContext.error(FileUtils.ENCODING_ERR);
677 } else if(e instanceof IOException ) {
678 callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR);
679 } else if(e instanceof EncodingException ) {
680 callbackContext.error(FileUtils.ENCODING_ERR);
681 } else if(e instanceof TypeMismatchException ) {
682 callbackContext.error(FileUtils.TYPE_MISMATCH_ERR);
683 } else if(e instanceof JSONException ) {
684 callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
685 } else if (e instanceof SecurityException) {
686 callbackContext.error(FileUtils.SECURITY_ERR);
687 } else {
688 e.printStackTrace();
689 callbackContext.error(FileUtils.UNKNOWN_ERR);
690 }
691 }
692 }
693 });
694 }
695
696 /**
697 * Allows the user to look up the Entry for a file or directory referred to by a local URI.
698 *
699 * @param uriString of the file/directory to look up
700 * @return a JSONObject representing a Entry from the filesystem
701 * @throws MalformedURLException if the url is not valid
702 * @throws FileNotFoundException if the file does not exist
703 * @throws IOException if the user can't read the file
704 * @throws JSONException
705 */
706 private JSONObject resolveLocalFileSystemURI(String uriString) throws IOException, JSONException {
707 if (uriString == null) {
708 throw new MalformedURLException("Unrecognized filesystem URL");
709 }
710 Uri uri = Uri.parse(uriString);
711 boolean isNativeUri = false;
712
713 LocalFilesystemURL inputURL = LocalFilesystemURL.parse(uri);
714 if (inputURL == null) {
715 /* Check for file://, content:// urls */
716 inputURL = resolveNativeUri(uri);
717 isNativeUri = true;
718 }
719
720 try {
721 Filesystem fs = this.filesystemForURL(inputURL);
722 if (fs == null) {
723 throw new MalformedURLException("No installed handlers for this URL");
724 }
725 if (fs.exists(inputURL)) {
726 if (!isNativeUri) {
727 // If not already resolved as native URI, resolve to a native URI and back to
728 // fix the terminating slash based on whether the entry is a directory or file.
729 inputURL = fs.toLocalUri(fs.toNativeUri(inputURL));
730 }
731
732 return fs.getEntryForLocalURL(inputURL);
733 }
734 } catch (IllegalArgumentException e) {
735 MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
736 mue.initCause(e);
737 throw mue;
738 }
739 throw new FileNotFoundException();
740 }
741
742 /**
743 * Read the list of files from this directory.
744 *
745 * @return a JSONArray containing JSONObjects that represent Entry objects.
746 * @throws FileNotFoundException if the directory is not found.
747 * @throws JSONException
748 * @throws MalformedURLException
749 */
750 private JSONArray readEntries(String baseURLstr) throws FileNotFoundException, JSONException, MalformedURLException {
751 try {
752 LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
753 Filesystem fs = this.filesystemForURL(inputURL);
754 if (fs == null) {
755 throw new MalformedURLException("No installed handlers for this URL");
756 }
757 return fs.readEntriesAtLocalURL(inputURL);
758
759 } catch (IllegalArgumentException e) {
760 MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
761 mue.initCause(e);
762 throw mue;
763 }
764 }
765
766 /**
767 * A setup method that handles the move/copy of files/directories
768 *
769 * @param newName for the file directory to be called, if null use existing file name
770 * @param move if false do a copy, if true do a move
771 * @return a Entry object
772 * @throws NoModificationAllowedException
773 * @throws IOException
774 * @throws InvalidModificationException
775 * @throws EncodingException
776 * @throws JSONException
777 * @throws FileExistsException
778 */
779 private JSONObject transferTo(String srcURLstr, String destURLstr, String newName, boolean move) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException {
780 if (srcURLstr == null || destURLstr == null) {
781 // either no source or no destination provided
782 throw new FileNotFoundException();
783 }
784
785 LocalFilesystemURL srcURL = LocalFilesystemURL.parse(srcURLstr);
786 LocalFilesystemURL destURL = LocalFilesystemURL.parse(destURLstr);
787
788 Filesystem srcFs = this.filesystemForURL(srcURL);
789 Filesystem destFs = this.filesystemForURL(destURL);
790
791 // Check for invalid file name
792 if (newName != null && newName.contains(":")) {
793 throw new EncodingException("Bad file name");
794 }
795
796 return destFs.copyFileToURL(destURL, newName, srcFs, srcURL, move);
797 }
798
799 /**
800 * Deletes a directory and all of its contents, if any. In the event of an error
801 * [e.g. trying to delete a directory that contains a file that cannot be removed],
802 * some of the contents of the directory may be deleted.
803 * It is an error to attempt to delete the root directory of a filesystem.
804 *
805 * @return a boolean representing success of failure
806 * @throws FileExistsException
807 * @throws NoModificationAllowedException
808 * @throws MalformedURLException
809 */
810 private boolean removeRecursively(String baseURLstr) throws FileExistsException, NoModificationAllowedException, MalformedURLException {
811 try {
812 LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
813 // You can't delete the root directory.
814 if ("".equals(inputURL.path) || "/".equals(inputURL.path)) {
815 throw new NoModificationAllowedException("You can't delete the root directory");
816 }
817
818 Filesystem fs = this.filesystemForURL(inputURL);
819 if (fs == null) {
820 throw new MalformedURLException("No installed handlers for this URL");
821 }
822 return fs.recursiveRemoveFileAtLocalURL(inputURL);
823
824 } catch (IllegalArgumentException e) {
825 MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
826 mue.initCause(e);
827 throw mue;
828 }
829 }
830
831
832 /**
833 * Deletes a file or directory. It is an error to attempt to delete a directory that is not empty.
834 * It is an error to attempt to delete the root directory of a filesystem.
835 *
836 * @return a boolean representing success of failure
837 * @throws NoModificationAllowedException
838 * @throws InvalidModificationException
839 * @throws MalformedURLException
840 */
841 private boolean remove(String baseURLstr) throws NoModificationAllowedException, InvalidModificationException, MalformedURLException {
842 try {
843 LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
844 // You can't delete the root directory.
845 if ("".equals(inputURL.path) || "/".equals(inputURL.path)) {
846
847 throw new NoModificationAllowedException("You can't delete the root directory");
848 }
849
850 Filesystem fs = this.filesystemForURL(inputURL);
851 if (fs == null) {
852 throw new MalformedURLException("No installed handlers for this URL");
853 }
854 return fs.removeFileAtLocalURL(inputURL);
855
856 } catch (IllegalArgumentException e) {
857 MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
858 mue.initCause(e);
859 throw mue;
860 }
861 }
862
863 /**
864 * Creates or looks up a file.
865 *
866 * @param baseURLstr base directory
867 * @param path file/directory to lookup or create
868 * @param options specify whether to create or not
869 * @param directory if true look up directory, if false look up file
870 * @return a Entry object
871 * @throws FileExistsException
872 * @throws IOException
873 * @throws TypeMismatchException
874 * @throws EncodingException
875 * @throws JSONException
876 */
877 private JSONObject getFile(String baseURLstr, String path, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
878 try {
879 LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
880 Filesystem fs = this.filesystemForURL(inputURL);
881 if (fs == null) {
882 throw new MalformedURLException("No installed handlers for this URL");
883 }
884 return fs.getFileForLocalURL(inputURL, path, options, directory);
885
886 } catch (IllegalArgumentException e) {
887 MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
888 mue.initCause(e);
889 throw mue;
890 }
891
892 }
893
894 /**
895 * Look up the parent DirectoryEntry containing this Entry.
896 * If this Entry is the root of its filesystem, its parent is itself.
897 */
898 private JSONObject getParent(String baseURLstr) throws JSONException, IOException {
899 try {
900 LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
901 Filesystem fs = this.filesystemForURL(inputURL);
902 if (fs == null) {
903 throw new MalformedURLException("No installed handlers for this URL");
904 }
905 return fs.getParentForLocalURL(inputURL);
906
907 } catch (IllegalArgumentException e) {
908 MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
909 mue.initCause(e);
910 throw mue;
911 }
912 }
913
914 /**
915 * Returns a File that represents the current state of the file that this FileEntry represents.
916 *
917 * @return returns a JSONObject represent a W3C File object
918 */
919 private JSONObject getFileMetadata(String baseURLstr) throws FileNotFoundException, JSONException, MalformedURLException {
920 try {
921 LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
922 Filesystem fs = this.filesystemForURL(inputURL);
923 if (fs == null) {
924 throw new MalformedURLException("No installed handlers for this URL");
925 }
926 return fs.getFileMetadataForLocalURL(inputURL);
927
928 } catch (IllegalArgumentException e) {
929 MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
930 mue.initCause(e);
931 throw mue;
932 }
933 }
934
935 /**
936 * Requests a filesystem in which to store application data.
937 *
938 * @param type of file system requested
939 * @param requiredSize required free space in the file system in bytes
940 * @param callbackContext context for returning the result or error
941 * @throws JSONException
942 */
943 private void requestFileSystem(int type, long requiredSize, final CallbackContext callbackContext) throws JSONException {
944 Filesystem rootFs = null;
945 try {
946 rootFs = this.filesystems.get(type);
947 } catch (ArrayIndexOutOfBoundsException e) {
948 // Pass null through
949 }
950 if (rootFs == null) {
951 callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_FOUND_ERR));
952 } else {
953 // If a nonzero required size was specified, check that the retrieved filesystem has enough free space.
954 long availableSize = 0;
955 if (requiredSize > 0) {
956 availableSize = rootFs.getFreeSpaceInBytes();
957 }
958
959 if (availableSize < requiredSize) {
960 callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FileUtils.QUOTA_EXCEEDED_ERR));
961 } else {
962 JSONObject fs = new JSONObject();
963 fs.put("name", rootFs.name);
964 fs.put("root", rootFs.getRootEntry());
965 callbackContext.success(fs);
966 }
967 }
968 }
969
970 /**
971 * Requests a filesystem in which to store application data.
972 *
973 * @return a JSONObject representing the file system
974 */
975 private JSONArray requestAllFileSystems() throws IOException, JSONException {
976 JSONArray ret = new JSONArray();
977 for (Filesystem fs : filesystems) {
978 ret.put(fs.getRootEntry());
979 }
980 return ret;
981 }
982
983 private static String toDirUrl(File f) {
984 return Uri.fromFile(f).toString() + '/';
985 }
986
987 private JSONObject requestAllPaths() throws JSONException {
988 Context context = cordova.getActivity();
989 JSONObject ret = new JSONObject();
990 ret.put("applicationDirectory", "file:///android_asset/");
991 ret.put("applicationStorageDirectory", toDirUrl(context.getFilesDir().getParentFile()));
992 ret.put("dataDirectory", toDirUrl(context.getFilesDir()));
993 ret.put("cacheDirectory", toDirUrl(context.getCacheDir()));
994 if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
995 try {
996 ret.put("externalApplicationStorageDirectory", toDirUrl(context.getExternalFilesDir(null).getParentFile()));
997 ret.put("externalDataDirectory", toDirUrl(context.getExternalFilesDir(null)));
998 ret.put("externalCacheDirectory", toDirUrl(context.getExternalCacheDir()));
999 ret.put("externalRootDirectory", toDirUrl(Environment.getExternalStorageDirectory()));
1000 }
1001 catch(NullPointerException e) {
1002 /* If external storage is unavailable, context.getExternal* returns null */
1003 LOG.d(LOG_TAG, "Unable to access these paths, most liklely due to USB storage");
1004 }
1005 }
1006 return ret;
1007 }
1008
1009 /**
1010 * Returns a JSON object representing the given File. Internal APIs should be modified
1011 * to use URLs instead of raw FS paths wherever possible, when interfacing with this plugin.
1012 *
1013 * @param file the File to convert
1014 * @return a JSON representation of the given File
1015 * @throws JSONException
1016 */
1017 public JSONObject getEntryForFile(File file) throws JSONException {
1018 JSONObject entry;
1019
1020 for (Filesystem fs : filesystems) {
1021 entry = fs.makeEntryForFile(file);
1022 if (entry != null) {
1023 return entry;
1024 }
1025 }
1026 return null;
1027 }
1028
1029 /**
1030 * Returns a JSON object representing the given File. Deprecated, as this is only used by
1031 * FileTransfer, and because it is a static method that should really be an instance method,
1032 * since it depends on the actual filesystem roots in use. Internal APIs should be modified
1033 * to use URLs instead of raw FS paths wherever possible, when interfacing with this plugin.
1034 *
1035 * @param file the File to convert
1036 * @return a JSON representation of the given File
1037 * @throws JSONException
1038 */
1039 @Deprecated
1040 public static JSONObject getEntry(File file) throws JSONException {
1041 if (getFilePlugin() != null) {
1042 return getFilePlugin().getEntryForFile(file);
1043 }
1044 return null;
1045 }
1046
1047 /**
1048 * Read the contents of a file.
1049 * This is done in a background thread; the result is sent to the callback.
1050 *
1051 * @param start Start position in the file.
1052 * @param end End position to stop at (exclusive).
1053 * @param callbackContext The context through which to send the result.
1054 * @param encoding The encoding to return contents as. Typical value is UTF-8. (see http://www.iana.org/assignments/character-sets)
1055 * @param resultType The desired type of data to send to the callback.
1056 * @return Contents of file.
1057 */
1058 public void readFileAs(final String srcURLstr, final int start, final int end, final CallbackContext callbackContext, final String encoding, final int resultType) throws MalformedURLException {
1059 try {
1060 LocalFilesystemURL inputURL = LocalFilesystemURL.parse(srcURLstr);
1061 Filesystem fs = this.filesystemForURL(inputURL);
1062 if (fs == null) {
1063 throw new MalformedURLException("No installed handlers for this URL");
1064 }
1065
1066 fs.readFileAtURL(inputURL, start, end, new Filesystem.ReadFileCallback() {
1067 public void handleData(InputStream inputStream, String contentType) {
1068 try {
1069 ByteArrayOutputStream os = new ByteArrayOutputStream();
1070 final int BUFFER_SIZE = 8192;
1071 byte[] buffer = new byte[BUFFER_SIZE];
1072
1073 for (;;) {
1074 int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE);
1075
1076 if (bytesRead <= 0) {
1077 break;
1078 }
1079 os.write(buffer, 0, bytesRead);
1080 }
1081
1082 PluginResult result;
1083 switch (resultType) {
1084 case PluginResult.MESSAGE_TYPE_STRING:
1085 result = new PluginResult(PluginResult.Status.OK, os.toString(encoding));
1086 break;
1087 case PluginResult.MESSAGE_TYPE_ARRAYBUFFER:
1088 result = new PluginResult(PluginResult.Status.OK, os.toByteArray());
1089 break;
1090 case PluginResult.MESSAGE_TYPE_BINARYSTRING:
1091 result = new PluginResult(PluginResult.Status.OK, os.toByteArray(), true);
1092 break;
1093 default: // Base64.
1094 byte[] base64 = Base64.encode(os.toByteArray(), Base64.NO_WRAP);
1095 String s = "data:" + contentType + ";base64," + new String(base64, "US-ASCII");
1096 result = new PluginResult(PluginResult.Status.OK, s);
1097 }
1098
1099 callbackContext.sendPluginResult(result);
1100 } catch (IOException e) {
1101 LOG.d(LOG_TAG, e.getLocalizedMessage());
1102 callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_READABLE_ERR));
1103 }
1104 }
1105 });
1106
1107
1108 } catch (IllegalArgumentException e) {
1109 MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
1110 mue.initCause(e);
1111 throw mue;
1112 } catch (FileNotFoundException e) {
1113 callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_FOUND_ERR));
1114 } catch (IOException e) {
1115 LOG.d(LOG_TAG, e.getLocalizedMessage());
1116 callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_READABLE_ERR));
1117 }
1118 }
1119
1120
1121 /**
1122 * Write contents of file.
1123 *
1124 * @param data The contents of the file.
1125 * @param offset The position to begin writing the file.
1126 * @param isBinary True if the file contents are base64-encoded binary data
1127 */
1128 /**/
1129 public long write(String srcURLstr, String data, int offset, boolean isBinary) throws FileNotFoundException, IOException, NoModificationAllowedException {
1130 try {
1131 LocalFilesystemURL inputURL = LocalFilesystemURL.parse(srcURLstr);
1132 Filesystem fs = this.filesystemForURL(inputURL);
1133 if (fs == null) {
1134 throw new MalformedURLException("No installed handlers for this URL");
1135 }
1136
1137 long x = fs.writeToFileAtURL(inputURL, data, offset, isBinary); LOG.d("TEST",srcURLstr + ": "+x); return x;
1138 } catch (IllegalArgumentException e) {
1139 MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
1140 mue.initCause(e);
1141 throw mue;
1142 }
1143
1144 }
1145
1146 /**
1147 * Truncate the file to size
1148 */
1149 private long truncateFile(String srcURLstr, long size) throws FileNotFoundException, IOException, NoModificationAllowedException {
1150 try {
1151 LocalFilesystemURL inputURL = LocalFilesystemURL.parse(srcURLstr);
1152 Filesystem fs = this.filesystemForURL(inputURL);
1153 if (fs == null) {
1154 throw new MalformedURLException("No installed handlers for this URL");
1155 }
1156
1157 return fs.truncateFileAtURL(inputURL, size);
1158 } catch (IllegalArgumentException e) {
1159 MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
1160 mue.initCause(e);
1161 throw mue;
1162 }
1163 }
1164
1165
1166 /*
1167 * Handle the response
1168 */
1169
1170 public void onRequestPermissionResult(int requestCode, String[] permissions,
1171 int[] grantResults) throws JSONException {
1172
1173 final PendingRequests.Request req = pendingRequests.getAndRemove(requestCode);
1174 if (req != null) {
1175 for(int r:grantResults)
1176 {
1177 if(r == PackageManager.PERMISSION_DENIED)
1178 {
1179 req.getCallbackContext().sendPluginResult(new PluginResult(PluginResult.Status.ERROR, SECURITY_ERR));
1180 return;
1181 }
1182 }
1183 switch(req.getAction())
1184 {
1185 case ACTION_GET_FILE:
1186 threadhelper( new FileOp( ){
1187 public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
1188 String dirname = args.getString(0);
1189
1190 String path = args.getString(1);
1191 JSONObject obj = getFile(dirname, path, args.optJSONObject(2), false);
1192 req.getCallbackContext().success(obj);
1193 }
1194 }, req.getRawArgs(), req.getCallbackContext());
1195 break;
1196 case ACTION_GET_DIRECTORY:
1197 threadhelper( new FileOp( ){
1198 public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
1199 String dirname = args.getString(0);
1200
1201 String path = args.getString(1);
1202 JSONObject obj = getFile(dirname, path, args.optJSONObject(2), true);
1203 req.getCallbackContext().success(obj);
1204 }
1205 }, req.getRawArgs(), req.getCallbackContext());
1206 break;
1207 case ACTION_WRITE:
1208 threadhelper( new FileOp( ){
1209 public void run(JSONArray args) throws JSONException, FileNotFoundException, IOException, NoModificationAllowedException {
1210 String fname=args.getString(0);
1211 String data=args.getString(1);
1212 int offset=args.getInt(2);
1213 Boolean isBinary=args.getBoolean(3);
1214 long fileSize = write(fname, data, offset, isBinary);
1215 req.getCallbackContext().sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
1216 }
1217 }, req.getRawArgs(), req.getCallbackContext());
1218 break;
1219 }
1220 } else {
1221 LOG.d(LOG_TAG, "Received permission callback for unknown request code");
1222 }
1223 }
1224}