/* * PhoneGap is available under *either* the terms of the modified BSD license *or* the * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text. * * Copyright (c) 2005-2010, Nitobi Software Inc. * Copyright (c) 2010-2011, IBM Corporation */ package com.phonegap; import java.io.*; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.nio.channels.FileChannel; import org.apache.commons.codec.binary.Base64; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.database.Cursor; import android.net.Uri; import android.os.Environment; import android.provider.MediaStore; import android.util.Log; import android.webkit.MimeTypeMap; import com.phonegap.api.Plugin; import com.phonegap.api.PluginResult; import com.phonegap.file.EncodingException; import com.phonegap.file.FileExistsException; import com.phonegap.file.InvalidModificationException; import com.phonegap.file.NoModificationAllowedException; import com.phonegap.file.TypeMismatchException; /** * This class provides SD card file and directory services to JavaScript. * Only files on the SD card can be accessed. */ public class FileUtils extends Plugin { 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 TEMPORARY = 0; public static int PERSISTENT = 1; public static int RESOURCE = 2; public static int APPLICATION = 3; FileReader f_in; FileWriter f_out; /** * Constructor. */ public FileUtils() { } /** * Executes the request and returns PluginResult. * * @param action The action to execute. * @param args JSONArry of arguments for the plugin. * @param callbackId The callback id used when calling back into JavaScript. * @return A PluginResult object with a status and message. */ public PluginResult execute(String action, JSONArray args, String callbackId) { PluginResult.Status status = PluginResult.Status.OK; String result = ""; //System.out.println("FileUtils.execute("+action+")"); try { try { if (action.equals("testSaveLocationExists")) { boolean b = DirectoryManager.testSaveLocationExists(); return new PluginResult(status, b); } else if (action.equals("getFreeDiskSpace")) { long l = DirectoryManager.getFreeDiskSpace(); return new PluginResult(status, l); } else if (action.equals("testFileExists")) { boolean b = DirectoryManager.testFileExists(args.getString(0)); return new PluginResult(status, b); } else if (action.equals("testDirectoryExists")) { boolean b = DirectoryManager.testFileExists(args.getString(0)); return new PluginResult(status, b); } else if (action.equals("readAsText")) { String s = this.readAsText(args.getString(0), args.getString(1)); return new PluginResult(status, s); } else if (action.equals("readAsDataURL")) { String s = this.readAsDataURL(args.getString(0)); return new PluginResult(status, s); } else if (action.equals("write")) { long fileSize = this.write(args.getString(0), args.getString(1), args.getInt(2)); return new PluginResult(status, fileSize); } else if (action.equals("truncate")) { long fileSize = this.truncateFile(args.getString(0), args.getLong(1)); return new PluginResult(status, fileSize); } else if (action.equals("requestFileSystem")) { long size = args.optLong(1); if (size != 0) { if (size > DirectoryManager.getFreeDiskSpace()) { JSONObject error = new JSONObject().put("code", FileUtils.QUOTA_EXCEEDED_ERR); return new PluginResult(PluginResult.Status.ERROR, error); } } JSONObject obj = requestFileSystem(args.getInt(0)); return new PluginResult(status, obj, "window.localFileSystem._castFS"); } else if (action.equals("resolveLocalFileSystemURI")) { JSONObject obj = resolveLocalFileSystemURI(args.getString(0)); return new PluginResult(status, obj, "window.localFileSystem._castEntry"); } else if (action.equals("getMetadata")) { JSONObject obj = getMetadata(args.getString(0)); return new PluginResult(status, obj, "window.localFileSystem._castDate"); } else if (action.equals("getFileMetadata")) { JSONObject obj = getFileMetadata(args.getString(0)); return new PluginResult(status, obj, "window.localFileSystem._castDate"); } else if (action.equals("getParent")) { JSONObject obj = getParent(args.getString(0)); return new PluginResult(status, obj, "window.localFileSystem._castEntry"); } else if (action.equals("getDirectory")) { JSONObject obj = getFile(args.getString(0), args.getString(1), args.optJSONObject(2), true); return new PluginResult(status, obj, "window.localFileSystem._castEntry"); } else if (action.equals("getFile")) { JSONObject obj = getFile(args.getString(0), args.getString(1), args.optJSONObject(2), false); return new PluginResult(status, obj, "window.localFileSystem._castEntry"); } else if (action.equals("remove")) { boolean success; success = remove(args.getString(0)); if (success) { return new PluginResult(status); } else { JSONObject error = new JSONObject().put("code", FileUtils.NO_MODIFICATION_ALLOWED_ERR); return new PluginResult(PluginResult.Status.ERROR, error); } } else if (action.equals("removeRecursively")) { boolean success = removeRecursively(args.getString(0)); if (success) { return new PluginResult(status); } else { JSONObject error = new JSONObject().put("code", FileUtils.NO_MODIFICATION_ALLOWED_ERR); return new PluginResult(PluginResult.Status.ERROR, error); } } else if (action.equals("moveTo")) { JSONObject entry = transferTo(args.getString(0), args.getJSONObject(1), args.optString(2), true); return new PluginResult(status, entry, "window.localFileSystem._castEntry"); } else if (action.equals("copyTo")) { JSONObject entry = transferTo(args.getString(0), args.getJSONObject(1), args.optString(2), false); return new PluginResult(status, entry, "window.localFileSystem._castEntry"); } else if (action.equals("readEntries")) { JSONArray entries = readEntries(args.getString(0)); return new PluginResult(status, entries, "window.localFileSystem._castEntries"); } return new PluginResult(status, result); } catch (FileNotFoundException e) { JSONObject error = new JSONObject().put("code", FileUtils.NOT_FOUND_ERR); return new PluginResult(PluginResult.Status.ERROR, error); } catch (FileExistsException e) { JSONObject error = new JSONObject().put("code", FileUtils.PATH_EXISTS_ERR); return new PluginResult(PluginResult.Status.ERROR, error); } catch (NoModificationAllowedException e) { JSONObject error = new JSONObject().put("code", FileUtils.NO_MODIFICATION_ALLOWED_ERR); return new PluginResult(PluginResult.Status.ERROR, error); } catch (JSONException e) { JSONObject error = new JSONObject().put("code", FileUtils.NO_MODIFICATION_ALLOWED_ERR); return new PluginResult(PluginResult.Status.ERROR, error); } catch (InvalidModificationException e) { JSONObject error = new JSONObject().put("code", FileUtils.INVALID_MODIFICATION_ERR); return new PluginResult(PluginResult.Status.ERROR, error); } catch (MalformedURLException e) { JSONObject error = new JSONObject().put("code", FileUtils.ENCODING_ERR); return new PluginResult(PluginResult.Status.ERROR, error); } catch (IOException e) { JSONObject error = new JSONObject().put("code", FileUtils.INVALID_MODIFICATION_ERR); return new PluginResult(PluginResult.Status.ERROR, error); } catch (EncodingException e) { JSONObject error = new JSONObject().put("code", FileUtils.ENCODING_ERR); return new PluginResult(PluginResult.Status.ERROR, error); } catch (TypeMismatchException e) { JSONObject error = new JSONObject().put("code", FileUtils.TYPE_MISMATCH_ERR); return new PluginResult(PluginResult.Status.ERROR, error); } } catch (JSONException e) { e.printStackTrace(); return new PluginResult(PluginResult.Status.JSON_EXCEPTION); } } /** * Allows the user to look up the Entry for a file or directory referred to by a local URI. * * @param url 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 url) throws IOException, JSONException { String decoded = URLDecoder.decode(url, "UTF-8"); File fp = null; // Handle the special case where you get an Android content:// uri. if (decoded.startsWith("content:")) { Cursor cursor = this.ctx.managedQuery(Uri.parse(decoded), new String[] { MediaStore.Images.Media.DATA }, null, null, null); // Note: MediaStore.Images/Audio/Video.Media.DATA is always "_data" int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); cursor.moveToFirst(); fp = new File(cursor.getString(column_index)); } else { // Test to see if this is a valid URL first @SuppressWarnings("unused") URL testUrl = new URL(decoded); if (decoded.startsWith("file://")) { fp = new File(decoded.substring(7, decoded.length())); } else { fp = new File(decoded); } } if (!fp.exists()) { throw new FileNotFoundException(); } if (!fp.canRead()) { throw new IOException(); } return getEntry(fp); } /** * Read the list of files from this directory. * * @param fileName the directory to read from * @return a JSONArray containing JSONObjects that represent Entry objects. * @throws FileNotFoundException if the directory is not found. * @throws JSONException */ private JSONArray readEntries(String fileName) throws FileNotFoundException, JSONException { File fp = new File(fileName); if (!fp.exists()) { // The directory we are listing doesn't exist so we should fail. throw new FileNotFoundException(); } JSONArray entries = new JSONArray(); if (fp.isDirectory()) { File[] files = fp.listFiles(); for (int i=0; i 0) { throw new InvalidModificationException("directory is not empty"); } } // Try to rename the directory if (!srcDir.renameTo(destinationDir)) { // Trying to rename the directory failed. Possibly because we moved across file system on the device. // Now we have to do things the hard way // 1) Copy all the old files // 2) delete the src directory } return getEntry(destinationDir); } /** * 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. * * @param filePath the directory to be removed * @return a boolean representing success of failure * @throws FileExistsException */ private boolean removeRecursively(String filePath) throws FileExistsException { File fp = new File(filePath); // You can't delete the root directory. if (atRootDirectory(filePath)) { return false; } return removeDirRecursively(fp); } /** * Loops through a directory deleting all the files. * * @param directory to be removed * @return a boolean representing success of failure * @throws FileExistsException */ private 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; } } /** * 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. * * @param filePath file or directory to be removed * @return a boolean representing success of failure * @throws NoModificationAllowedException * @throws InvalidModificationException */ private boolean remove(String filePath) throws NoModificationAllowedException, InvalidModificationException { File fp = new File(filePath); // You can't delete the root directory. if (atRootDirectory(filePath)) { throw new NoModificationAllowedException("You can't delete the root directory"); } // 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(); } /** * Creates or looks up a file. * * @param dirPath base directory * @param fileName 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 dirPath, String fileName, 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 (fileName.contains(":")) { throw new EncodingException("This file has a : in it's name"); } File fp = createFileObject(dirPath, fileName); 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 getEntry(fp); } /** * If the path starts with a '/' just return that file object. If not construct the file * object from the path passed in and the file name. * * @param dirPath root directory * @param fileName new file name * @return */ private File createFileObject(String dirPath, String fileName) { File fp = null; if (fileName.startsWith("/")) { fp = new File(fileName); } else { fp = new File(dirPath + File.separator + fileName); } return fp; } /** * Look up the parent DirectoryEntry containing this Entry. * If this Entry is the root of its filesystem, its parent is itself. * * @param filePath * @return * @throws JSONException */ private JSONObject getParent(String filePath) throws JSONException { if (atRootDirectory(filePath)) { return getEntry(filePath); } return getEntry(new File(filePath).getParent()); } /** * Checks to see if we are at the root directory. Useful since we are * not allow to delete this directory. * * @param filePath to directory * @return true if we are at the root, false otherwise. */ private boolean atRootDirectory(String filePath) { if (filePath.equals(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + ctx.getPackageName() + "/cache") || filePath.equals(Environment.getExternalStorageDirectory().getAbsolutePath())) { return true; } return false; } /** * Look up metadata about this entry. * * @param filePath to entry * @return a Metadata object * @throws FileNotFoundException * @throws JSONException */ private JSONObject getMetadata(String filePath) throws FileNotFoundException, JSONException { File file = new File(filePath); if (!file.exists()) { throw new FileNotFoundException("Failed to find file in getMetadata"); } JSONObject metadata = new JSONObject(); metadata.put("modificationTime", file.lastModified()); return metadata; } /** * Returns a File that represents the current state of the file that this FileEntry represents. * * @param filePath to entry * @return returns a JSONObject represent a W3C File object * @throws FileNotFoundException * @throws JSONException */ private JSONObject getFileMetadata(String filePath) throws FileNotFoundException, JSONException { File file = new File(filePath); if (!file.exists()) { throw new FileNotFoundException("File: " + filePath + " does not exist."); } JSONObject metadata = new JSONObject(); metadata.put("size", file.length()); metadata.put("type", getMimeType(filePath)); metadata.put("name", file.getName()); metadata.put("fullPath", file.getAbsolutePath()); metadata.put("lastModifiedDate", file.lastModified()); return metadata; } /** * 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(); if (type == TEMPORARY) { if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { fs.put("name", "temporary"); fs.put("root", getEntry(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + ctx.getPackageName() + "/cache/")); // Create the cache dir if it doesn't exist. File fp = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + ctx.getPackageName() + "/cache/"); fp.mkdirs(); } else { throw new IOException("SD Card not mounted"); } } else if (type == PERSISTENT) { if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { fs.put("name", "persistent"); fs.put("root", getEntry(Environment.getExternalStorageDirectory())); } else { throw new IOException("SD Card not mounted"); } } else if (type == RESOURCE) { fs.put("name", "resource"); } else if (type == APPLICATION) { fs.put("name", "application"); } else { throw new IOException("No filesystem of type requested"); } return fs; } /** * Returns a JSON Object representing a directory on the device's file system * * @param path to the directory * @return * @throws JSONException */ private JSONObject getEntry(File file) throws JSONException { JSONObject entry = new JSONObject(); entry.put("isFile", file.isFile()); entry.put("isDirectory", file.isDirectory()); entry.put("name", file.getName()); entry.put("fullPath", file.getAbsolutePath()); // I can't add the next thing it as it would be an infinite loop //entry.put("filesystem", null); return entry; } /** * Returns a JSON Object representing a directory on the device's file system * * @param path to the directory * @return * @throws JSONException */ private JSONObject getEntry(String path) throws JSONException { return getEntry(new File(path)); } /** * Identifies if action to be executed returns a value and should be run synchronously. * * @param action The action to execute * @return T=returns value */ public boolean isSynch(String action) { if (action.equals("testSaveLocationExists")) { return true; } else if (action.equals("getFreeDiskSpace")) { return true; } else if (action.equals("testFileExists")) { return true; } else if (action.equals("testDirectoryExists")) { return true; } return false; } //-------------------------------------------------------------------------- // LOCAL METHODS //-------------------------------------------------------------------------- /** * Read content of text file. * * @param filename The name of the file. * @param encoding The encoding to return contents as. Typical value is UTF-8. * (see http://www.iana.org/assignments/character-sets) * @return Contents of file. * @throws FileNotFoundException, IOException */ public String readAsText(String filename, String encoding) throws FileNotFoundException, IOException { byte[] bytes = new byte[1000]; BufferedInputStream bis = new BufferedInputStream(getPathFromUri(filename), 1024); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int numRead = 0; while ((numRead = bis.read(bytes, 0, 1000)) >= 0) { bos.write(bytes, 0, numRead); } return new String(bos.toByteArray(), encoding); } /** * Read content of text file and return as base64 encoded data url. * * @param filename The name of the file. * @return Contents of file = data:;base64, * @throws FileNotFoundException, IOException */ public String readAsDataURL(String filename) throws FileNotFoundException, IOException { byte[] bytes = new byte[1000]; BufferedInputStream bis = new BufferedInputStream(getPathFromUri(filename), 1024); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int numRead = 0; while ((numRead = bis.read(bytes, 0, 1000)) >= 0) { bos.write(bytes, 0, numRead); } // Determine content type from file name String contentType = null; if (filename.startsWith("content:")) { Uri fileUri = Uri.parse(filename); contentType = this.ctx.getContentResolver().getType(fileUri); } else { contentType = getMimeType(filename); } byte[] base64 = Base64.encodeBase64(bos.toByteArray()); String data = "data:" + contentType + ";base64," + new String(base64); return data; } /** * Looks up the mime type of a given file name. * * @param filename * @return a mime type */ public static String getMimeType(String filename) { MimeTypeMap map = MimeTypeMap.getSingleton(); return map.getMimeTypeFromExtension(map.getFileExtensionFromUrl(filename)); } /** * Write contents of file. * * @param filename The name of the file. * @param data The contents of the file. * @param offset The position to begin writing the file. * @throws FileNotFoundException, IOException */ /**/ public long write(String filename, String data, int offset) throws FileNotFoundException, IOException { boolean append = false; if (offset > 0) { this.truncateFile(filename, offset); append = true; } byte [] rawData = data.getBytes(); ByteArrayInputStream in = new ByteArrayInputStream(rawData); FileOutputStream out = new FileOutputStream(filename, append); byte buff[] = new byte[rawData.length]; in.read(buff, 0, buff.length); out.write(buff, 0, rawData.length); out.flush(); out.close(); return data.length(); } /** * Truncate the file to size * * @param filename * @param size * @throws FileNotFoundException, IOException */ private long truncateFile(String filename, long size) throws FileNotFoundException, IOException { RandomAccessFile raf = new RandomAccessFile(filename, "rw"); if (raf.length() >= size) { FileChannel channel = raf.getChannel(); channel.truncate(size); return size; } return raf.length(); } /** * Get an input stream based on file path or content:// uri * * @param path * @return an input stream * @throws FileNotFoundException */ private InputStream getPathFromUri(String path) throws FileNotFoundException { if (path.startsWith("content")) { Uri uri = Uri.parse(path); return ctx.getContentResolver().openInputStream(uri); } else { return new FileInputStream(path); } } }